Claude CodeのMCPサーバーを自作してみた

2026.04.07 09:00
2026.03.16 13:32
Claude CodeのMCPサーバーを自作してみた

Claude Codeを使っていると「こんなツールがあれば便利なのに」と思うことがありました。たとえば、外部APIからデータを取ってきたり、社内システムと連携したり。そんなときに使えるのが、MCPサーバーの自作です。

今回は、@modelcontextprotocol/sdkを使って、TypeScriptでオリジナルのMCPサーバーを作る方法をまとめてみました。実例として天気APIを取得するサーバーを作りながら、実践的なノウハウを紹介していきます。

MCPサーバーとは

MCP(Model Context Protocol)は、AIモデルと外部ツールをつなぐためのオープンソースの標準プロトコルです。MCPサーバーを作ると、Claude Codeに独自のツールを追加できます。

たとえば、こんなことができるようになります。

  • 外部APIからデータを取得する
  • データベースを検索・更新する
  • 社内システムと連携する
  • ファイル操作やデータ変換を自動化する

MCPサーバーはstdio(標準入出力)を通じてClaude Codeと通信します。JSON-RPCでメッセージをやり取りするシンプルな仕組みなので、TypeScriptが書ければ誰でも作れるんですよね。

なぜ自作するのか

すでに多くのMCPサーバーが公開されています。GitHub、Slack、データベース接続など、メジャーなサービスには既存のMCPサーバーがあります。

でも、「自分の業務で使うあのAPI」「社内の独自システム」に対応するMCPサーバーは当然ないんですよね。そういうとき、自作するしかないです。

自作のメリットはこんな感じです。

  • 既存にないツールを自由に追加できる
  • 自分の業務フローに最適化したツールが作れる
  • 複数のAPIを組み合わせた複合ツールも作れる
  • inputSchemaで引数のバリデーションも効かせられる

環境構築

まずはプロジェクトを作って、必要なパッケージをインストールします。

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

package.jsonに以下を追加します。

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  }
}

tsconfig.jsonも作成します。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

最小構成のMCPサーバー

まずは最小限のMCPサーバーを作ってみました。「Hello」と返すだけのシンプルなツールです。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// サーバーの作成
const server = new McpServer({
  name: "my-mcp-server",
  version: "1.0.0",
});

// ツールの登録
server.registerTool(
  "hello",
  {
    title: "Hello Tool",
    description: "名前を受け取って挨拶を返す",
    inputSchema: {
      name: z.string().describe("挨拶する相手の名前"),
    },
  },
  async ({ name }) => {
    return {
      content: [{ type: "text", text: `Hello, ${name}!` }],
    };
  }
);

// トランスポートの接続
const transport = new StdioServerTransport();
await server.connect(transport);

ポイントを整理するとこんな感じです。

  1. McpServer: サーバーのインスタンスを作る。nameとversionは必須
  2. registerTool: ツールを登録する。ツール名、メタ情報、ハンドラー関数の3つを渡す
  3. inputSchema: Zodスキーマで引数を定義する。バリデーションが自動で効く
  4. StdioServerTransport: 標準入出力でClaude Codeと通信する

実例: 天気APIを取得するMCPサーバー

実用的な例として、天気情報を取得するMCPサーバーを作ってみました。Open-Meteo APIを使います。このAPIは無料で、APIキーも不要です。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "weather-server",
  version: "1.0.0",
});

// 天気情報を取得するツール
server.registerTool(
  "get_weather",
  {
    title: "Get Weather",
    description: "指定した緯度・経度の現在の天気情報を取得する",
    inputSchema: {
      latitude: z.number().min(-90).max(90).describe("緯度"),
      longitude: z.number().min(-180).max(180).describe("経度"),
    },
  },
  async ({ latitude, longitude }) => {
    const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,wind_speed_10m,relative_humidity_2m&timezone=Asia/Tokyo`;

    const response = await fetch(url);

    if (!response.ok) {
      return {
        content: [
          {
            type: "text",
            text: `APIエラー: ${response.status} ${response.statusText}`,
          },
        ],
        isError: true,
      };
    }

    const data = await response.json();
    const current = data.current;

    const result = [
      `気温: ${current.temperature_2m}°C`,
      `湿度: ${current.relative_humidity_2m}%`,
      `風速: ${current.wind_speed_10m} km/h`,
    ].join("\n");

    return {
      content: [{ type: "text", text: result }],
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

inputSchemaで.min().max()を使っているのがポイントです。緯度は-90〜90、経度は-180〜180の範囲外の値が渡されると、Zodが自動でバリデーションエラーを返してくれます。

ビルドして動かすには、以下のコマンドを実行します。

# TypeScriptをコンパイル
npm run build

# 動作確認(MCP Inspectorで)
npx @modelcontextprotocol/inspector node build/index.js

Claude Codeへの登録

作ったMCPサーバーをClaude Codeに登録するには、CLIコマンドを使う方法と、設定ファイルを直接編集する方法があります。

CLIコマンドで登録する

# プロジェクト単位で登録(.mcp.json に保存される)
claude mcp add weather-server node /path/to/my-mcp-server/build/index.js

# ユーザー全体で登録(~/.claude.json に保存される)
claude mcp add --scope user weather-server node /path/to/my-mcp-server/build/index.js

設定ファイルを直接編集する

プロジェクトルートの.mcp.jsonを編集する方法もあります。

{
  "mcpServers": {
    "weather-server": {
      "type": "stdio",
      "command": "node",
      "args": ["/path/to/my-mcp-server/build/index.js"]
    }
  }
}

環境変数が必要な場合は、envフィールドで渡せます。

{
  "mcpServers": {
    "my-api-server": {
      "type": "stdio",
      "command": "node",
      "args": ["/path/to/build/index.js"],
      "env": {
        "API_KEY": "${MY_API_KEY}"
      }
    }
  }
}

${MY_API_KEY}のように書くと、シェルの環境変数が展開されます。APIキーをハードコードせずに済むので便利ですね。

inputSchemaの書き方

inputSchemaはZodスキーマで定義します。Claude Codeがツールを呼び出す際の引数バリデーションとして機能し、さらに.describe()で書いた説明はClaudeがツールの使い方を理解するのにも使われます。

よく使うパターンをまとめておきます。

import { z } from "zod";

// 基本的な型
const schema = {
  name: z.string().describe("ユーザー名"),
  age: z.number().int().positive().describe("年齢"),
  isActive: z.boolean().describe("アクティブかどうか"),
};

// オプショナルな引数
const schemaWithOptional = {
  query: z.string().describe("検索クエリ"),
  limit: z.number().int().min(1).max(100).default(10).describe("取得件数(デフォルト: 10)"),
};

// 列挙型(enum)
const schemaWithEnum = {
  format: z.enum(["json", "csv", "xml"]).describe("出力フォーマット"),
};

// 配列
const schemaWithArray = {
  tags: z.array(z.string()).min(1).max(5).describe("タグの配列(1〜5個)"),
};

.describe()は必ず付けた方がいいです。Claudeがツールをいつ・どう使うか判断する際の重要な情報になります。説明が不十分だと、意図しないタイミングでツールが呼ばれたり、逆に必要なときに呼ばれなかったりするんですよね。

デバッグ方法

MCPサーバーのデバッグには、MCP Inspectorを使うのが便利です。ブラウザ上のUIでツールの動作確認ができます。

# MCP Inspectorの起動
npx @modelcontextprotocol/inspector node build/index.js

起動するとhttp://localhost:6274でUIが開きます。登録されたツールの一覧が表示され、引数を入力してツールを実行し、レスポンスを確認できます。

ログ出力の注意点

MCPサーバーでは、stdoutにはJSON-RPCメッセージだけを流す必要があります。デバッグログをconsole.logで出すと、プロトコルが壊れてしまうんですよね。

ログ出力にはconsole.error(stderr)を使います。

// NG: stdoutに出力されてプロトコルが壊れる
console.log("デバッグ情報");

// OK: stderrに出力される
console.error("[DEBUG] リクエスト受信:", JSON.stringify(params));

これはstdioトランスポートを使う場合の鉄則です。stdoutはClaude Codeとの通信専用チャネルなので、余計な出力が混ざると正常に動作しなくなります。

実践Tips

エラーハンドリング

ツールのハンドラー内でエラーが発生した場合、isError: trueを返すことでClaude Codeにエラーを伝えられます。

server.registerTool(
  "fetch_data",
  {
    title: "Fetch Data",
    description: "外部APIからデータを取得する",
    inputSchema: {
      url: z.string().url().describe("取得先のURL"),
    },
  },
  async ({ url }) => {
    try {
      const response = await fetch(url);

      if (!response.ok) {
        return {
          content: [
            {
              type: "text",
              text: `HTTPエラー: ${response.status} ${response.statusText}`,
            },
          ],
          isError: true,
        };
      }

      const data = await response.json();
      return {
        content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
      };
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "不明なエラー";
      return {
        content: [{ type: "text", text: `エラー: ${message}` }],
        isError: true,
      };
    }
  }
);

ポイントは2つです。

  • try-catchで全体を囲む: ネットワークエラーなど予期しない例外もキャッチする
  • isError: trueを返す: Claudeがエラーを認識し、ユーザーに適切に報告できる

型安全なコードを書く

ZodスキーマとTypeScriptの型推論を活用すると、ハンドラー内の引数は自動的に型が付きます。

server.registerTool(
  "create_user",
  {
    title: "Create User",
    description: "ユーザーを作成する",
    inputSchema: {
      name: z.string().min(1).max(100),
      email: z.string().email(),
      role: z.enum(["admin", "editor", "viewer"]),
    },
  },
  async ({ name, email, role }) => {
    // name: string, email: string, role: "admin" | "editor" | "viewer"
    // TypeScriptの型推論が自動で効く
    return {
      content: [
        {
          type: "text",
          text: `ユーザー ${name} (${email}) をロール ${role} で作成しました`,
        },
      ],
    };
  }
);

Zodがランタイムのバリデーションを担い、TypeScriptがコンパイル時の型チェックを担います。この二重の安全網があるので、引数周りのバグが発生しにくいのが良いですね。

まとめ

MCPサーバーの自作は、思ったよりハードルが低かったです。@modelcontextprotocol/sdkとZodを使えば、数十行のコードでClaude Codeに独自ツールを追加できます。

振り返ると、押さえるべきポイントはこの5つでした。

  1. McpServer + StdioServerTransportでサーバーを立てる
  2. registerToolでツールを登録する
  3. inputSchemaはZodで定義し、.describe()を必ず付ける
  4. console.logは使わない(stderrのconsole.errorを使う)
  5. エラーハンドリングはtry-catchとisErrorで丁寧にやる

業務で「こんなツールがあれば」と思ったら、MCPサーバーの自作に挑戦してみると良いと思います。Claude Codeがさらに強力な相棒になる、、はず!

今回は以上です!