MCPサーバーの作り方入門 — Claude Codeを自分好みに拡張しよう

公開日

  • MCP
  • MCP サーバー 作り方
  • Claude Code 拡張
  • MCP 入門
  • TypeScript

📢 PR表記・AI利用開示
現時点で本記事にアフィリエイトリンクは掲載していません。今後掲載する場合は、リンクの近くに広告である旨を明記します。 本記事はAIツールを活用して執筆・編集しています。公式情報は執筆・更新時点(2026年10月)のものであり、最新仕様は公式サイトでご確認ください。

🔎 検証状況: 記事内のサーバーコードは、筆者が検証環境で実行して確認しています(結果は「動作確認ログ」の節)。確認していない範囲もその節に明記しています。

Claude Codeを使っていて「この外部サービスと連携できたらいいのに」と思ったことはありませんか? それを実現するのがMCP(Model Context Protocol)サーバーです。

この記事では、MCPサーバーをゼロから作る方法を、コード付きで解説します。TypeScriptとNode.jsの環境があれば、短時間で最初のMCPサーバーを動かせる場合があります(所要時間は環境や経験で大きく変わります)。

関連記事: Claude Code入門ガイド、AI副業の始め方

MCPとは

MCP(Model Context Protocol)は、AIアシスタント(Claude等)と外部ツール・データソースを接続するためのプロトコルです。

Claude Code と外部サービス(DB・API・ファイル等)の間にMCPサーバーが入り、双方向にやり取りを仲介します。

MCPサーバーを作ることで、Claude Codeに新しい能力を追加できます。

⚠️ SDKのバージョンについて(先にお読みください) 公式の MCP TypeScript SDK は、執筆時点(2026年10月)でv2系(@modelcontextprotocol/server / @modelcontextprotocol/client、Node.js 20以上、zod 4以上)が現行の安定版で、v1系(@modelcontextprotocol/sdk)は保守系列として位置づけられています(リポジトリREADMEの記載)。本記事のコードはv1系(@modelcontextprotocol/sdk)の書き方です。v2ではstdioトランスポートの import 先などが変わるため、新規に作る場合は 公式のv2移行ガイド を確認してください。本記事のv2対応コードの動作は確認していません。

MCPのクライアントとサーバーの構成
MCPのクライアントとサーバーの構成

MCPサーバーでできること

セットアップ

前提条件

プロジェクトの作成

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
# 注: 本記事は v1 系(@modelcontextprotocol/sdk)の書き方(冒頭の注意書き参照)
npm install -D typescript @types/node
npx tsc --init

tsconfig.json の設定

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "declaration": true,
    "types": ["node"]
  },
  "include": ["src/**/*"]
}

"types": ["node"] は、筆者の検証環境(TypeScript 7.0.2)で、これを入れないと tsc が Cannot find name 'node:stream' / Cannot find name 'Buffer'(@types/node を入れているのに型が見つからない)というエラーで止まったため追記しています。

package.json に追加

{
  "type": "module",
  "bin": {
    "my-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Hello World: 最初のMCPサーバー

src/index.ts を作成します。

#!/usr/bin/env node
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: "hello-mcp",
  version: "1.0.0",
});

server.registerTool(
  "greet",
  {
    description: "Greet someone by name",
    inputSchema: { name: z.string().describe("Name of the person to greet") },
  },
  async ({ name }) => ({
    content: [{ type: "text", text: `Hello, ${name}! 👋` }],
  })
);

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

ビルドと登録

# ビルド
npx tsc

# Claude Code に登録
claude mcp add --transport stdio hello-mcp -- node /absolute/path/to/dist/index.js

これでClaude Codeから greet ツールが使えるようになります。claude mcp list で登録状況を確認でき、Claude Codeに「greet ツールで World に挨拶して」のように依頼すると、ツールが呼び出されます(実際の表示や承認の求められ方はClaude Codeのバージョンや権限設定で異なります)。

実践: 外部API連携

外部のWeb APIを呼ぶツールの例として、天気情報を取得するMCPサーバーを作ってみましょう。ここでは、APIキー不要で利用できる Open-Meteo の予報エンドポイント(https://api.open-meteo.com/v1/forecast)を使います。緯度・経度と、取得したい現在値の変数(current)を指定します。

注意: Open-Meteoのキー不要の利用は非商用向けで、商用利用にはAPIキーが必要と公式に記載されています。利用前に最新の利用条件を確認してください。以下のコードは公式ドキュメントのパラメータ仕様に沿って書いたもので、実行結果は後述の「動作確認ログ」に載せています(Node.js標準の fetch が環境のプロキシを自動では使わない点など、環境依存の注意も併記しています)。

#!/usr/bin/env node
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-mcp",
  version: "1.0.0",
});

server.registerTool(
  "get_weather",
  {
    description: "Get current weather for a location (latitude/longitude)",
    inputSchema: {
      latitude: z.number().min(-90).max(90).describe("Latitude (e.g., 35.68)"),
      longitude: z.number().min(-180).max(180).describe("Longitude (e.g., 139.77)"),
    },
  },
  async ({ latitude, longitude }) => {
    const url =
      "https://api.open-meteo.com/v1/forecast" +
      `?latitude=${latitude}&longitude=${longitude}` +
      "&current=temperature_2m,relative_humidity_2m,wind_speed_10m";

    const response = await fetch(url);
    if (!response.ok) {
      return {
        content: [{ type: "text", text: `Failed to fetch weather (HTTP ${response.status})` }],
        isError: true,
      };
    }

    const data = await response.json();
    return {
      content: [{ type: "text", text: JSON.stringify(data.current, null, 2) }],
    };
  }
);

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

外部APIは仕様変更や停止があり得ます。実際に使うAPIの最新ドキュメントとレート制限・利用規約を確認し、失敗時は上のように isError: true で返すようにしておくと安全です。

ツール設計のベストプラクティス

1. 引数のバリデーション

zodスキーマで引数を定義すると、Claude が正しい引数を渡してくれます。

server.registerTool(
  "search_users",
  {
    description: "Search users by name or email",
    inputSchema: {
      query: z.string().min(2).describe("Search query (min 2 characters)"),
      limit: z.number().int().min(1).max(100).default(10)
        .describe("Number of results to return"),
    },
  },
  async ({ query, limit }) => {
    // ...
  }
);

2. エラーハンドリング

server.registerTool("risky_operation", { description: "...", inputSchema: { /* ... */ } }, async (args) => {
  try {
    const result = await doSomething(args);
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
      isError: true,
    };
  }
});

3. ツールの説明を丁寧に書く

Claudeはツールの description を読んで、いつどう使うかを判断します。具体的に書きましょう。

// ❌ 悪い例
"Get data"

// ✅ 良い例
"Get the current exchange rate between two currencies. Returns the rate and last updated timestamp."

リソースの実装

ファイルやDBのデータをClaudeに公開する場合はリソースを使います。ツールと同じく register 系のAPI(registerResource)を使い、引数は「リソース名、URI、メタデータ、読み取りコールバック」の順です(v1系の公式ドキュメントの例に基づく)。

server.registerResource(
  "config",
  "config://app",
  {
    title: "Application Config",
    description: "Application configuration data",
    mimeType: "application/json",
  },
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: JSON.stringify({
        version: "1.0.0",
        environment: "development",
      }),
    }],
  })
);

動作確認ログ(筆者がこの環境で実行した結果)

本記事のコード(Hello World、天気ツール、search_users、risky_operation、リソース)を、記事のとおりにコピーして新規プロジェクトに置き、実際にビルド・実行して確認しました。

以下は出力の抜粋です(JSONは整形し、tools/list の inputSchema 内の $schema と execution フィールドは省略しています)。

Hello World(greet)

tools/list -> name: "greet", description: "Greet someone by name"
  inputSchema.properties.name = { type: "string", description: "Name of the person to greet" }, required: ["name"]

callTool greet {name: "World"} ->
{ "content": [ { "type": "text", "text": "Hello, World! 👋" } ] }

天気ツール(get_weather)

tools/list では、latitude に minimum: -90 / maximum: 90、longitude に minimum: -180 / maximum: 180 が反映されていました。

範囲外の引数(latitude: 999)はzodの検証で弾かれ、ツール本体は呼ばれませんでした。

callTool get_weather {latitude: 999, longitude: 0} ->
{ "content": [ { "type": "text", "text": "MCP error -32602: Input validation error: Invalid arguments for tool get_weather: Too big: expected number to be <=90 at latitude" } ], "isError": true }

Open-Meteoへの実リクエスト(latitude: 35.68, longitude: 139.77)は、次の2通りの結果でした。

  1. 記事のコードをそのまま実行した場合: この検証環境はHTTPSの外部通信がプロキシ経由に限られ、Node.js 22の標準 fetch はプロキシを自動では使わないため、fetch failed となり、記事のコードどおり isError: true で返りました(curl ではHTTP 200で到達できたので、API側の停止ではなく環境の制約です)。
callTool get_weather {latitude: 35.68, longitude: 139.77} ->
{ "content": [ { "type": "text", "text": "fetch failed" } ], "isError": true }
  1. サーバーのコードは変えず、起動時にプロキシ設定(undici の ProxyAgent)を node --import で差し込んだ場合: 取得できました。
callTool get_weather {latitude: 35.68, longitude: 139.77} ->
{ "content": [ { "type": "text", "text": "{\n  \"time\": \"2026-10-13T10:15\",\n  \"interval\": 900,\n  \"temperature_2m\": 21.4,\n  \"relative_humidity_2m\": 84,\n  \"wind_speed_10m\": 2\n}" } ] }

通常のインターネット環境(プロキシなし)で、同じコードがそのまま動くかどうかは、この検証では確認していません。

引数バリデーションとエラーハンドリング

callTool search_users {query: "ab"} ->
{ "content": [ { "type": "text", "text": "{\"query\":\"ab\",\"limit\":10}" } ] }     # limit の default(10) が適用された

callTool search_users {query: "a"} ->
{ "content": [ { "type": "text", "text": "MCP error -32602: Input validation error: Invalid arguments for tool search_users: Too small: expected string to have >=2 characters at query" } ], "isError": true }

callTool risky_operation {fail: true} ->
{ "content": [ { "type": "text", "text": "Error: boom" } ], "isError": true }

callTool risky_operation {} ->
{ "content": [ { "type": "text", "text": "{\"ok\":true}" } ] }

リソース(registerResource)

resources/list ->
{ "resources": [ { "name": "config", "title": "Application Config", "uri": "config://app", "description": "Application configuration data", "mimeType": "application/json" } ] }

resources/read config://app ->
{ "contents": [ { "uri": "config://app", "text": "{\"version\":\"1.0.0\",\"environment\":\"development\"}" } ] }

Claude Code への登録方法

ローカル開発時

claude mcp add --transport stdio my-server -- node /absolute/path/to/dist/index.js

-- より後ろがサーバーを起動するコマンドです。--env や --scope などのオプションは名前より前(-- の前)に書きます(公式: MCP)。

npm公開後

claude mcp add --transport stdio my-server -- npx -y my-mcp-server

設定ファイルで管理

チームで共有するなら、プロジェクト直下の .mcp.json に書く方法(--scope project)が公式に用意されています:

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

APIキーなどの秘密情報は .mcp.json に直書きせず、環境変数展開(${VAR})を使ってください。

スコープの違い

スコープフラグ保存先チーム共有
local(既定)--scope local~/.claude.json(該当プロジェクト配下)しない
project--scope projectプロジェクト直下の .mcp.jsonする(バージョン管理)
user--scope user~/.claude.jsonしない(全プロジェクトで有効)

プロジェクトスコープのサーバーは、初回利用時に承認が求められます。

npm公開の手順

# package.json のnameを公開用に
# @your-name/my-mcp-server

# ビルド
npm run build

# npm ログイン
npm login

# 公開
npm publish --access public

公開後は誰でも npx @your-name/my-mcp-server で使えます。

MCPサーバーのアイデア

まとめ

MCPサーバーの開発は、Claude Codeの可能性を大きく広げます。TypeScriptが書ければ短時間で最初のサーバーを動かせる可能性があり、npmに公開すれば他のユーザーも使えるようになります。

まずはHello Worldから始めて、自分の日常業務で「Claude Codeにやってほしいこと」をMCPサーバーとして実装してみてください。

参考情報


免責事項