本文へスキップ
Claude Media
MCPサーバを自作してClaude Codeにつなぐ — TypeScript実装の完全手順

MCPサーバを自作してClaude Codeにつなぐ — TypeScript実装の完全手順

Model Context Protocol(MCP)サーバをTypeScriptで自作し、Claude Codeから呼び出せるようにする手順。stdio transportからHTTP transport、認証まで、本番運用に届く実装パターンを段階的に解説します。

読了目安 約33

はじめに

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 --init

tsconfig.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.logconsole.error に置き換える

症状4: HTTP transportで 405 Method Not Allowed

  • SSEServerTransport がSSEを返すべきなのに、別pathのハンドラが先に応答していないか
  • expressのmiddleware順序を確認

症状5: 認証ヘッダが届かない

  • Claude Code側の headers 設定が正しいか
  • リバースプロキシで Authorization ヘッダがストリップされていないか

セキュリティ考慮

MCPサーバはClaude Codeに任意ツールを生やす強力な仕組みです。次の防御を最初から組み込んでください。

  1. 入力バリデーション: zod 等でinput schemaを厳密に検査
  2. path traversal対策: ファイル系ツールでは path.resolve + 範囲チェック
  3. コマンド注入対策: Bash 系ツールを生やすなら許可リスト方式(Hooks実例カタログレシピ3と同パターン)
  4. rate limit: 1セッションあたりのツール呼び出し回数を制限
  5. 監査ログ: 何が呼ばれたか別ファイルに記録

まとめ

MCPサーバの自作は、TypeScript + SDKで30行ほどから始められるシンプルな仕組みです。stdioで動作確認 → HTTPに移行 → 認証追加、という順で段階的に拡張していくのが、複雑さを抑えながら本番運用に届く手順です。

Claude Codeの世界を自分のドメインに合わせてカスタマイズする入口として、独自MCPサーバは強力な選択肢になります。本記事のテンプレートをベースに、社内APIや独自DB、自作ツールを取り込んだClaude Codeを作ってみてください。

関連記事としてClaude CodeをDevContainerで安全に動かす完全実装では、MCPサーバをコンテナ境界の外側に置いて隔離する設計も扱っています。

この記事を共有:XLinkedIn