MCP(Model Context Protocol)実践ガイド — AIエージェントと外部ツールをつなぐ新標準の導入から自作サーバー構築まで
MCPとは何か — なぜ今、注目されているのか
MCP(Model Context Protocol)は、LLMアプリケーションと外部のデータソースやツールを標準化された方法で接続するためのオープンプロトコルだ。Anthropicが提唱し、2025年12月にはAnthropic・Block・OpenAIが共同設立したAgentic AI Foundation(AAIF、The Linux Foundation傘下のdirected fund)に寄贈され、オープンソースプロジェクトとして開発が進んでいる。
これまでAIエージェントに外部ツールを接続するには、ツールごとに独自のインテグレーションを組む必要があった。MCPはこの「N×M問題」を解消する。サーバー側がMCPに準拠していれば、どのMCPクライアント(Claude Desktop、Claude Code、Cursor、VS Codeなど)からでも同じ方法で接続できる。USBがデバイス接続を標準化したように、MCPはAIとツールの接続を標準化する。
2025年11月にリリースされた仕様バージョン 2025-11-25 が現行の安定版で、2025年6月の更新ではOAuth認証周りの仕様が大幅に強化された。
MCPの3つのコア概念
MCPサーバーが公開できる機能は、大きく3種類に分かれる。
| 概念 | 役割 | 具体例 |
|---|---|---|
| Tools | LLMがアクションを実行する(副作用あり) | DBへの書き込み、API呼び出し、ファイル操作 |
| Resources | 読み取り専用のデータを公開する | 設定ファイル、DBレコード、ログ |
| Prompts | 再利用可能なプロンプトテンプレート | コードレビュー用プロンプト、要約テンプレート |
正直、最初はResourcesとToolsの使い分けに戸惑った。判断基準はシンプルで、「副作用があるか」で分ければよい。データの取得だけならResource、何かを変更するならToolだ。
トランスポート — stdioとStreamable HTTP
MCPの通信には主に2つのトランスポート方式がある。
stdio(ローカル実行向け)
クライアントがMCPサーバーをサブプロセスとして起動し、標準入出力で通信する。ネットワークのオーバーヘッドがなく、マイクロ秒レベルのレスポンスが得られる。ローカルCLIツールとの連携に最適。
Streamable HTTP(リモート実行向け)
単一のHTTPエンドポイント(通常 /mcp)で双方向通信を行う。サーバーはSSEを使って複数メッセージをストリーミングできる。以前はSSEトランスポートが使われていたが、エンドポイントが2つ必要になる複雑さから非推奨となり、Streamable HTTPに置き換わった。
選定基準: ローカルのCLIツールならstdio、Webサービスやチーム共有ならStreamable HTTP。
Claude Desktopで既存のMCPサーバーを使う
まずは既存の公開サーバーを接続してみよう。設定ファイルは以下の場所にある。
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
ファイルシステムへのアクセスを許可する例:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/username/Projects"
]
}
}
}
GitHub連携の場合は環境変数でトークンを渡す:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxx"
}
}
}
}
設定を保存したらClaude Desktopを再起動する。チャット画面にツールアイコンが表示されれば接続成功だ。
Claude Codeでの設定
Claude Codeではコマンドラインから追加できる:
claude mcp add github -e GITHUB_PERSONAL_ACCESS_TOKEN=ghp_xxxxx -- npx -y @modelcontextprotocol/server-github
自作MCPサーバーを構築する
ここからが本題。TypeScript SDKを使って、実際に動くMCPサーバーをゼロから作る。
プロジェクトのセットアップ
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
npx tsc --init --module nodenext --moduleResolution nodenext --outDir dist
package.json に以下を追加:
{
"type": "module",
"bin": { "my-mcp-server": "dist/index.js" },
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
サーバーの実装
src/index.ts を作成する。今回は「プロジェクトのTODO管理」を行うMCPサーバーを例にする。
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// インメモリのTODOストア
interface Todo {
id: number;
title: string;
done: boolean;
}
let todos: Todo[] = [];
let nextId = 1;
const server = new McpServer({
name: "todo-manager",
version: "1.0.0",
});
// --- Tool: TODOを追加 ---
server.tool(
"add_todo",
"新しいTODOを追加する",
{ title: z.string().describe("TODOのタイトル") },
async ({ title }) => {
const todo: Todo = { id: nextId++, title, done: false };
todos.push(todo);
return {
content: [
{ type: "text", text: `TODO #${todo.id} "${todo.title}" を追加しました` },
],
};
}
);
// --- Tool: TODOを完了にする ---
server.tool(
"complete_todo",
"指定IDのTODOを完了にする",
{ id: z.number().describe("TODOのID") },
async ({ id }) => {
const todo = todos.find((t) => t.id === id);
if (!todo) {
return { content: [{ type: "text", text: `ID ${id} のTODOが見つかりません` }] };
}
todo.done = true;
return {
content: [{ type: "text", text: `TODO #${id} "${todo.title}" を完了にしました` }],
};
}
);
// --- Resource: TODO一覧 ---
server.resource(
"todo_list",
"todo://list",
{ description: "現在のTODO一覧を取得する" },
async () => ({
contents: [
{
uri: "todo://list",
mimeType: "application/json",
text: JSON.stringify(todos, null, 2),
},
],
})
);
// --- 起動 ---
const transport = new StdioServerTransport();
await server.connect(transport);
ビルドと動作確認
npm run build
Claude Desktopの設定ファイルに追加:
{
"mcpServers": {
"todo-manager": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"]
}
}
}
再起動後、Claudeに「TODOに『記事を書く』を追加して」と話しかけると、add_todo ツールが呼び出される。
よくあるハマりポイント
1. stdoutに余計な出力を書いてはいけない
stdioトランスポートでは、標準出力がそのままJSON-RPCの通信路になる。console.log でデバッグ出力すると通信が壊れる。デバッグログは必ず console.error(stderr)に出すこと。
// NG: 通信が壊れる
console.log("debug info");
// OK: stderrに出す
console.error("debug info");
2. npxキャッシュの罠
npx -y で起動するサーバーは、古いバージョンがキャッシュされていることがある。意図したバージョンが動かないときは明示的にバージョンを指定する:
npx -y @modelcontextprotocol/server-filesystem@latest
3. ESMとCommonJSの混在
@modelcontextprotocol/sdk はESMで提供されている。package.json に "type": "module" がないとimportに失敗する。TypeScriptの tsconfig.json では module を nodenext にしておくのが安全だ。
4. 環境変数が渡らない
Claude Desktopから起動されるサブプロセスには、シェルの環境変数が引き継がれないことがある。必要な環境変数は設定ファイルの env フィールドで明示的に指定する。
Streamable HTTPでリモートサーバーを公開する
チームで共有するなら、Streamable HTTPトランスポートを使ってHTTPサーバーとして公開する。以下はExpressと組み合わせる例:
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
const server = new McpServer({ name: "remote-todo", version: "1.0.0" });
// ... tool/resource登録は同じ ...
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res);
});
app.listen(8080, () => {
console.error("MCP server listening on http://localhost:8080/mcp");
});
これは地味に便利で、一度デプロイすればチーム全員が同じMCPサーバーを使えるようになる。
まとめ
MCPはAIエージェントと外部ツールの接続を標準化する実用的なプロトコルだ。要点を整理する:
- 既存サーバーの利用は設定JSONを書くだけで始められる
- 自作サーバーはTypeScript SDKで数十行から構築可能
- トランスポートはローカルならstdio、リモートならStreamable HTTP
- stdoutの汚染とESM設定が典型的なハマりポイント
公式のMCPサーバー一覧には、ファイルシステム・GitHub・Slack・PostgreSQLなど多数のリファレンス実装が公開されている。まずは既存サーバーを試し、自分のワークフローに合ったツールが見つからなければ自作する、という流れがスムーズだ。
