Model Context Protocol(MCP)を使うと、Claude Code が組み込みツールと同様に呼び出せるカスタムツールを追加できます。あなたのツールは Node.js プロセスができることを何でもします — データベースのクエリ・内部 API の呼び出し・独自のファイル形式の読み取り・ドメイン固有の計算 — そしてそれを Claude Code に構造化されたツール呼び出しとして公開します。
このガイドでは、ゼロから動作する MCP サーバーを構築して Claude Code に接続し、カスタムツールを有用なものにするパターンを説明します。
MCP サーバーとは何か
MCP サーバーは stdio(またはリモートサーバーの場合 HTTP/SSE)で Claude Code と通信するプロセスです。Claude Code が起動すると、設定した MCP サーバーを起動し、それぞれに tools/list リクエストを送り、利用可能なツールを学習します。それ以降、Claude Code があなたのツールを使いたいときは構造化された JSON-RPC 呼び出しを送り、サーバーがそれを実行して結果を返します。
インタラクションは以下の通りです。
Claude Code → JSON-RPC リクエスト → MCP サーバープロセス
Claude Code ← JSON-RPC レスポンス ← MCP サーバープロセス
サーバープロセスは Claude Code セッションの間ずっと動き続けます。ワンショットスクリプトではありません。
プロジェクトセットアップ
MCP TypeScript SDK をインストールします。
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node tsx
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext
tsconfig.json を更新して "outDir": "dist" を追加します。
基本的なサーバー構造
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{ name: "my-project-tools", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// 利用可能なツールを登録
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_database_schema",
description: "テーブル名・カラム・リレーションシップを含む現在のデータベーススキーマを返す。",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "query_feature_flags",
description: "すべてのフィーチャーフラグの現在の状態を返す。フラグの背後にあるかもしれない機能を実装する前に使う。",
inputSchema: {
type: "object",
properties: {
environment: {
type: "string",
enum: ["development", "staging", "production"],
description: "クエリする環境",
},
},
required: ["environment"],
},
},
],
}));
// ツール呼び出しを処理
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "get_database_schema":
return await getDatabaseSchema();
case "query_feature_flags": {
const { environment } = request.params.arguments as { environment: string };
return await queryFeatureFlags(environment);
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
async function getDatabaseSchema() {
const schema = {
tables: [
{ name: "users", columns: ["id", "email", "created_at"], relationships: ["orders(user_id)"] },
{ name: "orders", columns: ["id", "user_id", "total", "status", "created_at"], relationships: [] },
]
};
return {
content: [{ type: "text", text: JSON.stringify(schema, null, 2) }],
};
}
async function queryFeatureFlags(environment: string) {
const flags = await fetchFlags(environment); // 実装してください
return {
content: [{
type: "text",
text: `${environment} のフィーチャーフラグ:\n${flags.map((f: any) => `- ${f.name}: ${f.enabled}`).join("\n")}`,
}],
};
}
// サーバーを起動
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP サーバーが stdio で実行中"); // stderr を使う、stdout ではない
}
main().catch((error) => {
console.error("致命的なエラー:", error);
process.exit(1);
});
2つの重要な詳細があります。
stderrにログを記録し、stdoutには書かない。stdout は JSON-RPC 通信チャンネルです。stdout に書いた有効な JSON-RPC でないものはすべてプロトコルを破壊します。- エラーには適切な
ErrorCodeと共にMcpErrorを使う。汎用エラーをスローしないでください — MCP レスポンスにきれいにマッピングされません。
ツールスキーマの設計
inputSchema は JSON Schema オブジェクトで、Claude Code があなたのツールが受け付けるものを理解するために使います。慎重に書いてください — Claude Code がツールを呼び出す方法と、ユーザーへのツールの説明方法の両方に影響します。
{
name: "search_codebase",
description: "コードベースをパターンで検索する。新しいコードを書く前に、似た機能が既に存在するか確認するために使う。",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "検索パターン。ripgrep の正規表現構文をサポート。",
},
path: {
type: "string",
description: "検索するディレクトリ、プロジェクトルートからの相対パス。デフォルトはプロジェクト全体。",
},
fileTypes: {
type: "array",
items: { type: "string" },
description: "これらのファイル拡張子に検索を制限する(例: [\"ts\", \"tsx\"])。省略するとすべてのファイルを検索。",
},
},
required: ["pattern"],
},
}
良いツールスキーマのガイドライン:
- ツール自体の
description: 何をするかではなく、いつ使うかを説明する。「新しいコードを書く前に…確認するために使う」は Claude Code に適切なタイミングでツールを呼び出すコンテキストを与えます。 - 各パラメーターの
description: 名前だけでなくフォーマットと制約を説明する。 - 本当に必須のパラメーターだけを
requiredとマークする。合理的なデフォルトを持つオプションパラメーターはより自然なツール呼び出しを生み出す。 - 固定された有効な値のセットを持つパラメーターには
enumを使う。
エラーハンドリング
2つのエラーカテゴリがあります: ツールロジックからのエラーと、リクエスト自体のエラーです。
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "my_tool": {
if (!args || typeof args.requiredParam !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"requiredParam は文字列でなければなりません"
);
}
const result = await doWork(args.requiredParam);
return { content: [{ type: "text", text: result }] };
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof McpError) throw error;
throw new McpError(
ErrorCode.InternalError,
`ツール実行に失敗しました: ${error instanceof Error ? error.message : String(error)}`
);
}
});
エラーがユーザーにとって意味のある場合(バグではなく)は構造化されたコンテンツとしてツールエラーを返します。
// ツールは実行されたが "not found" の結果 — エラーではなくコンテンツとして返す
return {
content: [{
type: "text",
text: `"${query}" の結果が見つかりませんでした。より広い検索語を試すかスペルを確認してください。`,
}],
};
McpError は、ツールがまったく実行できなかった場合のために予約します。
Claude Code への接続
.claude/settings.json にサーバーを追加します。
{
"mcpServers": {
"my-project-tools": {
"command": "npx",
"args": ["tsx", "/path/to/my-mcp-server/src/index.ts"],
"env": {
"DATABASE_URL": "${DATABASE_URL}",
"FEATURE_FLAGS_API_KEY": "${FEATURE_FLAGS_API_KEY}"
}
}
}
}
コンパイル済みサーバーの場合:
{
"mcpServers": {
"my-project-tools": {
"command": "node",
"args": ["/path/to/my-mcp-server/dist/index.js"]
}
}
}
env フィールドは Claude Code プロセスからサーバーに環境変数を渡します。認証情報に使ってください — 決してハードコードしないこと。
ツールを実際に有用にする
有用な MCP ツールと無視されるツールの違い:
ツールをいつ使うかを具体的に示す。 「ユーザーデータを取得する」という説明は、Claude Code にいつ呼び出すかのシグナルを与えません。「ユーザーのサブスクリプション状態と機能の権利を取得する。サブスクリプション層によって変わる可能性のある機能を実装する前にこれを呼び出す」は、Claude Code にいつ使うべきかを正確に伝えます。
構造化された関連情報を返す。 ツールが 500 行の生の JSON を返すと、Claude Code は必要なものを抽出するためにパースしなければならない。期待される使用ケース向けに出力を事前にフォーマットする。
// 使いにくい: 生データをダンプ
return { content: [{ type: "text", text: JSON.stringify(rawApiResponse) }] };
// 使いやすい: キュレーションされたサマリー
return {
content: [{
type: "text",
text: `サブスクリプション: ${summary.tier}\n有効な機能: ${summary.features.join(", ")}\n更新日: ${summary.renewalDate}`,
}],
};
ツールを単一の責任に集中させる。 3つのことをするツールは、1つのことをする3つのツールより Claude Code にとって正しく呼び出すのが難しい。追加の MCP 呼び出しのオーバーヘッドは無視できます。明確なツールインターフェースの利点は大きい。
リソース(読み取り専用コンテキスト)
ツールに加えて、MCP サーバーはリソース — Claude Code がコンテキストとして読み取れる静的または動的コンテンツ — を公開できます。リソースはインタラクティブではありません(入力パラメーターなし)が、ドキュメント・設定・参照データに有用です。
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "docs://architecture/decisions",
name: "アーキテクチャ決定記録",
description: "このプロジェクトの現在の ADR",
mimeType: "text/markdown",
},
],
}));
リソースは CLAUDE.md に入れるには長すぎるが Claude Code が読めるべきプロジェクトドキュメントに適しています。
MCP サーバーのデバッグ
ツールが期待通りに動作しない場合:
# サーバーを直接実行してテストリクエストを送る
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | npx tsx src/index.ts
# ハンドラーに デバッグログを追加
console.error("受信したリクエスト:", JSON.stringify(request.params, null, 2));
Claude Code を --debug フラグで実行すると MCP 通信もログに記録されます。不正なリクエストや予期しないレスポンスの出力を確認してください。
まとめ
カスタム MCP サーバーを使うと、特定のコードベースを知るツールで Claude Code を拡張できます。投資はサーバーを書くための数時間の TypeScript で、見返りはデータベーススキーマをクエリしたり・フィーチャーフラグを確認したり・内部ドキュメントを検索したり・ドメイン固有の操作を実行したりできる Claude Code です — 組み込みツールと同じように。
主要な実践:
- stderr にログを記録し、stdout には絶対に書かない
- ツールをいつ使うかを含む具体的なツール説明を書く
- 生のダンプではなくキュレーションされた出力を返す
- エラーレスポンスには
McpErrorを使う - チーム全員がツールを得られるように
.claude/settings.jsonをコミットする