TL;DR
- MCP(Model Context Protocol)は2026年に AI エージェントの接続層標準として定着した
- 公開 MCP サーバが揃ったことで「組み合わせる」設計が容易になり、エージェント開発の中心が制御層に移った
- 自前 MCP サーバを書く判断軸は「社内固有システム・複数エージェント共通利用・認可境界の明確化」
- 設計で外せない3点:tool スキーマの明示・認可スコープの最小化・冪等性
- 公開しないなら認証はシンプル、外部公開なら OAuth と rate limit が前提
この記事の目的と成功基準
- 目的: MCP サーバを自前で実装する判断軸と設計パターンを、初学者でも実装着手できる粒度で整理する
- 想定読者: 社内システムをエージェントから利用させたい開発者、MCP の運用を考えるアーキテクト
- 成功基準: 「MCP 実装」「Model Context Protocol」関連クエリでの流入、AIエージェント3層成熟 への回遊
MCP が変えたもの
note の AI コーディング考察 や izanami のトレンド整理 でも繰り返し触れられている通り、MCP はエージェント設計に2つの大きな変化をもたらした。
- ツール提供と消費の分離: MCP サーバを1度書けば、Claude / Cursor / 自社エージェント / 他社エージェントから同じインターフェースで使える。OpenAI Functions / Anthropic Tools の各社実装を都度書き直す必要がなくなった
- エコシステム形成: Slack・GitHub・Linear・Postgres・S3 など主要システムは公式 / コミュニティの MCP サーバが揃った
これにより、エージェント設計の中心は「どう繋ぐか」から「どう判断させるか」(制御層)に移った。詳細は AIエージェント3層成熟 を参照。
自前 MCP サーバを書く判断軸
公開 MCP サーバで足りないケースは以下:
- 社内固有のデータベース・API への接続
- 複数のエージェントから共通機能を提供したい
- 認可境界をエージェント外で明示したい
- 監査ログを集約したい
これらに該当しないなら、公開 MCP サーバの設定で済む。書く前に「公開で足りないか」を必ず確認する。
設計の3要件
要件1: tool スキーマの明示
MCP は JSON Schema ベースで tool 定義を行う。
const searchUsers = {
name: "search_users",
description: "社員ディレクトリから氏名/メール/部署で検索",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "検索クエリ" },
department: { type: "string", description: "部署フィルタ(任意)" },
limit: { type: "integer", default: 10, maximum: 50 }
},
required: ["query"]
}
};
落とし穴:
- description が曖昧だと LLM が誤用する
- required が漏れるとプロンプト injection の余地が増える
- maximum / enum 制約を入れないとリソース消費が読めない
要件2: 認可スコープの最小化
エージェントが操作できる範囲を tool 単位で制御する。
| パターン | 適用場面 |
|---|---|
| Read-only tool | 検索・参照系。デフォルトはこちら |
| Write tool(idempotent) | 冪等な作成・更新(upsert) |
| Write tool(destructive) | 削除・取消し。人間承認 hook 必須 |
destructive な tool は MCP 経由で許可しても、必ず HITL(human-in-the-loop)を挟む設計にする。
要件3: 冪等性
エージェントの self-correction は失敗時の再試行を伴うため、tool 呼び出しは冪等にする。
# 悪い例: 同じ呼び出しで重複作成される
def create_ticket(title: str):
return db.tickets.create({"title": title})
# 良い例: client_request_id で冪等保証
def create_ticket(title: str, client_request_id: str):
existing = db.tickets.find_by_request_id(client_request_id)
if existing:
return existing
return db.tickets.create({"title": title, "request_id": client_request_id})
実装の最小骨格
Node.js(TypeScript)での例:
import { Server } from "@modelcontextprotocol/sdk/server";
const server = new Server({
name: "internal-directory",
version: "1.0.0",
});
server.setRequestHandler("tools/list", async () => ({
tools: [searchUsers, getUserDetail],
}));
server.setRequestHandler("tools/call", async (req) => {
const { name, arguments: args } = req.params;
const ctx = await authenticate(req); // 認可
switch (name) {
case "search_users":
return await handleSearchUsers(args, ctx);
case "get_user_detail":
return await handleGetUserDetail(args, ctx);
default:
throw new Error(`unknown tool: ${name}`);
}
});
await server.connect(transport);
Python なら mcp パッケージで同等の構造を組める。
認証・認可の現実解
- ローカル(stdio transport): プロセス境界が信頼境界。シンプル
- リモート(SSE transport): OAuth 2.1(Authorization Code + PKCE)が標準
- マルチテナント: tenant_id をヘッダ or token claim から取得し、すべての DB クエリで scope する
OAuth の実装は Anthropic の MCP authorization 仕様 を参照。
監査ログ
すべての tool 呼び出しを構造化ログで残す。
{
"timestamp": "2026-05-27T10:00:00Z",
"actor": "agent:claude-code",
"user_id": "user_123",
"tool": "create_ticket",
"args": { "title": "..." },
"result": "success",
"duration_ms": 145
}
これを後段の LLMオブザーバビリティ に集約し、不正アクセスや異常パターンを検知する。
アンチパターン
- 公開 MCP サーバを丸ごとラップ: 認可レイヤーを重ねるだけならゲートウェイで十分。MCP として書き直す必要はない
- destructive tool を引数 1つで実行可能にする: 「ticket_id を渡したら削除」のような設計は事故の元。確認用の dry_run option を入れる
- schema の description を空にする: LLM が誤用する
FAQ
Q. SDK は Node 版と Python 版どちらが良いですか? A. 既存スタックに合わせるのが基本です。Node 版は型サポートが厚く、Python 版はデータ系の周辺ライブラリが豊富です。
Q. MCP サーバの単一テストはどう書きますか? A. transport をモックして tool handler を直接呼ぶ単体テストと、実際の transport 越しに呼ぶ統合テストの2層構成が推奨です。
Q. 認可は MCP サーバ側と Gateway 側のどちらに置くべきですか? A. 両方です。Gateway で粗い粒度の認可(テナント・利用者)、MCP サーバで細粒度の認可(リソース単位)を担保するのが安全です。
まとめ
MCP は AI エージェントの接続層標準として2026年に定着した。自前で書くべきは「社内固有・複数エージェント共通・認可明示」のいずれかに該当する場合に限る。設計の核は tool スキーマの明示・認可スコープ最小化・冪等性の3点。監査ログと組み合わせて運用に乗せる。
