07. MCP stdio server を自前で書く
カスタムツールを Claude / Claude Code に追加するには MCP (Model Context Protocol) server を立てる。 本プロジェクトでは memory-server.ts がスタックちゃんの記憶 (SQLite) を 7+ ツールとして公開している。
仕組み
Claude Agent SDK (host)
│ JSON-RPC over stdin/stdout
▼
あなたの MCP server (子プロセス)
│
▼
リソース (SQLite, ファイル, API ...)stdin/stdout で JSON-RPC を喋るだけのシンプルな仕様。 @modelcontextprotocol/sdk がプロトコルを面倒見てくれる。
最小実装テンプレ
bash
npm install --legacy-peer-deps @modelcontextprotocol/sdkts
// src/mcp/my-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
type CallToolResult,
type Tool,
} from '@modelcontextprotocol/sdk/types.js';
// 1) ツール定義
const TOOLS: Tool[] = [
{
name: 'hello',
description: '挨拶を返す',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: '誰に挨拶するか' },
},
required: ['name'],
},
},
];
// 2) サーバー作成
const server = new Server(
{ name: 'my-server', version: '0.1.0' },
{ capabilities: { tools: {} } },
);
// 3) ツール一覧ハンドラ
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
// 4) ツール呼び出しハンドラ
server.setRequestHandler(CallToolRequestSchema, async (req): Promise<CallToolResult> => {
if (req.params.name === 'hello') {
const name = String((req.params.arguments as any)?.name ?? '');
return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
}
return { isError: true, content: [{ type: 'text', text: 'unknown tool' }] };
});
// 5) stdio で接続
const transport = new StdioServerTransport();
await server.connect(transport);
process.stderr.write('[my-server] connected\n');動作確認 (Claude SDK を介さず直接)
bash
(
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}'
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello","arguments":{"name":"Donack"}}}'
) | node --import tsx src/mcp/my-server.tsJSON 3 行 (initialize / tools/list / tools/call) を流し込むと、レスポンス JSON が stdout に出る。 Hello, Donack! が返ってくれば OK。
Claude Agent SDK から呼ぶ
ts
import { query } from '@anthropic-ai/claude-agent-sdk';
query({
prompt: 'hello ツールで Donack に挨拶して',
options: {
mcpServers: {
mine: {
type: 'stdio',
command: 'node',
args: ['--import', 'tsx', '/path/to/my-server.ts'],
},
},
allowedTools: ['mcp__mine__hello'], // ← `mcp__<server-key>__<tool-name>`
},
});落とし穴
| 症状 | 原因・対処 |
|---|---|
| ツール一覧に出ない | allowedTools に mcp__<server>__<tool> 形式で追加忘れ |
| サーバー spawn 失敗 | command: 'node', args: ['--import', 'tsx', '...'] の組み合わせを確認 |
| stdout に書くとプロトコルが壊れる | ログは process.stderr.write(...) で stderr へ |
| stdio が二重接続 | 同じファイルを CLI 用にも使うなら if (import.meta.url === ...) で分岐 |
実例
mac-studio/bridge/src/mcp/memory-server.ts がフル実装サンプル:
- 7+ tools (memory_list_parts / search_activities / shopping_list_top など)
- SQLite (better-sqlite3) と接続
- 入力スキーマ + ハンドラの実体まで含めて約 250 行