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);
ポイントを整理するとこんな感じです。
- McpServer: サーバーのインスタンスを作る。nameとversionは必須
- registerTool: ツールを登録する。ツール名、メタ情報、ハンドラー関数の3つを渡す
- inputSchema: Zodスキーマで引数を定義する。バリデーションが自動で効く
- 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}¤t=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つでした。
- McpServer + StdioServerTransportでサーバーを立てる
- registerToolでツールを登録する
- inputSchemaはZodで定義し、
.describe()を必ず付ける - console.logは使わない(stderrのconsole.errorを使う)
- エラーハンドリングはtry-catchとisErrorで丁寧にやる
業務で「こんなツールがあれば」と思ったら、MCPサーバーの自作に挑戦してみると良いと思います。Claude Codeがさらに強力な相棒になる、、はず!
今回は以上です!