MCPサーバー自作ガイド — SDKでのツール定義からClaude Code接続・配布まで
MCPサーバーをTypeScriptとPythonのSDKで自作する手順。ツール定義からMCP Inspectorでのテスト、Claude Codeへの接続、レジストリ公開まで、現行SDKのAPIで動くコードつきで解説します。
MCPサーバーの自作で何ができるか
MCP(Model Context Protocol)とは、ClaudeなどのLLMアプリケーションに外部ツール・データソース・定型プロンプトを標準形式で接続するためのオープンプロトコルです。MCPサーバーを自作すると、社内APIやデータベース、自作スクリプトをClaude CodeやClaude Desktopから直接呼び出せるようになり、公式SDKを使えば最小構成はTypeScript・Pythonとも30行前後で動きます。
MCPは2024年11月にAnthropicが公開し、その後OpenAIやMicrosoftなど他社製品にも採用が広がった、AIとツール連携の事実上の標準です。プロトコル仕様は日付形式のバージョンで管理されており、現行版は2025-11-25です。仕様の改訂はSDKが吸収するため、自作する側はSDKの安定版に乗っていれば追従できます。
本記事のゴールは次の5段階です。①TypeScriptでstdio(標準入出力)方式のサーバーを実装する、②MCP Inspectorで動作確認する、③Claude Codeに登録して使う、④HTTP対応してリモートから使えるようにする、⑤npmとMCPレジストリで配布する。Pythonでの最短実装も途中で併記します。
なお、GitHubやSentryのような既製のMCPサーバーをつなぐだけなら自作は不要です。既製サーバーの導入はMCP実用ガイド、Claude Code側の設定や運用の全体像はClaude Code MCPサーバー完全ガイドが扱っています。本記事は「サーバーを自分で書く」工程に絞ります。
前提条件とSDKの選び方
自作に必要な環境は、TypeScriptならNode.js 18以上、PythonならPython 3.10以上です。これは各SDKの動作要件で、加えてClaude Code(またはClaude Desktop)がローカルで動いていれば準備は揃います。
2つの公式SDKは設計思想が少し違います。2026年6月時点の比較は次のとおりです。
| 項目 | TypeScript SDK | Python SDK |
|---|---|---|
| パッケージ | TypeScript SDK@modelcontextprotocol/sdk | Python SDKmcp |
| 最新安定版 | TypeScript SDK1.29.0(2026年3月30日公開) | Python SDK1.27.2 |
| 動作要件 | TypeScript SDKNode.js 18以上 | Python SDKPython 3.10以上 |
| スキーマ定義 | TypeScript SDKzodで明示的に書く | Python SDK型ヒントとdocstringから自動生成 |
| 向く場面 | TypeScript SDKnpm配布、Web系チーム | Python SDKデータ処理資産の転用、記述量最小 |
TypeScript SDKは zod がpeer dependency(zod 3.25以降と4系の両対応)で、ツールの入力スキーマをコードとして厳密に書けるのが強みです。Python SDKのFastMCPは関数の型ヒントをそのままスキーマに変換するため、既存のPython関数を最小の手数でツール化できます。迷ったら、配布エコシステムが厚いTypeScriptから始める選択が無難です。
ツール・リソース・プロンプトの3要素
MCPサーバーがクライアントに提供できる機能は3種類あり、それぞれ呼び出しの主体が違います。自作の設計は「どの要素として公開するか」を決めるところから始まります。
| 要素 | 役割 | 呼び出しの主体 |
|---|---|---|
| tool | 役割検索・計算・副作用を伴う処理を実行する関数 | 呼び出しの主体モデル(Claudeが必要と判断して呼ぶ) |
| resource | 役割読み取り専用データの公開(ファイル・DBレコード等) | 呼び出しの主体クライアント(@メンションで参照) |
| prompt | 役割再利用できる定型プロンプトのテンプレート | 呼び出しの主体ユーザー(コマンドとして明示実行) |
Claude Codeでの見え方も3者で異なります。toolは通常のツール呼び出しとして自動で使われ、resourceは @サーバー名:docs://... の形でプロンプトに添付でき、promptは /mcp__サーバー名__プロンプト名 というスラッシュコマンドになります。最初の1本はtoolだけで成立するので、本記事もtool実装を軸に進め、resourceとpromptは手順4で追加します。
TypeScriptでstdioサーバーを作る手順
ローカル利用ならstdioトランスポートが第一候補です。クライアント(Claude Code)がサーバーをサブプロセスとして起動し、標準入出力でJSON-RPCメッセージを交換する方式で、ネットワーク設定も認証も不要なため、開発の起点に向いています。
手順1: プロジェクトを初期化する
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --inittsconfig.json は target: "ES2022"、module: "Node16"、moduleResolution: "Node16" あたりを指定し、package.json に "type": "module" を追加しておくとSDKのESM形式とそのまま噛み合います。
手順2: ツールを定義する
現行SDKの中心は McpServer クラスと registerTool() メソッドです。入力スキーマはzodのフィールド定義をオブジェクトで渡し、ハンドラーは型付きの引数を受け取ります。src/server.ts を次の内容で作成します。
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: "docs-server",
version: "0.1.0",
});
server.registerTool(
"search-docs",
{
title: "社内ドキュメント検索",
description: "キーワードで社内ドキュメントを検索し、該当箇所を返す",
inputSchema: {
query: z.string().describe("検索キーワード"),
limit: z.number().default(5).describe("返す件数の上限"),
},
},
async ({ query, limit }) => {
const results = await searchInternalDocs(query, limit); // 自前の検索処理
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("docs-server started"); // ログはstderrへdescription と各フィールドの .describe() は、Claudeが「いつこのツールを呼ぶか」を判断する材料になります。引数名だけで意味が伝わると思わず、用途と制約を1文で書いておくと呼び出し精度が安定します。
手順3: 起動を確認する
npx tsx src/server.tsstdioサーバーは起動しても画面には何も出ず、標準入力からのメッセージを待ち続けます。stderrに書いた起動ログが出れば正常です。Ctrl+C で止め、テストは次節のInspectorに任せます。
手順4: リソースとプロンプトを追加する
読み取り専用データは registerResource() で公開します。URIテンプレートを使うと、パラメーター付きの動的リソースを定義できます。
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
server.registerResource(
"doc",
new ResourceTemplate("docs://{docId}", { list: undefined }),
{ title: "社内ドキュメント", mimeType: "text/plain" },
async (uri, { docId }) => ({
contents: [{ uri: uri.href, text: await loadDoc(String(docId)) }],
}),
);定型プロンプトは registerPrompt() です。引数スキーマ(argsSchema)もzodで書きます。
server.registerPrompt(
"summarize-doc",
{
title: "ドキュメント要約",
description: "指定IDのドキュメントを5行で要約する",
argsSchema: { docId: z.string() },
},
({ docId }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `docs://${docId} の内容を読み、要点を5行で要約してください。`,
},
},
],
}),
);これでtool・resource・promptの3要素が揃いました。Claude Codeに登録すると、promptは /mcp__docs-server__summarize-doc として呼び出せるようになります。
MCP Inspectorでテストする
実装したサーバーは、Claude Codeにつなぐ前にMCP Inspectorで検証するのが効率的です。InspectorはMCPプロジェクト純正のGUIデバッグツールで、インストール不要でnpx経由で起動でき、サーバーをサブプロセスとして立ち上げてブラウザー上から操作できます。
npx @modelcontextprotocol/inspector npx tsx src/server.tsビルド済みのJavaScriptを検証するなら npx @modelcontextprotocol/inspector node dist/server.js の形です。起動するとブラウザーにUIが開き、次の操作ができます。
- Toolsタブ: ツール一覧とスキーマの確認、任意の入力値での実行テスト
- Resourcesタブ: リソース一覧、メタデータ、内容の確認
- Promptsタブ: 引数を与えたプロンプト生成のプレビュー
- Notificationsペイン: サーバーからのログと通知の監視
「コードを直す → 再ビルド → Inspectorで再接続 → 該当機能だけ叩く」のループは、Claude Code本体を再起動して試すより速く回ります。不正な入力値やエラーレスポンスの確認もこの段階で済ませておくと、後段のつまずきが減ります。起動オプションやWeb・CLI・TUIの使い分け、OAuth認証の検証手順はMCP Inspectorの使い方で扱っています。
Claude Codeに接続する
自作サーバーの登録は claude mcp add コマンドで行います。設定ファイルを手書きするより確実で、stdioサーバーなら次の形です。
claude mcp add --transport stdio docs-server \
-- npx tsx /absolute/path/to/my-mcp-server/src/server.ts構文には2つの注意点があります。第一に、--transport や --env などのオプションはサーバー名より前に置きます。第二に、--(ダブルダッシュ)より後ろがサーバーの起動コマンドとして解釈されます。パスは作業ディレクトリに依存しないよう絶対パスで書くのが安全です。
登録の保存先はスコープで変わります。チームで共有するかどうかで選んでください。
| スコープ | 読み込まれる範囲 | 保存場所 |
|---|---|---|
| local(既定) | 読み込まれる範囲現在のプロジェクトのみ・自分専用 | 保存場所~/.claude.json |
| project | 読み込まれる範囲現在のプロジェクト・チーム共有 | 保存場所プロジェクト直下の.mcp.json |
| user | 読み込まれる範囲全プロジェクト・自分専用 | 保存場所~/.claude.json |
チーム共有なら --scope project を付けて .mcp.json をリポジトリにコミットします。.mcp.json ではセキュリティのため初回に承認確認が入り、${VAR} や ${VAR:-デフォルト値} 形式の環境変数展開が command / args / env / url / headers で使えるため、APIキーをファイルに直書きせずに済みます。
登録後の確認と管理は次のコマンドです。
claude mcp list
claude mcp get docs-server
claude mcp remove docs-serverClaude Codeのセッション内では /mcp で接続状態とツール数を確認できます。あとは「社内ドキュメントからデプロイ手順を検索して」のように依頼すれば、Claudeが search-docs ツールを自動で選んで呼び出します。なお、Claude Codeはstdioサーバーの環境変数に CLAUDE_PROJECT_DIR(プロジェクトルートの絶対パス)を設定するため、サーバー側でプロジェクト相対のパス解決に使えます。
PythonのFastMCPで実装する場合
Python SDKのFastMCPは、デコレーターを付けた関数がそのままツールになる高水準APIです。型ヒントが入力スキーマに、docstringがツール説明に変換されるため、TypeScript版よりさらに記述量が少なく済みます。
uv init my-mcp-server && cd my-mcp-server
uv add "mcp[cli]"server.py を次の内容で作成します。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("docs-server")
@mcp.tool()
def search_docs(query: str, limit: int = 5) -> list[str]:
"""キーワードで社内ドキュメントを検索し、該当箇所を返す"""
return search_internal_docs(query, limit) # 自前の検索処理
@mcp.resource("docs://{doc_id}")
def read_doc(doc_id: str) -> str:
"""指定IDのドキュメント本文を返す"""
return load_doc(doc_id)
if __name__ == "__main__":
mcp.run() # 既定はstdiomcp.run(transport="streamable-http") とすればHTTP方式でも起動できます。Claude Codeへの登録はTypeScript版と同じ構文で、起動コマンドだけ差し替えます。
claude mcp add --transport stdio docs-server \
-- uv run --directory /absolute/path/to/my-mcp-server server.pyPythonでもstdoutの扱いは同じ落とし穴です。print() は既定でstdoutに書くため通信を壊します。デバッグ出力は print("...", file=sys.stderr) か、stderrに書くloggingの設定を使ってください。
HTTPトランスポートでリモート公開する
stdioはローカル専用なので、複数人やネットワーク越しに使うサーバーはStreamable HTTPトランスポートに切り替えます。Streamable HTTPは現行仕様の標準リモート方式で、単一のエンドポイント(例: https://example.com/mcp)がPOSTとGETを受け、必要に応じてSSE(Server-Sent Events)でストリーミング応答を返す設計です。旧来のHTTP+SSE方式(2つのエンドポイントを使う方式)はプロトコル2025-03-26版でStreamable HTTPに置き換えられた非推奨の互換用で、新規実装で選ぶ理由はありません。
TypeScriptでは StreamableHTTPServerTransport を使います。リクエストごとにサーバーとトランスポートを生成するステートレス構成が最も単純です。
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const server = buildServer(); // McpServerを組み立てる自前関数
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // セッション管理なしのステートレス運用
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on("close", () => {
transport.close();
server.close();
});
});
app.listen(3000, "127.0.0.1");会話状態をサーバー側に持ちたい場合は sessionIdGenerator にUUID生成関数を渡すと、仕様が定める Mcp-Session-Id ヘッダーによるセッション管理が有効になります。
HTTP化で増えるのがセキュリティ責務です。仕様は次の3点を要求しており、特に1点目は必須(MUST)です。
- Originヘッダーの検証: DNSリバインディング攻撃(悪意あるWebページがlocalhostのサーバーを操作する攻撃)を防ぐため、すべての接続でOriginを検証する
- バインド先の限定: ローカル実行時は0.0.0.0ではなく127.0.0.1にバインドする
- 認証の実装: すべての接続に適切な認証を入れる
Claude Code側の登録は --transport http に切り替え、認証トークンは --header で渡します。
claude mcp add --transport http docs-server https://mcp.example.com/mcp \
--header "Authorization: Bearer YOUR_TOKEN"OAuth 2.0に対応したサーバーであれば、ヘッダー指定なしで登録してセッション内の /mcp から認証フローを完了する形も取れます。ツールに任意コマンド実行のような強い能力を持たせる場合は、許可リスト方式で入力を絞る設計(Hooks実例カタログのコマンド検証レシピと同じ発想)を最初から組み込んでおくと安全です。
自作サーバーを配布する(npmとMCPレジストリ)
配布の最短ルートは利用範囲で変わります。チーム内限定なら、--scope project で生成した .mcp.json をリポジトリにコミットするだけで、クローンした全員が同じサーバーを使えます。一般公開するなら「パッケージレジストリへの公開 + MCPレジストリへのメタデータ登録」の2段構えです。
公式MCPレジストリ(registry.modelcontextprotocol.io)はサーバーのメタデータだけを管理し、コード本体は預かりません。そのため先にnpm(PythonならPyPI)へパッケージを公開し、その後 mcp-publisher CLIで登録します。
npm publish
mcp-publisher init
mcp-publisher login github
mcp-publisher publishmcp-publisher init が生成する server.json に、逆DNS形式の名前(例: io.github.あなたのユーザー名/docs-server)・説明・パッケージ情報・トランスポート種別を記述します。GitHub認証で名前空間の所有が検証されるため、自分のユーザー名配下しか登録できない仕組みです。レジストリに載ると、各種MCPクライアントやカタログサイトからの発見性が上がります。
Claude Codeのプラグインにサーバーを同梱する配布形態もあります。プラグインの .mcp.json で定義したサーバーはプラグイン有効化と同時に起動するため、スラッシュコマンドやスキルとセットで配る場合に向いています。
よくあるつまずき
サーバーが一覧に出ない
まず claude mcp list で登録自体を確認します。出てこない場合の典型は、登録時のスコープ違い(別プロジェクトでlocalスコープに入れた)、起動コマンドの相対パス、.mcp.json のJSON構文エラーの3つです。listに ⏸ Pending approval と出るなら、プロジェクトスコープのサーバーが未承認の状態なので、claude を対話モードで起動して承認します。承認の選択をやり直したいときは claude mcp reset-project-choices でリセットできます。
接続直後に切れる・パースエラーが出る
stdioサーバーがstdoutに通信以外のバイトを書いているサインです。自分のコードの console.log() / print() だけでなく、依存ライブラリが起動時バナーをstdoutに出すケースもあります。ログ出力を全部stderrに寄せた上で、Inspectorで接続し直すと切り分けが速いです。
claude mcp addの引数が意図どおり解釈されない
--env KEY=value などのオプションをサーバー名の後ろに書くと、サーバー側コマンドの引数と混ざって失敗します。「オプション → サーバー名 → -- → 起動コマンド」の順序を固定で覚えてください。
ツール定義を変えたのに反映されない
stdioサーバーはセッション開始時に起動されるため、コード変更後はClaude Codeの再起動(または新セッション)が必要です。サーバー側で list_changed 通知を実装すれば、稼働中のセッションにツール・リソース・プロンプトの変更を動的に反映させることもできます。
Claudeがツールを呼んでくれない
接続は正常なのに使われない場合、ツールの description が曖昧で選択肢に挙がっていないことが多いです。「何を入力すると何が返るか」「どんな依頼のときに使うか」を説明文に明記します。Claude Codeはツール定義を必要時に検索して読み込む方式(ツール検索)が既定のため、説明文の質がそのまま発見性に直結します。説明はそれぞれ2KBで切り詰められるので、重要な情報を先頭に書いてください。
よくある質問
MCPサーバーの自作はどの言語でもできますか
プロトコル自体はJSON-RPCベースで言語非依存です。公式SDKはTypeScript・Pythonのほか、Java・Kotlin・C#・Go・Ruby・Rust・Swift・PHPに提供されており、本記事で扱った2言語が最もドキュメントと実例が厚い選択肢です。
自作サーバーはClaude Desktopでも使えますか
使えます。Claude Desktopでは claude_desktop_config.json(macOSでは~/Library/Application Support/Claude/配下)の mcpServers にstdioサーバーの起動コマンドを登録します。逆に、Claude Desktopで設定済みのサーバーを claude mcp add-from-claude-desktop でClaude Code側に取り込むこともできます。
SSEトランスポートで実装してもよいですか
新規実装では推奨されません。HTTP+SSE方式はプロトコル2025-03-26版でStreamable HTTPに置き換えられ、Claude CodeのドキュメントでもSSEは非推奨と明記されています。既存のSSEサーバーへの接続は引き続き可能ですが、これから自作するリモートサーバーはStreamable HTTPで実装するのが妥当です。
ツールの実行がタイムアウトします
2系統の設定があります。サーバーの起動待ちは環境変数 MCP_TIMEOUT(ミリ秒)で延長できます。ツール実行そのものは、.mcp.json の該当サーバーに "timeout": 600000 のようにミリ秒で指定するか、環境変数 MCP_TOOL_TIMEOUT で全体を調整します。時間のかかる処理は、ジョブ投入と結果取得をツール2つに分ける設計も有効です。
ツールの出力が大きすぎると警告されます
Claude CodeはMCPツール出力が10,000トークンを超えると警告を出し、既定の上限は25,000トークンです。上限は環境変数 MAX_MCP_OUTPUT_TOKENS で引き上げられますが、サーバー側でページネーションや件数制限を実装してコンテキスト消費を抑えるほうが、応答品質の面でも有利です。
コードを書かずにサーバーの雛形を作れますか
Claude Code公式の mcp-server-dev プラグイン(/plugin install mcp-server-dev@claude-plugins-official で導入)を使うと、ユースケースの質問に答えるだけでstdio / HTTPサーバーをスキャフォールド(雛形生成)できます。生成後のスキーマ調整やテストは本記事の手順がそのまま使えます。
まとめ
MCPサーバーの自作は「stdioで最小実装 → Inspectorでテスト → claude mcp addで接続 → 必要ならHTTP化 → npmとレジストリで配布」という段階を踏むのが、複雑さを抑えつつ本番運用に届く道筋です。現行SDK(TypeScript 1.29.0 / Python 1.27.2)では、ツール1本なら30行前後、3要素を揃えても100行に収まります。
押さえる原則は3つに集約されます。stdioではstdoutを汚さない、HTTPではOrigin検証と認証を最初から入れる、ツールのdescriptionはClaudeが読む前提で書く。この3点を押さえれば、社内APIや自作ツールをClaudeの行動範囲に組み込む拡張が安定して回ります。
設計の幅を広げたい場合は、ツール呼び出しをコード実行に寄せる設計論を扱ったMCPでコード実行する設計、サーバーをコンテナ境界の外側に置いて隔離するDevContainerでの安全な実行環境構築が次の一歩として参考になります。