MCPサーバーは、関数を一つ公開すれば完成するものではありません。入力契約、権限、実行経路、失敗時の返し方までそろって、はじめてAIクライアントから安全に利用できます。本稿では顧客向けFAQを検索する読み取り専用サーバーを題材に、Python環境の固定からInspectorによる異常系確認までを一続きで実装します。
MCPサーバーの完成状態を「呼び出せる」より先まで定義する
今回作るのは、社内で承認済みのFAQデータを検索し、候補を最大5件返す小さなMCPサーバーです。データの更新、顧客情報の取得、外部サイトへのアクセスは行いません。最初の題材を読み取り専用にするのは、実装を簡単に見せるためではなく、入力ミスやモデルの誤判断が業務データを変更する経路を最初から閉じるためです。
完成条件は四つあります。第一に、PythonとSDKの版を別の担当者が再現できること。第二に、ツールの名前、説明、入力スキーマ、戻り値が一致すること。第三に、正常値だけでなく空文字、上限超過、該当なしを確認すること。第四に、標準出力へMCPメッセージ以外を書かず、障害調査に必要なログを標準エラーへ分離することです。これらを満たして初めて、クライアント接続の検証へ進みます。
作業の区切りは「コードを書いた」ではなく、「同じ固定環境で、正常系と異常系の証跡を第三者が再実行できた」です。
| 項目 | 今回の決定 | 検証証跡 | 未達なら止める場面 |
|---|---|---|---|
| 提供機能 | 承認済みFAQの部分一致検索 | 代表3件と該当なし1件の結果 | 更新処理や外部通信が混在した |
| 入力 | queryは1〜100文字、limitは1〜5 | 境界値テストの記録 | 空文字を検索として受理した |
| 出力 | 件数とFAQの配列をJSON互換値で返す | Inspectorの応答保存 | 内部パスやスタックを返した |
| 運用 | stdio、ローカル実行、読み取り専用 | 起動コマンドと版一覧 | 本番権限の資格情報が必要になった |
MCPの現行仕様は2026年7月28日版です。Python SDKの公式文書はPython 3.10以上とSDK 2.0.0以上を前提にしています。古い記事にある初期化ハンドシェイクや以前のクラス名をそのまま写すと、現行SDKと設計が食い違う可能性があります。仕様書とSDK文書を同じ確認日に開き、採用した版を受け入れ条件シートへ残します[1][2]。
Python、uv、SDKの実体をコマンドで固定する
開発端末にPythonが入っているだけでは再現条件になりません。シェルが参照する実行ファイル、仮想環境、SDKの実インストール版を記録します。以下はPowerShellまたは一般的な端末で確認できる例です。出力はチケットや検証記録へ貼り、担当者の記憶で版を補わないようにします。
環境を固定した後は、空の端末またはコンテナーでロックファイルから復元する試験を一度行います。開発者の端末に偶然残ったパッケージへ依存していると、別担当者の環境で初めて失敗します。復元時間、取得先、失敗した依存物を記録し、ネットワークが必要な工程と実行時に不要な工程を分けます。
Pythonのマイナー版を上げる場合もSDK更新と同じ変更として扱います。型の評価、標準ライブラリ、ネイティブ依存が変わる可能性があるためです。採用版を一覧へ書くだけでなく、uv run python -m compileall .、単体試験、Inspector試験を再実行する入口をCIに置くと、担当者が更新手順を忘れても検出できます。
python --versionuv --versionuv init faq-mcp-servercd faq-mcp-serveruv add "mcp[cli]>=2.0.0,<3"uv run python -c "import importlib.metadata as m; print(m.version('mcp'))"uv tree
mcp>=2.0.0,<3は本稿の想定範囲であり、すべての組織に共通する推奨値ではありません。再現性を重視するリリースでは、検証を通した正確な版をロックファイルで固定します。上限だけを設けず自動更新すると、SDKの新機能だけでなく型、既定値、依存パッケージも同時に変わり得ます。更新は別ブランチで行い、後述の検証を再実行します。
プロジェクトには少なくともserver.py、faq.json、tests/、uv.lockを置きます。FAQファイルに個人情報や未公開情報を入れないでください。サンプルデータのつもりで本番データを複製すると、後でリポジトリの履歴から削除する作業が必要になります。テスト用データは架空の内容を新規作成し、出所と利用範囲をREADMEではなくデータ管理台帳にも記録します。
Windowsでの確認:Get-Command pythonとGet-Command uvで実体のパスを確認します。複数のPythonがある端末では、表示された版だけでなく実行ファイルの場所が再現性を左右します。
SDKを入れられない閉域端末では、非公式な配布ファイルを持ち込んで先へ進めないでください。社内の依存パッケージ審査、ハッシュ確認、ライセンス確認を済ませたミラーを用意できない場合、この手順は開始条件を満たしていません。
ツールの説明文と入出力契約を先に決める
モデルはツール名と説明を手掛かりに呼び出しを選びます。曖昧な説明は、コード上のバグがなくても誤利用を誘発します。search_faqの説明には、対象が承認済みFAQであること、部分一致であること、顧客個別の回答を生成しないことを明記します。「検索する便利なツール」のような説明では、ナレッジ全般を探せると誤認されます。
検索語の正規化も契約に含めます。全角・半角、大文字・小文字、前後空白をどこまで同一視するかを決め、利用者には原文を返しません。入力をログへ残す必要がある場合は、ハッシュ化だけで十分か、再現用の匿名データを別管理するかを情報管理担当と決めます。FAQに顧客名が混ざる可能性があるなら、検索語の全文記録を既定にしない方が安全です。
ツールの説明には副作用の有無も明記します。将来、検索結果を閲覧履歴へ記録するだけでも、純粋な読み取りとは異なる扱いが必要です。モデルが何度呼んでも同じ業務結果になるか、失敗後の再試行で重複記録が生じないかを確認します。更新系を追加するなら検索ツールへ隠して入れず、承認と冪等性を持つ別ツールとして審査します。
入力制約はサーバー側で必ず再検証します。クライアントがスキーマを表示していても、モデルや別クライアントが制約外の値を送る可能性は残ります。空白だけの文字列、巨大なlimit、想定外の文字コードを受けた場合に、無制限検索へフォールバックしてはいけません。業務上の入力エラーとして短い説明を返し、秘密情報を含む例外メッセージはログ側へ送ります。
| 契約要素 | 良い決め方 | 避ける決め方 |
|---|---|---|
| 名前 | 動詞と対象が分かるsearch_faq | runやhelperのように用途が広い |
| 説明 | 検索対象、方式、非対応範囲を一文ずつ示す | 利用場面をモデルに推測させる |
| 入力 | 文字数、件数、既定値をサーバーで検査する | 画面側の制約だけを信頼する |
| 出力 | 件数、識別子、題名、要約を固定キーで返す | 成功時ごとに文章の形が変わる |
| 失敗 | 利用者向け理由と運用ログを分離する | スタックトレースを応答へ載せる |
戻り値にFAQ本文全体を含める必要がなければ、短い要約と識別子だけを返します。モデルへ渡す情報量が減り、誤って内部注記を表示する経路も狭くなります。完全な内容が必要な場合は、検索と取得を別ツールまたはResourceに分け、取得時に改めて権限を確認する設計が候補になります。
読み取り専用の最小サーバーを実装する
次の例は構造を理解するための最小実装です。公式Python SDKの現行例に合わせてMCPServerを使います。実際の業務データを扱う前に、架空FAQだけで起動と契約を確認してください。環境によって型検証の表現や起動オプションが変わるため、インストール済みSDKのリファレンスも併読します[2]。
FAQデータの受入検査では、識別子の重複、必須キーの欠落、文字コード、最大件数、想定外のHTMLを確認します。表示先がHTMLを解釈する場合、データに含まれるタグやリンクを無害化する責任はクライアント任せにできません。サーバーは構造化値を返し、最終表示側で文脈に合ったエスケープを行います。
件数が増えると線形検索は遅くなります。想定最大件数の架空データで応答時間を測り、上限を超えたときは起動警告ではなく配布工程を拒否します。データベースへ移す場合は、接続権限をSELECTへ限定し、クエリのタイムアウトと結果上限を設定します。実装方式を変えても、ツールの公開契約を維持できるか回帰試験で確かめます。
from mcp.server import MCPServermcp = MCPServer("approved-faq")FAQS = [ {"id": "F-001", "title": "請求書の再発行", "summary": "管理画面から再発行を申請できます。"}, {"id": "F-002", "title": "契約担当者の変更", "summary": "本人確認後に変更手続きを案内します。"},]@mcp.tool()def search_faq(query: str, limit: int = 5) -> dict: """承認済みFAQを部分一致で検索する。個別回答は生成しない。""" normalized = query.strip() if not 1 <= len(normalized) <= 100: raise ValueError("query must contain 1 to 100 characters") if not 1 <= limit <= 5: raise ValueError("limit must be between 1 and 5") hits = [ item for item in FAQS if normalized.lower() in (item["title"] + item["summary"]).lower() ][:limit] return {"count": len(hits), "items": hits}if __name__ == "__main__": mcp.run(transport="stdio")
例外の種類は、SDKがクライアントへどう変換するかを確認してから決めます。業務では独自のエラーコードを付け、入力不正、権限不足、一時障害、内部障害を区別すると調査しやすくなります。ただし、データベース名、ファイルパス、SQL、資格情報の断片をエラー本文に入れないでください。
stdioでは標準入力と標準出力がプロトコルの通信路です。print()によるデバッグ文字列を標準出力へ混ぜると、クライアントはJSON-RPCメッセージとして解釈できず接続を失います。ログはPythonのloggingで標準エラーへ送り、端末上で時刻、レベル、イベント名、相関IDを追えるようにします。パスワード、アクセストークン、FAQの全文はログ対象から外します。現行のstdio仕様も、標準出力へMCP以外を書かないことを要求しています[3]。
import loggingimport syslogging.basicConfig( stream=sys.stderr, level=logging.INFO, format="%(asctime)s %(levelname)s event=%(message)s",)logger = logging.getLogger("approved-faq")
データをファイルから読む場合は、起動時に存在とスキーマを検証し、不正ならサーバーを明示的に停止します。壊れたデータを空配列として扱うと、「該当なし」という正常応答に見え、障害の発見が遅れます。ファイル更新が必要なら別の承認済み配布工程で差し替え、ツール呼び出しのたびに外部URLから取得する構造は避けます。
Inspectorで一覧、呼び出し、境界値を確かめる
公式Inspectorは、Resources、Prompts、Tools、通知を個別に確認できる対話型の検証ツールです。Python SDKの開発コマンドから起動する方法と、任意のサーバーコマンドをInspectorへ渡す方法があります。Node.jsとnpxが必要になる構成では、その版も記録します[4]。
試験記録にはスクリーンショットだけでなく、入力と応答を機密情報を除いてJSONで保存します。画面の表示はInspectorの更新で変わっても、契約値なら自動比較できます。日時、OS、Python、SDK、Inspector、サーバーのコミットを一つのrun IDへ結び付けると、同じ名前の試験を別版で実行した結果を混同しません。
CIでは、サーバープロセスを起動し、ツール発見、正常呼び出し、入力拒否、終了を自動化します。プロセスが残留しないこと、タイムアウト時に強制終了できることも検査します。外部データへ接続する統合試験は、専用の読取環境と短命な資格情報を使い、プルリクエストから本番DBへ到達させません。
node --versionnpx --versionuv run mcp dev server.py# サーバーコマンドを明示してInspectorを起動する例npx @modelcontextprotocol/inspector uv run python server.py
画面が開いたら、接続成功だけで終了しないでください。Tools一覧にsearch_faqが一つだけ表示され、説明と入力項目が設計表どおりであることを読み合わせます。その後、「請求書」、存在しない語、空文字、101文字、limit=0、limit=6を試します。入力エラーでサーバープロセスが落ちず、次の正常呼び出しを受けられることまで確認します。
| 試験ID | 入力 | 期待結果 | 判定に残す証跡 |
|---|---|---|---|
| T-01 | query=請求書, limit=3 | F-001を含み、件数と配列が一致 | 要求・応答、SDK版、実行時刻 |
| T-02 | 存在しない語 | count=0、空配列 | エラーではないこと |
| T-03 | 空白だけ | 入力不正、内部情報なし | エラー分類と継続稼働 |
| T-04 | limit=6 | 上限違反として拒否 | サーバーログとの相関 |
| T-05 | 異常後に正常入力 | 再起動せず正常応答 | 同一プロセスでの連続結果 |
自動テストでは、検索ロジックの単体試験とMCP経由の契約試験を分けます。前者は文字列処理や上限を高速に検査し、後者はツール発見とシリアライズを含めて検査します。テストがSDK内部の細部へ過度に依存すると更新のたびに壊れるため、公開契約である名前、スキーマ、結果、エラー分類を中心に据えます。
失敗時は、最初に標準出力への余計な文字列、実行ディレクトリ、仮想環境、SDK版を確認します。次にInspectorが実際に起動したコマンドを見て、手元で成功したシェルと同じか比べます。推測で依存パッケージを更新すると原因が増えるので、固定版のまま一つずつ切り分けます。
業務クライアントへつなぐ前に権限と配置を見直す
Inspectorで成功しても、本番クライアントへ直結する判断には足りません。クライアントはサーバープロセスをどのユーザー権限で起動するか、作業ディレクトリをどこにするか、環境変数をどこから渡すかを確認します。MCP設定ファイルへ資格情報を平文で書く構成は採用せず、OSの資格情報管理または組織のシークレット管理を使います。
限定試験では、許可する利用者とクライアント版を記録し、サーバーの起動回数、ツール呼び出し件数、入力拒否、異常終了を観察します。モデルが期待どおりの場面でツールを選ぶかは、サーバー単体試験とは別の評価です。FAQを尋ねていない依頼で検索を繰り返すなら、説明文、クライアントの指示、利用モデルのいずれに原因があるかを分けます。
利用者には、検索結果が承認済みFAQの候補であり、顧客個別の最終回答ではないことを画面で伝えます。サーバーの説明文だけに注意事項を置くと、最終利用者へ届かない可能性があります。試験中の誤回答を報告する入口と、即時に接続設定を無効化できる担当者を設定してから利用を始めます。
ローカルのstdioサーバーは、起動したユーザーが読めるファイルや到達できるネットワークへ同じ権限でアクセスできる可能性があります。今回のサーバーはFAQだけを読む設計でも、将来のコード変更で範囲が広がることがあります。実行ユーザー、許可ディレクトリ、送信先、利用クライアントを台帳へ記録し、変更時に再審査できる状態にします。サンドボックスや権限分離の必要性はMCPのセキュリティ文書でも示されています[5]。
試験利用へ進める条件:読み取り専用、架空または承認済みデータ、固定版、境界値テスト合格、標準出力の純粋性、ログの機密除外、実行権限の台帳化がすべてそろっていること。
クライアント接続の設定例は製品ごとに異なります。古い画面や他製品のJSONを転用せず、利用するクライアントの現行公式文書を確認してください。接続作業の詳しい切り分けは「MCPクライアントへの接続手順」で扱います。サーバー側の契約を先に安定させておくと、接続失敗がクライアント設定なのか実装なのかを分けやすくなります。
最小MCPサーバーが向かないケースは別設計へ切り替える
この最小構成が向くのは、限定された利用者がローカルで、読み取り専用の低機密データを試す場面です。顧客レコードの更新、送金、メール送信、契約変更など、取り消しが難しい操作にはそのまま転用できません。操作ごとの認可、承認画面、冪等性キー、監査ログ、再実行方針、停止スイッチを含む設計が必要です。
Streamable HTTPで社内ネットワークや外部へ公開する場合は、stdioとは脅威が異なります。Origin検証、ホスト制限、TLS、認証・認可、レート制限、サーバー側のトークン検証、障害監視を追加します。現行のHTTPトランスポートは一つのエンドポイントへPOSTし、プロトコル版などをヘッダーで伝える構造です。古いGETストリームやセッション前提の実装を新規標準として採らないでください[6]。
- 更新操作が必要:業務責任者とセキュリティ担当を加え、確認画面と取消方法を設計する。
- 個人情報を扱う:取得目的、最小化、保存期間、ログマスキング、委託関係を確認する。
- 外部ネットワークへ公開:HTTP認可、Origin、TLS、レート制御、インシデント対応を先に決める。
- 24時間利用する:可用性目標、監視、オンコール、更新手順、ロールバックを用意する。
- SDKを更新する:ロックファイルだけ変更せず、契約試験とクライアント互換試験を再実行する。
反対に、固定された一つのアプリケーションから既存APIを決め打ちで呼ぶだけなら、MCPサーバーを新設する価値は小さい場合があります。AIクライアントによる機能発見や複数クライアントでの再利用が要件にないなら、既存APIの入力検証と監査を強化する方が運用点を増やしません。採用技術ではなく、呼び出し主体と運用責任の違いで判断します。
引き渡し時にコード以外の再現材料をそろえる
試作担当者しか起動できない状態では、業務導入へ進めません。リポジトリには固定版を含むロックファイル、架空データ、起動コマンド、契約試験、既知の制約を含めます。シークレットや本番データは含めません。運用台帳には所有部門、技術責任者、利用クライアント、データ分類、実行権限、更新期限、停止連絡先を記録します。
引き渡し時には、FAQデータの版とサーバー版を別々に識別します。コードを変更せずFAQだけを差し替えた場合でも、検索結果と業務判断は変わります。応答へ内部版を常に露出する必要はありませんが、相関IDからどのデータ版を使ったか運用者が追えるようにします。データの承認者、配布日時、旧版への戻し方も記録します。
障害手順は少なくとも三種類に分けます。サーバーが起動しない場合は版と標準エラーを確認し、検索結果が空になる場合はデータ読込と件数を確認し、誤ったFAQが返る場合はデータ版と検索条件を確認します。すべてを再起動で処理すると、壊れたデータや契約変更を一時的なプロセス障害として見逃します。
試験版を廃止するときは、クライアント設定、仮想環境、FAQ複製、ログ、資格情報を対象にします。リポジトリを削除しただけでは、利用者の設定から古いコマンドが起動され続ける可能性があります。接続不能、プロセス不在、秘密の失効、保持対象以外のデータ削除を確認し、所有者が廃止記録を承認します。
| 判定領域 | 合格条件 | 責任者 | 再確認の契機 |
|---|---|---|---|
| 再現性 | クリーン環境で版確認から起動まで再実行できる | 実装担当 | Python、SDK、依存関係の更新 |
| 契約 | 説明、入力、出力、エラーが設計票と一致する | 機能責任者 | ツール名または業務範囲の変更 |
| 安全性 | 最小権限、ログ除外、データ分類が承認済み | セキュリティ担当 | 権限、データ、通信先の追加 |
| 運用 | 監視、停止、更新、問い合わせ先が定義済み | 運用責任者 | 利用者拡大または本番移行 |
| 証跡 | Inspectorと自動試験の結果を版と紐付けて保存 | 品質担当 | コードまたは設定の変更 |
判定票の一項目でも未定なら、実装の完成と業務リリースを同一視しないことが重要です。ローカル検証版として明示し、利用者とデータを限定します。特に更新操作や外部公開を後から足す場合は、最小サーバーの延長ではなく、新しいリスク区分のシステムとして設計審査をやり直します。
次の行動:実装担当は、架空FAQの代表3件と該当なし1件を使い、固定版の環境で正常系と境界値を実行します。品質担当は同じロックファイルから再実行し、4件の応答と標準エラーログをリリース判定票へ添付します。
ここまでの手順で得るべき成果物は、動くPythonファイルだけではありません。受け入れ条件、固定版、ツール契約、検証記録、権限台帳、リリース判定が互いに参照できることが、次の担当者に渡せるMCPサーバーの完成形です。
参考文献・一次情報
- Model Context Protocol公式『Specification 2026-07-28』(2026-07-30確認)
- MCP公式Python SDKドキュメント(2026-07-30確認)
- MCP公式仕様『stdio transport』(2026-07-30確認)
- Model Context Protocol公式『MCP Inspector』(2026-07-30確認)
- Model Context Protocol公式『Security Best Practices』(2026-07-30確認)
- MCP公式仕様『Streamable HTTP』(2026-07-30確認)