Skip to content

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/sdk
ts
// 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.ts

JSON 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>`
  },
});

落とし穴

症状原因・対処
ツール一覧に出ないallowedToolsmcp__<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 行

関連

aieo-product / stack-chan project