私の最初のMCPサーバーには4時間かかった。40分で終わるはずだったのに。残りの時間は、存在もしない問題をデバッグするのに費やした。タイピングを始める前にMCPが実際には何なのかを理解していなかったからだ。だから、あなたのその午後を守らせてほしい。
MCP — Model Context Protocol — は、Claude Codeに新しいツールを渡す方法にすぎない。それだけだ。エディタはすでにClaudeにファイルアクセスとシェルを与えている。MCPサーバーはそれに加えて与える:クエリできるデータベース、呼び出せるAPI、あなたのLinearボード、あなたのPostgres、何でも。サーバーは独自のプロセスとして動く。Claudeはシンプルなプロトコルでそれと通信する。Claudeがあなたのツールが必要だと判断したとき、それを呼び出し、結果を受け取り、先へ進む。
それがメンタルモデルの全てだ。しっかり覚えておいてほしい。
最初のMCPサーバーを始める前に必要なもの
Node 18以降。ターミナル。Claude Codeがインストールされて動作していること。リストはこれだけだ。Dockerは不要、クラウドアカウントも不要、プロトコルのワイヤフォーマットを理解する必要もない — SDKがすべて処理してくれる。
これから作るのは、ひとつのツールを公開するシンプルなサーバーだ:都市の現在の天気を取得するツール。データは偽物でいい。重要なのは仕組みであって、天気ではないから。仕組みが動いたら、実際のAPIに置き換えるのは5分の変更で済む。
ステップ1:プロジェクトのひな型を作る
mkdir weather-mcp && cd weather-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
zodパッケージはオプションの飾りではない。MCPツールにはスキーマが必要で、Claudeはそれを見てどの引数を渡すべきかを知る。zodはそれを書く最もすっきりとした方法だ。Claudeが毎回ぴったりの形式を渡してくれるようになったとき、あなたは自分に感謝するだろう。
ステップ2:サーバーを書く
index.jsを作成する:
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", version: "1.0.0" });
server.tool(
"get_weather",
"Get the current weather for a city",
{ city: z.string().describe("City name, e.g. Lisbon") },
async ({ city }) => ({
content: [{ type: "text", text: `It's 22°C and sunny in ${city}.` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
どれだけシンプルかを見てほしい。ツールひとつ、スキーマひとつ、偽のレスポンスひとつ。最初のとき私を1時間も混乱させたのはStdioServerTransportの部分だ — これはサーバーがネットワークポートではなく標準入出力を通じてClaudeと通信することを意味する。だからサーバーにはconsole.logによるデバッグができない。stdoutに何かを出力すると、プロトコルストリームが壊れてしまうからだ。代わりにconsole.errorに出力すること。これを記憶に焼き付けておいてほしい。
ステップ3:Claude Codeに登録する
Claude Codeは設定ファイルからMCPサーバーを読み込む。一番簡単な方法はCLIを使うことだ:
claude mcp add weather -- node /full/path/to/weather-mcp/index.js
絶対パスを使うこと。相対パスは見た目は問題なさそうに見えて、Claudeが別の作業ディレクトリからサーバーを起動したときに謎めいた失敗をする。私はこれを3回も間違えた。絶対パス。毎回。
次に登録されたか確認する:
claude mcp list
出力にweatherが見えるはずだ。見えなければ、addコマンドが静かに失敗している — 再実行してエラーを読もう。
ステップ4:実際に使ってみる
任意のプロジェクトでClaude Codeセッションを開始して、こう尋ねてみよう:
Lisbon の天気はどうですか?
Claudeはget_weatherツールを持っていることを認識し、それを呼び出して、あなたの偽のレスポンスで答えるはずだ。これが初めてうまくいったとき、少し魔法のように感じる — 別のファイルに関数を書いただけなのに、AIアシスタントが自分でそれを呼び出せるようになったのだから。
Claudeがツールを呼び出さない場合、10回のうち9回は説明が曖昧すぎるからだ。「都市の現在の天気を取得する」というのは、Claudeにいつそれを使うべきかを正確に伝える。「天気ツール」では伝わらない。ツールの説明はプロンプトだ。使うタイミングを判断する新しいチームメンバーにツールを説明するつもりで書こう。
つまずきやすい3つのこと
stdoutへの出力。 もう言ったが、もう一度言う。最初のサーバーで最も多いバグだからだ。console.logは使わない。絶対に。プロトコルはstdoutで生きている。
古いプロセス。 index.jsを編集しても、Claudeはホットリロードしない。Claude Codeセッションを再起動して、サーバーを再起動させること。私はかつて20分間、コードの変更が効いていないと確信していたが、実は古いバージョンを動かしていただけだった。
スキーマの不一致。 zodスキーマがcityを必須にしていてClaudeが何も渡さないと、わかりにくいエラーが出る。省略可能なものは実際に.optional()で省略可能にして、すべてのフィールドに.describe()を付けてClaudeに何を入れるべきかを教えよう。
おもちゃから本物へ
ここからが楽しいところだ。偽の天気文字列?関数の中身を実際の天気APIへの本物のfetchに置き換えよう。スキーマも、登録の仕方も、Claudeがそれを呼び出す方法も — 何も変わらない。あなたはすでに難しい部分を作り終えている。
MCPについて誰も教えてくれないのがこれだ:最初のサーバーが学びの90%だ。その後のサーバーはどれも、中身の関数が違うだけの同じ5ステップだ。Claudeにデータベースをクエリさせたい?同じパターン。会社の内部APIを叩く?同じパターン。Notionを読む?もうわかったはずだ。
今夜、天気サーバーを作ってみよう。私が取った4時間の遠回りをスキップすれば、40分でできる。そして明日、本当に必要なものを作ろう。
