創刊準備号 · 2026年秋English · RSS
AgentKoji

MCPサーバー入門と設計の5原則

エージェントから呼べる最小のMCPサーバーを、実際に動くコードで作る。人間のタイポとは質の違う誤入力から、ツール側で身を守る設計まで。

MCP(Model Context Protocol)は、AI のクライアントと外部のツールやデータをつなぐための共通の規格です。1つサーバーを書けば、MCP に対応したどのクライアントからでも同じツールを呼べます。

この記事では、手元のメモを読むだけの最小サーバーを作ります。そのうえで、編集部が自前のツールを運用して分かった「エージェント向けツールの設計」を5つにまとめます。

1. 動く最小サーバー

mkdir notes-mcp && cd notes-mcp && npm init -y
npm i @modelcontextprotocol/sdk zod

server.mjs:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import path from 'node:path';
import fs from 'node:fs/promises';

const ROOT = path.resolve(process.env.NOTES_DIR ?? './notes');

async function readNoteImpl({ name }) {
  if (!/^[\w.-]{1,64}$/.test(name) || name.includes('..')) throw new Error(`invalid name: ${JSON.stringify(name)}`);
  const p = path.resolve(ROOT, name + '.md');
  if (!p.startsWith(ROOT + path.sep)) throw new Error('path escapes root');
  return fs.readFile(p, 'utf8');
}

const server = new McpServer({ name: 'notes', version: '0.1.0' });
server.registerTool('read_note', {
  description: 'Read one markdown note by name (without .md). Use when the user refers to a saved note.',
  inputSchema: { name: z.string().describe('note name, e.g. "deploy-checklist"') },
}, async (args) => ({ content: [{ type: 'text', text: await readNoteImpl(args) }] }));

await server.connect(new StdioServerTransport());

stdio で動くサーバーです。クライアントの設定に node /絶対パス/server.mjs を登録すれば呼べるようになります。編集部で確かめたところ、read_note({name:"hello"}) は本文を返し、read_note({name:"../../etc/passwd"}) は isError: true で拒否されました。

2. 設計の5原則

原則1:実装は1つにして、CLI と MCP の両方から呼ぶ

上のコードの readNoteImpl は MCP から切り離してあります。編集部の自前ツールでは、同じ _impl 関数を CLI(Click など)と MCP(FastMCP など)の両方から呼び、スキーマは型定義から自動で生成しています。テストは _impl に対して1か所に書けば済みます。順番としては、先に「エージェントがどう使うか」を設計し、人間向けの CLI はその副産物と考えています。

原則2:細かいフラグより JSON を受け取る

LLM は構造化した JSON を作るのが得意です。--from、--to、--dry-run のようなフラグに分けると、変換の途中で意味が抜け落ちやすくなります。入力は1つのオブジェクトとして受け取り、そのオブジェクトをスキーマで検証します。

原則3:入力は「人間のタイポ」ではなく「別種の誤り」として疑う

エージェントの誤入力は、人間のタイポとは質が違います。編集部で実際に来たものを挙げます。

ツールは最後の防衛線です。名前の allowlist による検証と、パスを解決したあとのルート配下チェックの両方を入れます(上のコードの2つの if)。

原則4:返す量を絞れるようにする

レスポンスは、そのままモデルのコンテキストを消費します。フィールドマスク(fields: ["id","title"])や、1行1件の NDJSON で返せるようにして、必要な分だけ渡します。

原則5:ツールの説明は「いつ使うか」を書く

description には機能の説明ではなく、使う場面を書きます(上の例の “Use when the user refers to a saved note”)。やってはいけないこと(不変条件)は、別に skill やドキュメントに書いておきます。

ツールに渡す権限は、読み取り専用のラッパーで絞れます。「AIエージェントの権限設計」に書きました。

3. 常時ロードされる MCP は「コンテキスト税」

登録した MCP サーバーのツール定義は、呼ばれなくても毎ターン送られます。

常時読まれる文章を削った実測は「CLAUDE.mdは削るほど効く」にあります。

4. 「消したのに復活する」とき

削除したはずの MCP サーバーがまた読み込まれる原因は、たいてい設定の残骸です。

全部を掃除して、ようやくゼロになりました。

もう1つの典型は、GUI から起動したクライアントがシェルの環境変数を継承しないことです。設定ファイルの ${API_KEY} が空のまま渡り、サーバーは「認証エラー」とだけ言います。サーバーを起動するときに、キーの有無(値は出さない)をログに出すようにしておくと、切り分けが早くなります。

既存の MCP サーバーを選んで入れるときの基準と、権限の絞り方は「MCPサーバーの選び方」にまとめました。

まとめ