MCPサーバー設計
このスキルは、MCPサーバーが製品をエージェントに公開する方法を決定します。ツールがコードのどこに位置するか、接続がどのように1つのテナントと1人の人物にバインドされるか、どの呼び出しにその人物の確認が必要か、そして結果とエラーがどのようにモデルを次の正しい呼び出しに導くかを定義します。プロトコル構文はMCP仕様(執筆時点ではリビジョン2026-07-28)とSDKドキュメントに従います。クエリスコーピング、権限カタログ、ID形式は tenant-isolation、authorization-rbac、dual-key-identifiers から取得します。これによって防止する障害:モデルが間違ったテナントを選択したり、顧客に2回メールを送信したり、第三者が仕込んだテキストに基づいて行動したりすることを防ぎます。なぜなら、サーバーがモデル、クライアント、またはツールの説明にゲートを残していたからです。
適用時 / 非適用時
- 適用時:製品用のMCPサーバー構築、ツールの追加またはレビュー、MCPクレデンシャルがテナントや人物にバインドされる方法の選択、顧客に通知するツール、担当者を呼び出すツール、アラートをミュートするツール、削除ツール、クライアントリトライ後の重複書き込み、エラーと結果の形状、ツール名と説明、小規模モデル用のツールセット、ツール使用の評価。
- 非適用時:他者のMCPサーバーを使用するためのクライアント設定、RESTまたはGraphQLエンドポイント設計、MCPではないチャットボットやスラッシュコマンド、製品独自のアシスタント向けプロンプト作成、非MCPルートへのレート制限。
ルール
最も重要なものから順に。例では予約システム(予約、顧客、技術者)を使用します。
- [不変] サーバーは、間違ってはいけないことを強制します。説明やアノテーションは誘導するだけです。以下のすべてのゲートは、すべての呼び出しでサーバーコード内で実行されます。なぜなら、アノテーションはヒントであり、自動承認またはプロンプトを表示しないクライアントが存在するからです。テスト:ヒントを無視する生のJSON-RPCクライアントから各ガード付きツールを呼び出します。すべてのガードが依然として作動します。ソース:MCPスキーマ 2026-07-28、
ToolAnnotations:「クライアントは信頼できないサーバーから受信したToolAnnotationsに基づいてツール使用の決定を行うべきではありません」(https://modelcontextprotocol.io/specification/2026-07-28/schema、2026-10-31確認)。 - [不変] ツールはサービス層の第2のアダプターです。UIやAPIと同じドメインサービスを呼び出し、ループバックで自身のHTTP APIを呼び出したり、ORMを直接呼び出したりしません。データを保護するチェック(制限、SSRFに対するURL許可リスト、参照レコードの所有権)は、コントローラーやフォームリクエストだけでなく、サービスに存在します。関数ごとに1つの書き込みパスを持ちます。なぜなら、薄いアダプターは、その兄弟アダプターだけが実行したすべてのチェックを継承するからです。テスト:アーキテクチャテストがツールクラス内の永続化呼び出しで失敗します。書き込みツールごとに、APIが拒否する入力はツールでも拒否されます。ソース:上記のテストで再現可能。
- [不変] 1接続、1テナント:テナントは接続作成時にクレデンシャルにバインドされ、ツールスキーマにテナントパラメータはなく、
workspace_idのような引数は不明として拒否され、すべてのハンドルはバインドされたテナント内で解決されます。複数のテナントにサービスを提供するクレデンシャルは、トランスポート(パスまたはヘッダー)にテナントを格納し、クレデンシャルに対してチェックします。ツール引数には決して含めません。なぜなら、テナントパラメータがあるとモデルがテナントを選択できてしまうからです。テスト:参照を持つツールごとに、別のテナントの有効なハンドルを渡します。not_foundと変更されていない行を期待します。ソース:MCPツール仕様、ツールセットは「リクエストに提示された認可によって異なる場合がある」(https://modelcontextprotocol.io/specification/2026-07-28/server/tools);セキュリティベストプラクティス、「クライアントから提供されるのではなく、検証されたトークンから派生したユーザーIDによるキーステート」(https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices);両方とも2026-10-31確認。 - [不変] 権限はライブでカットされます。有効な権限 = トークン権限 ∩ 接続を作成した人物の現在の権限、
tools/listとtools/callのたびに計算されます。トークン権限は作成時に上限が設定され、作成者が削除または無効化されると接続は終了し、権限の取り消しは監査するサービスを通じて実行されます。なぜなら、トークンに固定された権限はロール変更後も存続するからです。テスト:作成者のロールを削除し、書き込みツールを呼び出します。次の呼び出しで拒否されます。ソース:MCPツール仕様、サーバーは「適切なアクセス制御を実装」しなければなりません(2026-10-31確認)。 - [不変] MCPリクエストにアンビエントユーザーなし:ミドルウェアは接続コンテキスト(テナント、作成者、ライブ権限)をバインドし、フレームワークのカレントユーザースロットは空のままにします。ツールはアクターをコンテキストから読み取り、サービスはアクターをパラメータとして受け取ります。なぜなら、所有者がカレントユーザーとして設定されていると、ツール内の迷子になったポリシーチェックが、所有者はすべてを実行できるため通過してしまうからです。ユーザーがいなければ、失敗クローズドになります。テスト:アーキテクチャテストがツールコード内のゲート、ポリシー、カレントユーザーヘルパーを禁止します。意図的に追加されたポリシーチェックにより、呼び出しは失敗します。ソース:上記のテストで再現可能。
- [不変] 各ツールは正確に1つのエフェクトクラスを持ちます:読み取り、サイレント書き込み、通知(顧客に届く、または顧客の見るものを変更する)、ウェイク(担当者をページングまたは呼び出す)、アラート(アラートをミュート、一時停止、または迂回させる)、削除。クラスは名前ではなくコードパス(イベント、リスナー、オブザーバー、ジョブ、メール)から導出し、2つにまたがるツールは分割します。なぜなら、ヒント、レートバケット、プレビュー義務、接続切り替えはクラスに従うからです。テスト:キュー、メール、イベントのフェイクを使用して、サイレントツールは通知を一切ディスパッチしません。ソース:コネクタレビュー基準、catch-allの読み取り/書き込みツールは拒否され、書き込みはアクションタイプで分割されます(https://claude.com/docs/connectors/building/review-criteria、2026-10-31確認)。
- [不変] 通知、ウェイク、アラート、削除は2段階で行われます。ツールは準備のみを行います。保留中のアクション(接続、ツール、正規化された引数とダイジェスト、送信されるコンテンツのハッシュ、有効期限)を保存し、
PREVIEW、confirm_token、およびサーバーが書き込んだconfirm_summaryを返します。confirm_actionだけが実行され、接続、未使用、期限切れでない、拒否されていない、正確なサマリー、ロック、ライブ権限とスイッチ、およびコンテンツハッシュがまだ一致する新しい準備(そうでなければ新しいプレビュー、古いトークンは無効)をチェックした後に実行されます。なぜなら、モデルもクライアントも確実に人間に尋ねるとは限らず、承認されたテキストがその間に変更される可能性があるからです。テスト:プレビューだけでは何も送信されません。再利用された、期限切れの、外部の、間違ったサマリーのトークンは失敗します。ステップ間の編集はchanged_since_previewを返します。ソース:MCPツール仕様、「ツール呼び出しを拒否する機能を持つヒューマン・イン・ザ・ループが常に存在するべき(SHOULD)」;MRTRサーバー要件:クライアントによってエコーされる状態は「攻撃者制御の入力」であり、プリンシパルにバインドされ、短い有効期限とパラメータダイジェストを持ち、サーバーサイドでシングルユースが強制されます(https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr);両方とも2026-10-31確認。 - [不変] すべてのサイレント書き込みに対するリプレイ保護:フィンガープリント = 接続 + ツール + 正規化された引数の正規JSON(すべてのレベルでキーをソート)のSHA-256。ウィンドウ内でのヒットは、最初の結果を
replayed: trueとともに返し、何も実行しません。オプションのidempotency_keyは引数ダイジェストにバインドされます。キャッシュはプレゼンター出力を保持し、使い捨ての機密情報は保持しません。なぜなら、壊れたストリームは送信中のリクエストを失い、クライアントは新しいIDでそれを再発行しなければならないため、実行された書き込みが再度到着するからです。テスト:異なるJSON-RPC IDで同じ呼び出しを2回実行します。1行、2番目の結果はreplayed: true。ソース:MCPチェンジログ2026-07-28、メジャーチェンジ9(https://modelcontextprotocol.io/specification/2026-07-28/changelog);Claude Codeは、idempotentHintまたはreadOnlyHintがマークされたツールへの保留中の呼び出しを、新しい接続で一度繰り返します(https://code.claude.com/docs/en/mcp);両方とも2026-10-31確認。 - [不変] すべての作成ツールには、兄弟となる更新ツールと、ビジネスキーに対する重複ガードがあります。ヒットは、既存のハンドルと更新ツールを指定するエラーを返します。それでも作成するには
separate: trueと理由が必要です。ガードは同期カラム(created_by、created_at)を読み取り、キューに入れられた監査ログは読み取りません。編集ツールには通知パラメータはありません。なぜなら、「テキストを良くする」ように求められたモデルは、停止されなければ2つ目のオブジェクトを作成するからです。テスト:同じキーで2回作成します。1行、それを指定するエラー。ソース:上記のテストで再現可能。 - [不変] 更新は、呼び出し元が読み取ったバージョンを保持します。オブジェクトとメッセージの結果には
version(コンテンツハッシュ)があり、更新ツールと編集ツールはそれを必要とし、古いバージョンは現在の状態とともにconflictを返します。なぜなら、古い読み取りから作業しているエージェントが同僚の編集を上書きするからです。テスト:読み取り、他の場所で行を変更し、古いバージョンで更新します。conflict、行は変更されません。ソース:上記のテストで再現可能。 - [不変] バッチツールは、明示的なハンドル、またはフィルターと必須の
expected_countを受け取ります。一致数が異なる場合は、一致を返し、何も実行しません。なぜなら、モデルが期待したよりも広いフィルターは、1つの間違いを増幅するからです。テスト:expected_count: 3で12の予約に一致するフィルター。エラーは12をリストし、何も変更されません。ソース:上記のテストで再現可能。 - [不変] すべての引数をツール独自のルールでサーバー上で検証し、不明なフィールドは許可されたフィールドのリストとともに拒否します。なぜなら、SDKは常に
inputSchemaに対して引数をチェックするとは限らず、無視されたフィールドはモデルに何かを設定したと信じ込ませるからです。テスト:余分なフィールドは許可リストを返し、何も書き込みません。ソース:MCPツール仕様、サーバーは「すべてのツール入力を検証」しなければなりません(2026-10-31確認)。 - [不変] 結果内のサードパーティテキストは信頼されません。顧客、エンドユーザー、外部システム、アップロードが書き込めるテキスト(メモ、メール本文、webhookペイロード、リモートエラーメッセージ)は、切り詰められ、制御文字が除去され、
untrusted_textの下に、その中の指示に従わないようにとの注意とともに返されます。生の本文は保持されません。説明はツールを説明し、モデルにどのように振る舞うかを決して伝えません。なぜなら、外部に影響を与えるツールを操作する注入テキストは最大のリスクであり、プレビューステップが2番目の防御線だからです。テスト:すべてのサードパーティフィールドのインジェクションカナリアは、ラッパー内にのみ表示されます。ソース:MCPツール仕様、サーバーは「ツール出力をサニタイズ」しなければなりません。コネクタレビュー基準は、説明内のプロンプトインジェクションパターンを拒否します。両方とも2026-10-31確認。 - [不変] 結果は機密情報を保持せず、タスクに必要な個人データのみを保持します。明示的なフィールドリストを持つプレゼンターを使用し、レコード全体のシリアライザーは使用しません。ログはツール名、結果、公開IDを保持し、引数は保持しません。なぜなら、すべての結果はユーザーのモデルプロバイダーに送られるからです。テスト:フィクスチャのすべての機密フィールドにベイト値を入れ、すべての読み取りツールを呼び出し、結果とログをgrepします。ベイトは見つかりません。ソース:上記のテストで再現可能。レコード保持は
gdpr-engineeringに従います。 - [不変] ビジネス、バリデーション、権限の失敗はツール実行エラー(
isError: true)であり、そのテキストはError:で始まり、原因、修正方法、実際のハンドルを持つ正しいツールを指定し、最後にJSON詳細の1行で終わります。「Not found」はfind_ツールを指し示し、作成を指し示しません。権限、ツールセット、プランによって非表示にされたツールは、その理由で応答します。なぜなら、モデルは自分が見て理解できるエラーにのみ基づいて自己修正するからです。テスト:コントラクトスイート内のすべてのエラーパスはError:で始まり、パース可能なJSON行を持ちます。ソース:MCPスキーマ、CallToolResult.isError:ツールエラーは「結果オブジェクト内に報告されるべき(SHOULD)...そうでなければ、LLMはエラーが発生したことを確認できず、自己修正できません」。コネクタレビュー基準は汎用的な「Internal Server Error」応答を失敗とします。両方とも2026-10-31確認。 - [不変] テキストブロックと
structuredContentは同じJSONを保持し、宣言されたoutputSchemaはエラーを含むすべての構造化結果をカバーします。なぜなら、クライアントによってモデルに届くチャネルが異なり、宣言されたスキーマに対して検証するべき(SHOULD)だからです。テスト:スイート内のすべての結果について、パースされたテキストはstructuredContentと等しいです。ソース:MCPツール仕様、構造化コンテンツを持つツールは「シリアル化されたJSONをTextContentブロックでも返すべき(SHOULD)」(2026-10-31確認)。 - [不変] 時間と金額はサーバーの仕事です。時間は、ローカル時間とIANAゾーン、またはオフセット付きのISO 8601として到着し、決して単独では到着しません。存在しない、または曖昧なローカル時間はオプションとともに拒否されます。結果内のすべての時間は、曜日、ローカル時間とゾーン、UTC、現在からの経過時間を示します。合計、税、丸めは、10進文字列または整数の最小単位からサーバー上で計算されます。なぜなら、1日ずれやDSTエラーは曜日なしでは見えないからです。テスト:
2026-03-29 02:30をEurope/Berlinで指定すると拒否されます。2026-10-25 02:30は両方のオフセットを返します。ソース:上記のテストで再現可能。 - [不変] キルスイッチは1か所に:グローバルフラグ(両方を監査するCLIコマンドと管理者トグルで設定)により、すべてのMCP呼び出しは接続を維持したまま503と
Retry-Afterを返します。デフォルトが一度定義されたテナントごとのスイッチにより、テナント管理者はエージェントアクセスをオフにできます。なぜなら、インシデントにはすべてのエージェント書き込みを停止する1つの操作が必要だからです。テスト:フラグオン、書き込みツールを呼び出します。503、行なし、ジョブなし。ソース:上記のテストで再現可能。 - [不変] すべてのMCP書き込みは、チャネル
mcp、接続名、クライアント名で監査され、エントリポイントでカラムとして設定され、list_recent_changesはこの接続が何を変更したかを示します。なぜなら、テナントはエージェントが何をしたかを確認でき、コンテキストを失ったエージェントは別のドラフトを作成する代わりに自分のドラフトを見つけられるからです。テスト:各書き込みツールは、チャネルmcpを持つ正確に1つの監査行を生成します。ソース:上記のテストで再現可能。 - [不変] MCPとAPIのクレデンシャルは交差しません。MCPトークンは独自のタイプとプレフィックスを持ち、ハッシュ化されて保存され、一度だけ表示され、すべてのAPIルートで拒否されます。APIトークンとセッションCookieはMCPエンドポイントで拒否されます。OAuthの場合、このサーバーに対して発行されたトークンのみを受け入れ、決して通過させません。
Originを検証します。なぜなら、MCPトークンはクライアント設定ファイルに平文で保存されるからです。テスト:各ペアリングは401を返します。不正なOriginは403を返します。ソース:MCP認可仕様、サーバーは「他のトークンを受け入れたり、中継したりしてはならない(MUST NOT)」(https://modelcontextprotocol.
…
[翻訳のため省略]