MCPサーバーの作り方は、公式SDKでツールを1つ定義して、Claude Codeに登録するだけです。最小構成なら20行ほどで動きます。難しいのは実装そのものではなく、「そもそも自作すべきか」の判断と、繋がらないときの原因切り分けのほうです。
ただし、いま出回っている解説の多くはそのまま動きません。MCPは2026年7月末に仕様もSDKも世代交代しており、パッケージ名・登録API・プロトコルの前提が入れ替わったからです。「コピペしたのに動かない」の大半は、書いた人が悪いのではなく情報が古いことが原因です。
この記事では、2026年8月時点の公式仕様とSDKに沿って、自作MCPサーバーをゼロから作ってClaude Codeで動かすまでを一本道で解説します。掲載しているコードとコマンドは、すべて実際に手元で実行して動作を確認したものです。
作る前に、作らない選択肢を潰す
MCPサーバーは「Claude Codeに新しい能力を足す」ための仕組みですが、能力を足す方法はMCPだけではありません。むしろ多くのケースは、もっと軽い手段で片付きます。自作に入る前に、次の表で自分の要件がどこに当たるかを確認してください。
| やりたいこと | 適した手段 | 自作MCPは必要か |
|---|---|---|
| 決まった手順をまとめて実行させたい | Skill | 不要 |
| よく使う指示を短縮したい | スラッシュコマンド | 不要 |
| 編集後に自動でフォーマットしたい | Hooks | 不要 |
| Figma・Notion・DevToolsなど既存サービスに繋ぎたい | 既製のMCPサーバー | 不要 |
| 社内APIや独自DBをAIから叩かせたい | 自作MCPサーバー | 必要 |
| 会話のたびに同じ外部データを参照させたい | 自作MCPサーバー | 必要 |
判断の軸はシンプルで、「AIに毎回コードを書かせるか、道具として固定するか」です。手順が言葉で説明できる範囲ならSkillで足ります。外部システムと通信する必要があり、しかも引数と戻り値の形を固定したいなら、そこで初めてMCPサーバーの出番になります。SkillとMCPの使い分けはClaude Codeのスキル・ループ・ワークフローの使い分けで詳しく整理しています。
接続先が有名サービスなら、探せばたいてい公式のMCPサーバーが存在します。作り始める前にClaude CodeのMCP連携ガイド一覧で既製品を確認してください。既製で足りるなら、保守する対象を1つ増やさずに済みます。
MCPサーバーが提供できる3つの機能
実装に入る前に、何を作れるのかを押さえておきます。MCP公式仕様(2026-07-28版)によると、サーバーが提供できるプリミティブは3種類です。
| プリミティブ | 制御主体 | 中身 | 主なメソッド |
|---|---|---|---|
| Tools(ツール) | モデル制御 | モデルが呼び出せる関数。DB照会やAPI呼び出しなど | tools/list/tools/call |
| Resources(リソース) | アプリケーション主導 | モデルに文脈を与えるデータ。URIで一意に識別される | resources/list/resources/read |
| Prompts(プロンプト) | ユーザー制御 | ユーザーが明示的に選んで使うテンプレート | prompts/list/prompts/get |
「制御主体」は、その機能を誰が使うと決めるかを表します。Toolsはモデルが文脈から判断して自動的に呼びます。Resourcesはホストアプリが必要に応じて読み込みます。Promptsはユーザーが自分で選びます。自作の入口としては、まずToolsを1つ作るのが最短です。
なお公式仕様はToolsについて、信頼性と安全性のために「ツールの実行を拒否できる人間が常にループに入っているべきである」と明記しています。作る側も、勝手に実行されて困る操作をツールにしない前提で設計する必要があります。
どの方式・どの言語で作るか
実装に入る前に、2つだけ決めておくことがあります。どのトランスポート(通信方式)で繋ぐか、どの言語で書くかです。どちらも選択肢は実質2つずつしかないので、迷う時間は短くて済みます。
トランスポートは stdio か Streamable HTTP
MCP公式仕様(2026-07-28版)が標準トランスポートとして定義しているのは、stdioとStreamable HTTPの2つだけです。かつて独立した方式だったHTTP+SSEは、プロトコルバージョン2025-03-26の時点ですでに非推奨とされ、2026-07-28版で正式に「Deprecated」へ再分類されました。公式の移行先はStreamable HTTPです。
| 方式 | 動き方 | 向いている用途 |
|---|---|---|
| stdio | クライアントがサーバーをサブプロセスとして起動し、標準入出力でやり取りする | ローカルで動かす個人用・チーム用ツール。自作の入門はこちら |
| Streamable HTTP | MCPエンドポイントへのHTTP POSTで通信し、応答はJSONまたはSSEストリームで返る | 複数人・複数マシンから使う共有サーバー |
ここで誤解されやすいのが「SSEは廃止された」という言い方です。廃止されたのは独立したトランスポート方式としてのHTTP+SSEであって、SSEという技術自体は消えていません。公式仕様によると、Streamable HTTPの応答はJSONオブジェクトか、リクエストに紐づくSSEストリームのいずれかで返ります。内部では現役です。
言語はTypeScriptかPython
公式SDKはTypeScriptとPythonの両方が用意されています。どちらも2026年7月末にメジャーバージョンが上がり、パッケージ名やクラス名が変わりました。ここを間違えると最初のimportで詰まります。
| 言語 | 現行パッケージ | 旧世代(使わない) |
|---|---|---|
| TypeScript | @modelcontextprotocol/server | @modelcontextprotocol/sdk |
| Python | mcp の MCPServer | 同パッケージの FastMCP(改称前の名前) |
この記事ではTypeScriptで進めます。Node.jsが入っていればビルド環境をほぼ追加せずに始められること、Claude Code自体がNode環境で動くことが理由です。Pythonでも構造は同じで、デコレータでツールを登録する形になります。
プロジェクトを初期化する
作業用のディレクトリを作り、依存を入れます。SDK v2はNode.js 20以上とZod 4.2以上を必要とします。
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install -D typescript @types/nodenpm pkg set type=module を忘れないでください。SDK v2はESMファーストで、CommonJSのままだとimportで落ちます。次に tsconfig.json を置きます。
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "build",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}module と moduleResolution を Node16 にしておくのがポイントです。SDK v2はサブパスexports(@modelcontextprotocol/server/stdio のような書き方)を使っているため、古いモジュール解決だと型が見つからずビルドが通りません。
最小のサーバーを実装する
src/index.ts を作ります。ここではバンドルサイズを分析するツールを1つ持つサーバーを例にします。
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod";
const server = new McpServer({
name: "my-custom-server",
version: "1.0.0",
});
server.registerTool(
"analyze-bundle",
{
description: "バンドルサイズを分析して改善点を提示",
inputSchema: z.object({
entryPoint: z.string().describe("エントリーポイントのパス"),
}),
},
async ({ entryPoint }) => ({
content: [{ type: "text", text: `分析結果: ${entryPoint}` }],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);これで全部です。McpServer を作り、registerTool() でツールを登録し、stdioで繋ぐ。この3つしかありません。ビルドして動かします。
npx tscbuild/index.js ができれば成功です。Claude Codeに繋ぐ前に、ターミナルだけで動作確認できます。標準入力にJSON-RPCのリクエストを流し込む方法です。
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node build/index.js登録したツールがJSONで返ってくれば、サーバーとしては完成しています。ここで返ってこないなら、Claude Codeに繋いでも動きません。先にこの段階で切り分けておくと、あとのデバッグがとても楽になります。
inputSchemaの書き方で精度が決まる
自作MCPサーバーで一番手を抜いてはいけないのが description と inputSchema です。ここはAIが読む仕様書だからです。人間向けのドキュメントではなく、モデルがツールを呼ぶかどうかを判断し、引数に何を入れるかを決めるための唯一の手がかりになります。
inputSchema: z.object({
entryPoint: z
.string()
.describe("解析するエントリーポイントの相対パス。例: src/main.ts"),
threshold: z
.number()
.min(0)
.default(100)
.describe("警告を出すサイズのしきい値(KB)。省略時は100"),
format: z
.enum(["summary", "detailed"])
.default("summary")
.describe("出力の詳しさ。summaryは合計のみ、detailedはファイル別"),
})書き方のコツは3つです。
- 単位と例を書く — 「しきい値」ではなく「しきい値(KB)。省略時は100」。単位が抜けているとモデルはバイトかKBか判断できません
- 選択肢は
enumで閉じる — 自由入力の文字列にすると、モデルは存在しない値を渡してきます - 省略可能なものには
defaultを置く — 必須項目が多いほど、モデルは呼び出しをためらいます
返り値とエラーの返し方
ツールの戻り値は content 配列で返します。エラーを伝えたいときは例外を投げるのではなく、isError: true を付けて返すのが基本です。例外にするとプロトコルレベルの失敗になり、モデルが状況を理解して次の手を打てなくなります。
async ({ entryPoint }) => {
if (!existsSync(entryPoint)) {
return {
content: [
{ type: "text", text: `ファイルが見つかりません: ${entryPoint}` },
],
isError: true,
};
}
return {
content: [{ type: "text", text: `分析結果: ${entryPoint}` }],
};
}「ファイルが見つかりません」と文章で返しておけば、モデルはパスを直して呼び直すという判断ができます。エラーメッセージも、人間向けではなくモデルが次の行動を決められる文章で書くのがコツです。
Claude Codeに登録する
ビルドが通ったら、Claude Codeに登録します。stdioサーバーの登録コマンドはこの形です。
claude mcp add --transport stdio my-custom-server -- node /絶対パス/build/index.jsここで重要なのが -- です。公式ドキュメントによると、-- は --transport や --env といったClaude Code自身のオプションと、サーバーを起動するコマンドを区切る記号で、-- より後ろはそのままサーバーへ渡されます。これを忘れると、サーバー起動用の引数がClaude Codeのオプションとして解釈されて失敗します。
パスは絶対パスで書いてください。相対パスにすると、Claude Codeを起動したディレクトリによって解決先が変わり、動いたり動かなかったりする厄介な状態になります。
3つのスコープを使い分ける
登録先には3つのスコープがあり、既定は Local です。--scope(短縮形は -s)で切り替えます。
| スコープ | 読み込まれる範囲 | チーム共有 | 保存先 |
|---|---|---|---|
| Local(既定) | 現在のプロジェクトのみ | されない | ~/.claude.json |
| Project | 現在のプロジェクトのみ | される(バージョン管理経由) | プロジェクト直下の .mcp.json |
| User | 自分の全プロジェクト | されない | ~/.claude.json |
自分だけで試す間は既定のLocalで十分です。チームに配るなら Project を選びます。-s project を付けて登録すると、プロジェクト直下に .mcp.json が生成されます。
{
"mcpServers": {
"my-custom-server": {
"type": "stdio",
"command": "node",
"args": [
"/絶対パス/build/index.js"
],
"env": {}
}
}
}このファイルをGitに入れれば、チーム全員が同じMCPサーバーを使えます。なお同名のサーバーが複数スコープにある場合、優先順位は Local → Project → User の順で、最上位の定義だけが使われます。フィールド単位でマージされることはないので、「Userに書いた環境変数がProjectの定義にも効くはず」といった期待は外れます。
APIキーが必要なサーバーなら、-e KEY=value で環境変数を渡せます。.mcp.json の中では ${VAR} や ${VAR:-default} の形で展開できるので、キーそのものをGitに入れずに済みます。claude mcp 系のコマンド全体はClaude Codeのコマンド一覧で整理しています。
発火を確認する・繋がらないときのデバッグ
自作MCPサーバーで一番つまずくのがここです。MCPサーバーは繋がらなくてもエラーを出さず、ただ黙って無視されます。「登録したのにAIが使ってくれない」の正体は、たいてい接続失敗です。まず接続状態を確認します。
claude mcp listClaude Codeのセッション内なら /mcp でも確認できます。ここで connected になっていなければ、AIからは存在しないのと同じです。繋がらないときは、次の順に潰していくと原因にたどり着きます。
| 症状 | よくある原因 | 確認方法 |
|---|---|---|
| 登録したのに一覧に出ない | 別スコープに登録された | claude mcp get サーバー名 で定義を見る |
| connected にならない | ビルドし忘れ。build/index.js が無い | そのパスを node で直接実行してみる |
| connected にならない | 相対パスで登録している | 絶対パスで登録し直す |
| 起動直後に落ちる | type: module の指定漏れ | node build/index.js のエラーを読む |
| ツールが呼ばれない | description が曖昧でモデルが選ばない | 説明文に用途と使いどころを書き足す |
切り分けの鉄則は、Claude Codeを疑う前に、サーバー単体で動かすことです。先ほどの echo でJSON-RPCを流す方法でツール一覧が返るなら、問題はサーバーではなく登録側にあります。返らないなら、サーバー側のバグです。この一手間で原因が半分に絞れます。
なお、stdioサーバーでは標準出力に余計なものを書いてはいけません。標準出力はJSON-RPCの通信路そのものなので、console.log() でデバッグ出力を混ぜると通信が壊れます。ログを出したいときは console.error() を使って標準エラー出力へ流してください。ブラウザ操作の実測とデバッグを組み合わせる例はChrome DevTools MCPの使い方が参考になります。
ツールの粒度とセキュリティの線引き
動くようになったあと、設計で迷うのが「1つのサーバーにツールをいくつ持たせるか」です。目安は1サーバー=1つの関心事。社内APIを叩くサーバーとGitを操作するサーバーは分けます。ツールが増えるほどモデルは選択を誤りやすくなり、説明文の総量も膨らんで文脈を圧迫します。
粒度の判断は、ツール名を声に出して読んでみると分かります。「このサーバーは○○をするツールの集まりです」と一文で言えなければ、分けたほうがいい状態です。
セキュリティで押さえるべき線引きは3つです。
- 取り返しのつかない操作をツールにしない — 削除・送金・本番デプロイのような操作は、確認を挟む設計にするか、そもそもツール化しない
- 認証情報をコードに埋めない — 環境変数で渡し、
.mcp.jsonには${VAR}の形で書く - 入力を信用しない — 引数はモデルが生成した値です。パスやクエリをそのまま使わず、必ず検証してから実行する
3つ目が特に見落とされがちです。ツールの引数は人間が入力したものではなく、モデルが文脈から組み立てた値です。ファイルパスを受け取るツールなら、プロジェクト外を指していないかを確認してから読む。この一行があるかないかで、事故の可能性がまったく変わります。
チームで共有する・次の一手
1人で動かせるようになったら、次は配り方です。もっとも手軽なのは、サーバーのコードをリポジトリに入れて .mcp.json を Project スコープでコミットする方法です。メンバーはクローンして npm install と npx tsc を実行するだけで同じ環境になります。
ここで役に立つのが CLAUDE_PROJECT_DIR です。Claude Codeは起動したstdioサーバーの環境変数にプロジェクトルートを入れてくれるので、サーバー側から process.env.CLAUDE_PROJECT_DIR として読めます。パスをハードコードせずに「いま開いているプロジェクト」を基準に動くツールが書けます。
自作サーバーが手に馴染んだら、次はサブエージェントと組み合わせる段階です。特定の役割を持つサブエージェントに、自作ツールだけを使わせる構成にすると、AIの動きが安定します。設計の考え方はClaude Codeのルール・エージェント設計術にまとめています。
既製のMCPサーバーがどう作られているかを読むのも勉強になります。たとえばWordPress公式のMCP Adapterは、既存のREST APIをどうツールとして切り出すかの実例として参考になります。詳しくはWordPressとClaude Codeを連携する方法で解説しています。MCP・Hooks・Skills・サブエージェントを含む拡張機能の全体像はClaude Code上級編が地図になります。
よくある質問
Q. MCPサーバーを自作する最小構成は?
npm install @modelcontextprotocol/server zod でSDKを入れ、McpServer に registerTool() でツールを1つ登録し、stdioで接続するだけです。20行ほどのコードで動くMCPサーバーになります。
Q. ネットで見つけたサンプルコードが動かないのはなぜ?
2026年7月末にMCPの仕様とSDKが世代交代したためです。パッケージ名が @modelcontextprotocol/sdk から @modelcontextprotocol/server に変わり、ツールの登録方法も変更されました。それ以前に書かれた記事のコードは、そのままでは動きません。
Q. Claude Code以外のツールでも使えますか?
使えます。MCPはクライアントを問わない共通プロトコルなので、同じサーバーを対応する他のエディタやAIクライアントからも登録できます。サーバー側のコードを書き換える必要はなく、変わるのは登録方法だけです。
Q. 作ったサーバーを公開せず自分だけで使えますか?
使えます。ローカルのファイルパスを指定して登録するだけなので、npmへの公開もリポジトリの公開も必要ありません。個人用のツールとして手元に置いたまま運用できます。
Q. 登録したのにAIがツールを使ってくれません
まず claude mcp list か /mcp で接続状態を確認してください。connected になっていなければ接続失敗で、AIからは存在しないのと同じ状態です。接続できているのに使われない場合は、ツールの description が曖昧でモデルが選べていない可能性が高いです。
Q. Pythonでも作れますか?
作れます。公式のPython SDKがあり、パッケージ名は mcp です。ただし2.0.0で FastMCP が MCPServer に改称されているため、古い記事のコードをそのまま写すと動きません。
まとめ
MCPサーバーの自作は、実装だけ見れば拍子抜けするほど短いコードで終わります。時間を取られるのは、作るべきかの判断と、繋がらないときの切り分けです。この2つを先に押さえておけば、詰まる時間はぐっと減ります。
- Skillや既製MCPで足りるなら作らない。自作は外部システムと繋ぐときの手段
- トランスポートはstdioとStreamable HTTPの2つ。ローカル用途ならstdio
- 実装は
McpServer+registerTool()+ stdio接続の3つだけ descriptionとinputSchemaはAIが読む仕様書。単位と例を書く- 繋がらないときは、Claude Codeを疑う前にサーバー単体で動かす
MCPの周辺は動きが速い領域です。この記事の内容は2026年8月時点の公式仕様(リビジョン2026-07-28)とSDKに基づいています。バージョン番号やコマンドの細部は変わる可能性があるので、詰まったときは公式ドキュメントの現行版を確認してください。
