MCPサーバを自作してClaude Codeにつなぐ — TypeScript実装の完全手順
Model Context Protocol(MCP)サーバをTypeScriptで自作し、Claude Codeから呼び出せるようにする手順。stdio transportからHTTP transport、認証まで、本番運用に届く実装パターンを段階的に解説します。
はじめに
Model Context Protocol(MCP)は、Claude(Claude Desktop / Claude Code)が外部ツール / データソース / APIと通信するための標準プロトコルです。Anthropicが主導する仕様で、すでにOpenAI / Microsoftなども実装に乗り始めています。
本チュートリアルは、自前のMCPサーバをTypeScriptで書き、Claude Codeから呼び出せるようにする手順です。完成すると、独自の社内APIやDB、自作ツールをClaude Codeの世界に組み込めるようになります。
完成品の動き:
- Claude Codeから
@my-mcp <query>のようにツール呼び出しが届く - MCPサーバが処理して結果を返す
- Claude Codeがその結果を踏まえて応答を生成
前提条件
- Node.js 20以上
- TypeScriptの基本知識
- Claude Codeがローカルで動いていること(あるいはClaude Desktop)
- MCPの概念理解(必須ではない、本記事内で都度説明)
MCPの概念をざっくり
MCPは以下の3要素を提供します。
| 要素 | 説明 |
|---|---|
| Resources | 「読める情報源」(ファイル / DB / APIレスポンス等) |
| Tools | 「呼べる関数」(検索、ファイル取得、計算等) |
| Prompts | 「テンプレートプロンプト」(再利用可能な定型) |
最も使うのはToolsです。本記事もToolsの実装を中心に進めます。
通信のtransport(伝送方式)は2つ:
- stdio: プロセス標準入出力経由。シンプル、ローカル限定
- HTTP / SSE: HTTP経由。ネットワーク越しに使える、認証可能
入門はstdioから始めて、必要に応じてHTTPに拡張するのが王道です。
手順1: プロジェクト初期化
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --inittsconfig.json を最低限編集(あればskip):
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}手順2: stdio MCPサーバの最小実装
src/server.ts:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const server = new Server(
{
name: "my-mcp-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
},
);
// ツール一覧の応答
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "echo",
description: "入力をそのまま返す動作確認用ツール",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "返却したい文字列",
},
},
required: ["message"],
},
},
],
}));
// ツール呼び出しの処理
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "echo") {
const message = request.params.arguments?.message ?? "";
return {
content: [
{
type: "text",
text: `Echo: ${message}`,
},
],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// stdio で起動
const transport = new StdioServerTransport();
await server.connect(transport);手順3: Claude Codeに登録
~/.claude/settings.json(またはプロジェクトルートの .claude/settings.json)に登録します。
{
"mcpServers": {
"my-mcp": {
"command": "npx",
"args": ["--yes", "tsx", "/absolute/path/to/my-mcp-server/src/server.ts"]
}
}
}絶対パスで指定するのがポイントです。Claude Codeを再起動すると認識されます。
動作確認:
@my-mcp echo こんにちはEcho: こんにちは が返れば疎通完了です。
手順4: 実用的なツールに拡張
echoは動作確認用。実用ツールの例として、ローカルファイルを検索する search-files を追加してみます。
import { glob } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import path from "node:path";
// ListToolsRequestSchema の handler に追加
{
name: "search-files",
description: "指定ディレクトリ内でファイルを glob 検索し、本文の先頭を返す",
inputSchema: {
type: "object",
properties: {
pattern: { type: "string", description: "glob パターン(例: '**/*.ts')" },
cwd: { type: "string", description: "検索起点ディレクトリ" },
limit: { type: "number", description: "返却ファイル数の上限", default: 10 },
},
required: ["pattern", "cwd"],
},
}
// CallToolRequestSchema の handler に追加
if (request.params.name === "search-files") {
const { pattern, cwd, limit = 10 } = request.params.arguments as {
pattern: string;
cwd: string;
limit?: number;
};
// 安全のため、cwd 外への参照を遮断
const resolved = path.resolve(cwd);
const files: string[] = [];
for await (const file of glob(pattern, { cwd: resolved })) {
files.push(file);
if (files.length >= limit) break;
}
const results = await Promise.all(
files.map(async (f) => {
const full = path.join(resolved, f);
const head = (await readFile(full, "utf-8")).slice(0, 500);
return `## ${f}\n${head}\n...`;
}),
);
return {
content: [
{
type: "text",
text: results.join("\n\n") || "(該当ファイルなし)",
},
],
};
}これでClaude Codeから @my-mcp 指定ディレクトリの TS ファイルを検索して のようにクエリすると、ファイル本文の先頭が応答に含まれます。
手順5: HTTP transportへの拡張
stdioはローカル限定。ネットワーク越しに使うにはHTTP transportへ移行します。
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
// ... server の定義は同じ
const app = express();
// SSE エンドポイント
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});
// メッセージ受信エンドポイント
app.post("/messages", express.json(), async (req, res) => {
// transport がここで POST を捌く
});
app.listen(3000, () => {
console.log("MCP server on http://localhost:3000");
});Claude Code側の登録もHTTP用に変更:
{
"mcpServers": {
"my-mcp": {
"url": "http://localhost:3000/sse"
}
}
}手順6: 認証(Bearer Token)
HTTP transportを本番運用に乗せるなら認証は必須です。最も簡単なのはBearer Token検証。
app.use((req, res, next) => {
if (req.path === "/sse" || req.path === "/messages") {
const auth = req.headers.authorization;
if (auth !== `Bearer ${process.env.MCP_TOKEN}`) {
res.status(401).send("Unauthorized");
return;
}
}
next();
});Claude Code側の登録にheaderを追加:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.example.com/sse",
"headers": {
"Authorization": "Bearer ${MCP_TOKEN}"
}
}
}
}${MCP_TOKEN} は環境変数で渡す。コミットに焼かないこと。
よくあるつまずき
症状1: Claude Code起動時にMCPサーバが表示されない
~/.claude/settings.jsonのJSON構文が正しいかcommandのパスが絶対パスになっているかnpm installが完了しているか- Claude Codeを完全再起動したか
症状2: ツール呼び出しがタイムアウト
- 重い処理を同期的に書いていないか
- 30秒程度で応答を返さないとclientがタイムアウトする
- 重い処理は別worker / 別プロセスに分離
症状3: stdioで EPIPE エラー
- サーバが標準出力にJSON以外を書き込んでいる可能性。ログは必ずstderrへ
console.logをconsole.errorに置き換える
症状4: HTTP transportで 405 Method Not Allowed
SSEServerTransportがSSEを返すべきなのに、別pathのハンドラが先に応答していないか- expressのmiddleware順序を確認
症状5: 認証ヘッダが届かない
- Claude Code側の
headers設定が正しいか - リバースプロキシで
Authorizationヘッダがストリップされていないか
セキュリティ考慮
MCPサーバはClaude Codeに任意ツールを生やす強力な仕組みです。次の防御を最初から組み込んでください。
- 入力バリデーション:
zod等でinput schemaを厳密に検査 - path traversal対策: ファイル系ツールでは
path.resolve+ 範囲チェック - コマンド注入対策:
Bash系ツールを生やすなら許可リスト方式(Hooks実例カタログレシピ3と同パターン) - rate limit: 1セッションあたりのツール呼び出し回数を制限
- 監査ログ: 何が呼ばれたか別ファイルに記録
まとめ
MCPサーバの自作は、TypeScript + SDKで30行ほどから始められるシンプルな仕組みです。stdioで動作確認 → HTTPに移行 → 認証追加、という順で段階的に拡張していくのが、複雑さを抑えながら本番運用に届く手順です。
Claude Codeの世界を自分のドメインに合わせてカスタマイズする入口として、独自MCPサーバは強力な選択肢になります。本記事のテンプレートをベースに、社内APIや独自DB、自作ツールを取り込んだClaude Codeを作ってみてください。
関連記事としてClaude CodeをDevContainerで安全に動かす完全実装では、MCPサーバをコンテナ境界の外側に置いて隔離する設計も扱っています。
関連する記事
Claude Code をもっと見る →MCP実用ガイド — Google Drive / Slack / GitHub連携と自作サーバー最小実装
MCPでコード実行する設計 — Anthropicが示す「ツール呼び出し」から「コードAPI」への移行
Claude Codeでよくあるエラー10選 — 起動失敗から認証・MCP・権限まで実機で踏みやすい順に対処
Anthropic「Trustworthy Agents in Practice」を読む — エージェント時代の5原則と現場の落とし所
Claudeを学ぶ1週間ロードマップ — 初日から実戦投入までの7日ガイド
Claude CodeのPro / Max / API従量、どれを選ぶか — コスト試算と選び方の判断ガイド
Claude CodeをGitHub Actionsに組み込む実用ガイド — claude-code-action v1とheadless実行の使い分け
Anthropic Agent SDKでSlack常駐botを作る — 実装30分のミニチュートリアル