TL;DR
- 自社MCPサーバー設計は tool粒度・primitive選択・エラー設計・auth・transport の5軸で迷う。本記事はその判断テンプレを公式仕様と経験則に分けて整理する。
- 粒度は「副作用の大きさ × 呼出頻度」で決める。read系は粗く、write系は細かく、合計10〜30個に収めるのが運用上の落とし所(経験則)。
- transportは配布形態で決まる。デスクトップクライアント配布なら stdio、組織サーバー運用なら Streamable HTTP(公式値: 2025-06-18 spec で標準化、2025-11-25 latest でも維持)。
はじめに
MCP(Model Context Protocol)は2024年公開後、2025-06-18 の spec 改訂で OAuth 2.1 ベースの authorization が正式に組み込まれ、Streamable HTTP という新しい transport が SSE を置き換える形で標準入りした。本記事は 2025-06-18 spec を基準に整理する(公式サイト最新は 2025-11-25 latest、Authorization の Client Registration 等で差分があるため、関連箇所では注記する)。仕様は安定してきたが、「自社で MCP server を作るとき何を判断すれば良いか」 の運用ノウハウは公式ドキュメントに散在しており、まとまった設計指針がまだ少ない。
本記事は 自社 MCP サーバーを内製する Platform Team・AI 活用エンジニア 向けに、設計時に必ず迷う5軸を判断テンプレに落とす。既存記事との関係は次の通り:
- 権限統制(クライアント側の allowlist と scope 命名)→ MCP権限設計の判断軸
- 分離アーキ(運転席と作業者の境界)→ MCPで運転席と作業者を分離して事故率を下げる
- 実装の事例(GitHub MCP の secret scanning)→ GitHub MCP Server で secret scanning を組み込む
これらが「権限統制と分離」を扱うのに対し、本記事は サーバー実装側の設計パターン に絞る。
1. 3 primitives の責務分解
判断の出発点は、MCP が提供する 3 つの primitive(Tools / Resources / Prompts)の責務を理解することだ。これを混同すると tool が肥大化し、後段の権限設計まで歪む。
| primitive | 副作用 | 主導権 | 典型ユースケース |
|---|---|---|---|
| Tools | あり得る(write/calc) | model 呼出 | create_issue, run_query, send_email |
| Resources | なし(read-only) | client 取得 | file://path/to.md, db://orders/123 |
| Prompts | なし(テンプレ展開) | user 起動 | /code-review, /summarize-pr |
公式値: MCP specification で 3 primitive はそれぞれ別の RPC method として定義され、tools/list, resources/list, prompts/list で列挙される。
1.1 振り分けの判断軸
迷ったら次の順で問う:
- 副作用があるか → Yes なら必ず Tools
- read-only データの参照か → Yes なら Resources
- ユーザーが明示的に呼ぶテンプレか → Yes なら Prompts
例えば「現在の DB スキーマを取得」は Resources(db://schema)。「DB にレコードを追加」は Tools。「DB に対するレビュープロンプト」は Prompts。同じ DB 関連でも 3 つに分かれる。
1.2 Resources の URI scheme は具体的に決める
Resources は URI で一意識別するので、scheme 設計が後から効く(経験則)。社内では次のような命名で運用している:
file://repos/<repo>/<path> # ファイル
db://<dataset>/<table>/<id> # DB レコード
log://<service>/<date>/<trace_id> # ログ
config://env/<env>/<key> # 設定値
scheme を分けておくと監査ログのフィルタが書きやすく、後で resource 単位の認可ポリシーを足すときの工数が下がる。
2. tool 粒度の決め方
primitive 振り分けの次に必ず迷うのが、Tools の 粒度 だ。
2.1 粒度の3段階
| 粒度 | 例 | 強み | 弱み |
|---|---|---|---|
| 粗 | run_command(cmd: str) 1本 | 汎用、tool数最小 | 監査も認可も粗い、LLM が誤用しやすい |
| 中 | git_status, git_log, git_diff の3本 | 責務明確、scope と紐付けやすい | 似た tool が並ぶので description が重要 |
| 細 | git_log_for_branch(branch, since) のように引数固定で複数本 | LLM 選択精度◎、認可も細粒度 | tool 数が爆発 |
2.2 idempotency と副作用境界
write 系 tool は idempotency key を引数に持たせるのがベストプラクティス(経験則):
// TypeScript SDK での最小例(@modelcontextprotocol/sdk)
server.tool(
"create_issue",
{
title: z.string(),
body: z.string(),
idempotency_key: z.string().optional(), // 同一key で重複実行を防ぐ
},
async ({ title, body, idempotency_key }) => {
if (idempotency_key && await wasProcessed(idempotency_key)) {
return { content: [{ type: "text", text: "already processed" }] };
}
const issue = await github.createIssue({ title, body });
return { content: [{ type: "text", text: `created #${issue.number}` }] };
}
);
公式値: TypeScript SDK は github.com/modelcontextprotocol/typescript-sdk で公開され、server.tool(name, schema, handler) の3引数 API が標準。
2.3 落とし所(経験則)
経験則として、tool 総数は10〜30個 に収まる設計が運用しやすい。これを超えると LLM の tool 選択精度が落ち、ユーザーから「なぜこの tool を呼んだ?」の問い合わせが増える。30個を超えそうなら、別 server に分割するか、resource 化を検討する。
実例: GitHub MCP Server で secret scanning を組み込む では、scan 系 tool を3本に絞り、結果の参照は resources に逃がしている。
3. エラー設計と再試行
エラー設計はサーバー設計のうち最も「動いてから後悔」しやすい領域だ。MCP は2層に分かれている。
3.1 JSON-RPC error と isError の使い分け
公式値: MCP は JSON-RPC 2.0 を基盤とし、エラーは2層に分かれる。
| 層 | 表現 | 使うべきケース |
|---|---|---|
| プロトコル層 | JSON-RPC error フィールド(code: -32700〜-32603) | 不正リクエスト、メソッド不在、パース失敗 |
| アプリ層 | tool result の isError: true + content | バリデーション失敗、外部API失敗、権限不足 |
// アプリ層エラーは isError + content で返す(プロトコル層エラーにしない)
return {
isError: true,
content: [{
type: "text",
text: JSON.stringify({
code: "GITHUB_RATE_LIMIT",
retry_after_sec: 120,
message: "Rate limit hit. Retry in 120s."
})
}]
};
isError を使う最大の理由は、LLM 側が次のアクションを判断できる構造化情報 を返せること。code と retry_after_sec を JSON で渡せば、LLM が自律的に backoff してくれる。
3.2 retry / backoff の指針
経験則として、tool 側で recommend する retry 戦略は次の3段:
- 1 回目失敗 → 1 秒後
- 2 回目失敗 → 2 秒後
- 3 回目失敗 → 4 秒後(exponential, jitter ±20%)
- 4 回目以降 → 諦めて isError で人間にエスカレーション
これは hook 側で強制することもできる。詳細は PlanGate v8.6 Hookで承認境界を強制する の retry policy 節を参照。
4. auth と監査ログ(OAuth 2.1)
auth は2025-06-18 spec で大きく変わった領域だ。古い記事のサンプルをそのまま流用すると詰む。
4.1 OAuth 2.1 spec(2025-06-18)の要点
公式値: authorization spec 2025-06-18 で次が確定した。
- OAuth 2.1 ベース(PKCE 必須、implicit flow 廃止)
- Authorization Server と Resource Server を分離可能
- Client Registration は preregistered → Client ID Metadata Documents → Dynamic Client Registration (fallback) の優先順(公式値: 最新 2025-11-25 authorization で DCR は MAY、Client ID Metadata Documents が SHOULD に変更。2025-06-18 spec 基準で実装するなら DCR でも可だが、本番設計は最新の優先順に合わせるのが安全)
- token は Bearer で
Authorization: Bearer <token>ヘッダ
スクラッチで実装するより、Auth0 / Clerk / WorkOS など IdP を Authorization Server として置き、MCP server を Resource Server として実装する構成のほうが堅い(経験則)。
4.2 scope 命名(既存記事と接続)
scope 命名は別記事で詳しく扱った。本記事では server 側の責務として、認証層と tool 実行層で 401 / isError の役割を分離する点を強調する。
| 失敗の発生位置 | 返し方 | 仕様根拠 |
|---|---|---|
| HTTP transport / 認証層(トークン無効・期限切れ・scope 不足) | HTTP 401 + WWW-Authenticate ヘッダに RFC 9728 Resource Server Metadata URL を含める(公式値: 2025-06-18 / 2025-11-25 spec で MUST) | authorization 2025-06-18 |
| tool 実行層(権限はあるが処理続行不能、入力不足) | tool result の isError: true + content に構造化 reason | tools 2025-06-18 |
HTTP middleware 例(401 + WWW-Authenticate):
if (!verifyToken(req.headers.authorization)) {
return res
.status(401)
.setHeader(
"WWW-Authenticate",
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"'
)
.end();
}
tool result 例(runtime に「権限はあるが追加情報が必要」を返す):
if (!hasScope(token, "github.write.repo")) {
return {
isError: true,
content: [{ type: "text", text: JSON.stringify({
code: "INSUFFICIENT_SCOPE",
required_scope: "github.write.repo"
})}]
};
}
scope 設計のフルバージョンは MCP権限設計の判断軸 allowlist粒度とscope命名から組織導入まで を参照。
4.3 監査ログに残す項目
経験則として、監査ログに必ず残す項目は次の8つ:
ts, tool_name, args_hash, args_masked, result_status,
caller_user_id, token_id, request_id
args_hash は重複検出用、args_masked は PII / secret をマスクした再現用。詳細は AIコーディング導入のセキュリティ設計 の L4 監査層も参照。
5. transport の選定軸
最後に通信レイヤー。MCP の transport は3種類あるが、用途は明確に分かれる。
5.1 stdio / SSE / Streamable HTTP の比較
| transport | 用途 | 強み | 弱み | 採用判断 |
|---|---|---|---|---|
| stdio | ローカル子プロセス | 起動単純、信頼境界明確 | リモート不可 | デスクトップ配布 |
| SSE | HTTP 上の片方向ストリーム | 既存 HTTP 基盤と親和 | 双方向に弱い、deprecated 方向 | 既存資産がある場合のみ |
| Streamable HTTP | HTTP 上の双方向 | 拡張性◎、再接続容易 | 実装やや複雑 | 組織内 server / リモート |
公式値: 2025-06-18 spec で SSE は後方互換扱いとなり、新規実装では Streamable HTTP が推奨される(2025-11-25 transports でも維持。Streamable HTTP は POST/GET + optional SSE で server→client notifications を可能にする設計)。
5.2 配布形態で決める
判断は実は単純で、配布形態が transport を決める:
- ユーザーが PC で動かす → stdio(Claude Desktop / VS Code 拡張など)
- 組織サーバーで動かしリモートクライアントから接続 → Streamable HTTP
- 既に SSE で動いているサーバーがある → SSE 維持+新規は Streamable HTTP
stdio は OS のプロセス境界をそのまま信頼境界にできるので、auth が単純(OS user で完結)。Streamable HTTP は OAuth 2.1 を前提に組み込む。
まとめ:設計5軸チェックリスト
自社 MCP server を作る前に、次の5軸でレビューする:
- primitive Tools / Resources / Prompts の振り分けは副作用と主導権で説明できるか
- tool粒度 副作用大は細粒度・read系は粗、合計10〜30個に収まっているか
- エラー プロトコル層(JSON-RPC error)とアプリ層(isError)を区別できているか
- auth OAuth 2.1(2025-06-18 spec 基準、2025-11-25 latest と整合)に準拠し、認証層失敗は 401 + WWW-Authenticate(RFC 9728)、tool 層失敗は
isErrorで分離しているか - transport 配布形態(ローカル / 組織サーバー)で transport を決めているか
5つすべてが言語化できれば、設計は破綻しにくい。
関連:
FAQ
Q1. MCP server で tool は何個までが適切ですか
経験則として 10〜30個 が運用上限の目安です。30個を超えると LLM の tool 選択精度が落ち、誤呼出が増えます。超える場合は server を分割するか、read 系を resources に逃がして tool 数を減らしてください。
Q2. resources と tools の違いは何ですか
公式値として、副作用の有無と主導権 で分かれます。Tools は副作用あり(write/calc)でモデルが呼出を判断、Resources は read-only でクライアントが URI 指定で取得します。「DB から読むだけ」なら Resources、「DB に書く」なら Tools です。
Q3. transport は何を選べばよいですか
配布形態で決まります。 ユーザーが PC で動かすなら stdio、組織サーバーで動かしリモート接続させるなら Streamable HTTP です。SSE は新規採用は避け、既存資産がある場合のみ後方互換用途で使ってください(公式値: 2025-06-18 spec、最新 2025-11-25 でも維持)。
Q4. 認証は何が必須ですか
公式値として、2025-06-18 spec から OAuth 2.1(PKCE 必須) が標準になりました。Authorization Server を Auth0 / Clerk / WorkOS などの IdP に分離し、MCP server を Resource Server として実装する構成が現実解です。Client Registration は preregistered → Client ID Metadata Documents → Dynamic Client Registration (fallback) の優先順で(2025-11-25 latest で DCR は MAY、Client ID Metadata Documents が SHOULD に変更)。スクラッチ実装は推奨しません。
Q5. エラーはどう返すべきですか
プロトコル層エラーとアプリ層エラーを分離してください。 JSON-RPC error フィールドは不正リクエストやパース失敗のみ。バリデーション失敗や外部API失敗は tool result の isError: true + 構造化 content で返します。code と retry_after_sec を JSON で含めると LLM が自律的に backoff できます。
関連記事
- MCP権限設計の判断軸 allowlist粒度とscope命名から組織導入まで — 権限統制の親
- GitHub MCP Server で secret scanning を組み込む — 実装例
- MCPで運転席と作業者を分離して事故率を下げる — 分離アーキ
- Claude Skills対Subagents使い分け5軸 — レイヤーの違い
- PlanGate v8.6 Hookで承認境界を強制する — server を hook で統治
- AIコーディング導入のセキュリティ設計 4層モデルで攻撃面を整理する — セキュリティ親
- Responses API 時代のツール呼び出し設計 — クライアント側 tool use / function calling / structured output の設計判断
