「MCPサーバーを自作するには、何から始めればいいの?」
「MCPサーバーの作り方を、動くところまで一気に知りたい!」
MCPサーバーを自作すると、AIエージェントに自分の業務専用の手足を持たせられます。
ところが、ネット上の手順どおりに写経しても動かない、という声がここ最近になって増えました。
理由ははっきりしていて、TypeScript SDKがv2になり、パッケージ名もツール定義のAPIも変わったからです。
この記事では公式クイックスタート準拠のv2の書き方で、実装から配布までを通しでまとめています。
この記事でわかること
- MCPサーバーを自作するときに、自分がどの層を作るのかの整理
- TypeScript SDK v2で変わったパッケージ構成とツール定義のAPI
- 環境構築からクライアント登録までの作り方を通しで追う手順
- 動作確認・デバッグ・設計上の注意点と、配布までの流れ
MCPサーバーを自作する前に押さえる全体像
作り方に入る前に、自分がどの層を作ろうとしているのかをはっきりさせておきましょう。
ここが曖昧なままだと、他の記事の手順とかみ合わなくなります。
MCPサーバーを自作する前に整理すること
- MCPサーバーが3層のどこに位置し、つなぐ側・設定する側と何が違うのか
- サーバーが提供できるTools・Resources・Promptsの役割分担
- 自作すべきケースと、既存サーバーで足りるケースの見分け方
MCPサーバーが担う役割と、つなぐ側・設定する側との違い
MCPは、ホスト・クライアント・サーバーの3層でできています。
ホストがClaude DesktopやClaude Codeのようなアプリ、クライアントがその中でサーバーと話す部品、そしてサーバーが実際の処理を持つ側です。
自作するのは、この一番外側のサーバーです。
AIに新しい手足を1本生やす作業だと考えるとイメージしやすいと思います。
ここを分けて考える利点は、詰まったときの切り分けが速くなることでした。
ツールが呼ばれないのはサーバー側の問題なのか、そもそも接続が成立していないのか。層で区切ると、確認する場所が半分になります。
| 立場 | やること | 書くもの |
|---|---|---|
| つなぐ側 | 既存サーバーをエージェントに接続する | 接続の構成 |
| 設定する側 | クライアント側の設定を書く | 設定ファイル |
| 作る側(本記事) | サーバーそのものを実装して配布する | サーバーのコード |
既存サーバーをエージェントにつなぐ話はAIエージェントMCP連携方法|初心者でも今すぐ動かせる手順を解説にまとめてあります。
クライアント側の設定だけを知りたい場合はClaude CodeのMCP設定方法を詳しく解説!のほうが近いです。
自作に取りかかる前に、既存のサーバーを1本だけつないで動かしておくのもおすすめします。
正常に動いている状態を一度でも見ておくと、自作したものが動かないときの比較対象になるからです。
MCPサーバーが提供できる3つの機能
サーバーが外に出せる機能は3種類です。
公式ドキュメントではTools・Resources・Promptsとして整理されています。
この3つは似て見えますが、呼ばれ方がまったく違います。
混ぜて設計すると、あとで「なぜかLLMが使ってくれない」という状態になりました。
違いを一言でいうと、誰が呼ぶかが違うということです。
ToolsはLLMが自分で判断して呼び、Resourcesはクライアントが取りに行き、Promptsは人間が選びます。
サーバーが提供できる機能の役割分担
- Tools:LLMが判断して呼び出す関数。副作用のある操作もここ
- Resources:クライアントが読み取るデータ。ファイルやAPIレスポンスに近い扱い
- Prompts:ユーザーが選んで使うテンプレート。定型作業の入り口
実際のところ、最初の1本はToolsだけで十分です。
ResourcesとPromptsは、使う場面がはっきりしてから足せば間に合います。
逆に、読み取り専用のデータをToolsとして出してしまうと、LLMが毎回「呼ぶかどうか」を判断することになり、動きが安定しません。
設計に迷ったら、その情報を常に見てほしいのか、必要なときだけ取りに行かせたいのかで分けてください。
前者ならResources、後者ならToolsに寄せると、だいたい収まります。
自作すべきケースと、既存サーバーで足りるケース
作り始める前に、既存のサーバーで足りないかを確認してください。
公式とコミュニティのサーバーはかなり揃っていて、ファイル操作やGit連携あたりは自作する意味がほとんどありません。
自作の出番は、そのデータや手順が自分の環境にしかないときです。
社内APIを叩く、独自フォーマットの帳票を読む、自分だけのワークフローを1コマンドにまとめる。こういう場面で効きます。
逆に「便利そうだから作ってみる」で始めると、たいてい2週間で放置されます。
呼ぶ場面が思い浮かばないツールは、LLMも呼びません。
自作する前に確認したいこと
- 同じことをする公式・コミュニティのサーバーが既にないか
- そのツールを呼ぶ場面が、月に何回あるのか
- 自分以外にも使う人がいるのか(いるなら配布の設計も要る)
- 保守を続けられるか(APIが変わったら直すのは自分)
私が最初に作ったのは、社内の記事管理シートを読んで下書きの雛形を返すだけの小さなサーバーでした。
それでも、毎回コピペしていた手順が1回の呼び出しになって、体感がかなり変わりました。
最初の1本は、機能を1つに絞るのが正解だと思っています。
作りながらMCPの型が身につくので、2本目からの速度がまるで違ってきます。
MCPサーバーの自作で使うSDKとバージョンの選び方
ここが今いちばん事故の多いところです。
2026年に入ってからSDKの構成が変わったのに、ネット上の記事の多くが前の書き方のまま残っています。
MCPサーバーの自作で最初に確認するSDK事情
- TypeScript SDKはv2でパッケージが役割ごとに分割された
- v1のコードは写経してもimportの時点で食い違う
- TypeScriptとPythonのどちらを選ぶかの判断軸
TypeScript SDK v2でパッケージが役割ごとに分かれた
v1までは@modelcontextprotocol/sdkという1つのパッケージにすべてが入っていました。
v2ではこれが役割ごとに分かれています。
サーバーを作るなら@modelcontextprotocol/server、クライアントを作るなら@modelcontextprotocol/client、共通のスキーマ定数は@modelcontextprotocol/coreです。
このほかにExpressなどフレームワーク向けのパッケージも分かれています。
| パッケージ | 役割 |
|---|---|
| @modelcontextprotocol/server | サーバーを実装する(本記事で使う) |
| @modelcontextprotocol/client | クライアント側を実装する |
| @modelcontextprotocol/core | 共通のスキーマ定数を参照する |
公式クイックスタートのインストールコマンドも、すでに新しいパッケージ名に更新されています。
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript
v2は2026-07-28版のプロトコル仕様に対応した安定版です。
v1系もしばらくは修正が続きますが、これから書き始めるならv2を選ばない理由はありません。
記事を読むときは、まずnpm installの行を見てください。
そこが@modelcontextprotocol/sdkならv1前提の記事だと判断できます。
古い手順を見分ける目印
- インストール対象が
@modelcontextprotocol/sdkになっている - importパスに
/sdk/server/mcp.jsのような深い階層が含まれている - ツール登録が
server.tool()で書かれている
この3つのどれかが出てきたら、そのコードは書き換えないと動きません。
とはいえ考え方の部分は共通なので、読む価値がなくなるわけではないです。
読み方としては、設計や設計判断は参考にして、コードだけは公式で答え合わせをするのが安全でした。
v1のコードをそのまま写経すると動かない箇所
変わったのはパッケージ名だけではありません。
ツールを登録するAPIの形も変わっていますので、対比で押さえておきましょう。
| 項目 | v1(古い記事の書き方) | v2(現在の公式) |
|---|---|---|
| パッケージ | @modelcontextprotocol/sdk | @modelcontextprotocol/server |
| ツール登録 | server.tool(名前, 説明, 形, 処理) | server.registerTool(名前, 設定, 処理) |
| 入力スキーマ | zodの形をそのまま渡す | inputSchemaにz.object()で包んで渡す |
| zodのバージョン | v3でも動いた | v4.2.0以上が必須 |
すでにv1で書いたサーバーを持っている場合は、公式が用意している移行ツールが便利です。
パッケージのルートで実行すると、import文を一括で書き換えてくれます。
npx @modelcontextprotocol/codemod@latest v1-to-v2 .
自動で直せなかった箇所には@mcp-codemod-errorというマーカーが入ります。
実行後にこの文字列を検索して、残りを手で片付ければ移行は終わりです。
codemodを使う手順
- コミットしていない変更を先に片付けておく
- パッケージのルートでcodemodを実行する
@mcp-codemod-errorを検索して残りを手で直す- zodをv4.2.0以上に上げてからビルドし直す
手作業が残りやすいのは、zodのバージョン差に由来する部分でした。
v2はzod v4.2.0以上が前提なので、v3の書き方をしている箇所は自動変換の対象外になります。
移行するかどうか迷ったら、動いているv1のサーバーは急いで触らなくて構いません。
ただ、これから新しく書くものはv2に寄せておかないと、社内で2種類の書き方が並走して面倒になります。
TypeScriptとPythonのどちらで書くか
SDKは複数の言語で公式に用意されていますが、実質の選択肢はTypeScriptかPythonの2択です。
どちらでも同じことができるので、決め手は言語の好みより「何を作るか」でした。
外部APIを叩く、ファイルを扱う、社内のWebサービスと話す。
このあたりならTypeScriptが素直です。npxで配布できるので、他の人に渡すときの手間も少なくて済みます。
一方、データ加工や集計、既存の解析スクリプトをそのまま外に出したいならPythonが早いです。
手元の関数にデコレータを付けるだけでツールになるので、実験の回転がとにかく速くなります。
SDKを選ぶときの判断軸
- 型で守りたい・配布まで考えている → TypeScript
- データ処理や機械学習の資産がすでにPythonにある → Python
- チームの主言語がある → 迷わずそちらに合わせる
この記事はTypeScript SDK v2で進めます。
公式クイックスタートと同じ構成なので、詰まったときに一次情報へ戻りやすいという利点もあります。
なお、途中で言語を乗り換えるのは思ったより簡単です。
MCPの設計(ツールの粒度・説明文・入力スキーマ)は言語に依存しないので、移すのは実装の中身だけで済みます。
MCPサーバーの作り方をTypeScript SDK v2で実装する
ここからが実装です。
手を動かしながら読めるよう、コマンドとコードをそのまま貼れる形で並べていきます。
MCPサーバーを作る手順
- 開発環境とプロジェクトを用意する
- サーバーインスタンスを生成する
- registerToolでツールを定義する
- stdioトランスポートで起動する
- ビルドしてクライアントに登録する
【手順1】開発環境とプロジェクトを用意する
必要なのはNode.jsだけです。
公式が求めているのはバージョン20以上なので、まず手元を確認してください。
node --version
npm --version
足りていなければNode.js公式サイトから入れ直してください。
そのうえで、プロジェクトの器を作ります。
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript
mkdir src
touch src/index.ts
| パス | 役割 |
|---|---|
| src/index.ts | サーバーのソース。ここに実装を書く |
| build/index.js | ビルド後の実行ファイル。クライアントに教えるのはこちら |
| package.json | 起動方法と配布設定 |
| tsconfig.json | TypeScriptのビルド設定 |
次にpackage.jsonを開いて、ESモジュールとして動くように書き足します。
ここを忘れるとimport文でつまずくので、先に入れておくのが安全です。
{
"type": "module",
"bin": { "my-mcp-server": "./build/index.js" },
"scripts": { "build": "tsc && chmod 755 build/index.js" },
"files": ["build"]
}
あわせてtsconfig.jsonをルートに置きます。
出力先をbuild、入力をsrcにしておけば、あとの登録作業が楽になります。
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"types": ["node"],
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
手順1で用意するもの
- Node.js 20以上(
node --versionで確認) @modelcontextprotocol/serverとzod(v4.2.0以上)type: moduleを書いたpackage.json- 出力先を
buildにしたtsconfig.json
ここまでで、ビルドするとbuild/index.jsが生まれる状態になりました。
このbuild/index.jsのパスが、あとでクライアントに教えるパスになります。
ちなみにmoduleをNode16にしているのは、importの解決をNodeの流儀に揃えるためです。
ここを変えると拡張子つきのimportが必要になったりするので、最初は公式のまま使うのが無難でした。
【手順2】サーバーインスタンスを生成する
ここからはsrc/index.tsを書いていきます。
まずはインポートと、サーバー本体の生成です。
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
const server = new McpServer({
name: "my-mcp-server",
version: "1.0.0",
});
nameはクライアント側に表示される識別子です。
設定ファイルに書くキーと揃えておくと、あとで見返したときに混乱しません。
importが2行に分かれている点にも注目してください。
サーバー本体は@modelcontextprotocol/server、stdio用のトランスポートはそのサブパスから読み込みます。
サーバー生成で決めておくこと
- name:他のサーバーと被らない、用途がわかる名前にする
- version:セマンティックバージョニングで、破壊的変更のたびに上げる
- ファイル構成:ツールが増えたら
src/tools/に切り出す前提で置く
最初はすべてindex.tsに書いて構いません。
ツールが3つを超えたあたりから読みにくくなるので、その時点で分けると迷いが少なく済みます。
versionは飾りではなく、あとで効いてきます。
ツールの引数を変えたり返す形を変えたりしたら、必ず上げておいてください。
利用者が複数いる場合、どのバージョンで動いているのかを聞けるだけで調査がかなり楽になります。
1人で使ううちは実感しにくいのですが、配ったあとに効いてくる部分です。
【手順3】registerToolでツールを定義する
MCPサーバーの中心はここです。
v2ではregisterToolを使い、設定オブジェクトの中に説明と入力スキーマを入れます。
server.registerTool(
"get_article_status",
{
description: "記事スラッグを渡すと、公開状況と最終更新日を返す",
inputSchema: z.object({
slug: z
.string()
.min(1)
.describe("記事のスラッグ(例: mcp-server-build)"),
}),
},
async ({ slug }) => {
const result = await fetchStatus(slug);
return {
content: [{ type: "text", text: result }],
};
},
);
| 引数 | 中身 | 効いてくる場面 |
|---|---|---|
| 第1引数 | ツール名 | LLMが呼び出すときの識別子 |
| 第2引数 | description と inputSchema | 呼ぶかどうかの判断と、引数の埋め方 |
| 第3引数 | 処理本体 | 実際の実行と、返す内容の組み立て |
見落とされがちですが、descriptionはLLMが呼ぶかどうかを決める材料そのものです。
関数名の言い換えではなく、「どういう場面で使うか」を書いてください。
入力スキーマの.describe()も同じ役割を持ちます。
ここが薄いと、LLMが引数を推測で埋めて、意図しない呼び出しが起きます。
書き方のコツは、人間向けのドキュメントではなくLLM向けの指示書として書くことでした。
「記事の公開状況を調べたいときに使う」のように、使う場面を主語にすると精度が上がります。
ツール定義でつまずきやすい点
- zodをv3のまま使っている(v2はv4.2.0以上が必須)
- zodの形を裸で渡している(
z.object()で包む) - 戻り値を文字列で返している(
contentの配列で返す) - 失敗時に例外を投げっぱなしにしている(テキストで理由を返す)
処理が失敗したときは、例外で落とすよりテキストで理由を返すほうが扱いやすいです。
LLMがその文面を読んで、次の一手を自分で選べるようになります。
返す文面も、エラーコードの羅列より自然文のほうが効きます。
「指定されたスラッグの記事が見つかりません。スラッグの綴りを確認してください」と返せば、LLMは自分で修正して再実行します。
ここはAPI設計とは感覚が違うところでした。
相手が機械ではなく言語モデルなので、読んで意味が通る返し方が正解になります。
【手順4】stdioトランスポートで起動する
ローカルで動かすサーバーは、標準入出力でクライアントとやり取りします。
ファイルの末尾に起動処理を足しましょう。
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
ここで絶対にやってはいけないのがconsole.log()です。
標準出力はJSON-RPCの通信路そのものなので、1行でも余計な文字を書くとメッセージが壊れてサーバーが死にます。
ログを出したいときはconsole.error()を使ってください。
こちらは標準エラー出力に流れるので、通信に影響しません。
stdioサーバーでのログの扱い
console.log()は使わない(標準出力がJSON-RPCの通信路のため)- ログは
console.error()か、ファイルに書き出すライブラリを使う - 依存ライブラリが標準出力に何か書いていないかも確認する
この落とし穴、公式ドキュメントにも明記されているのですが、日本語の解説記事ではほとんど触れられていません。
私も最初はここで半日溶かしました。
3つめは見落としやすい点です。
自分のコードにconsole.log()が1つもないのに壊れる場合、疑うのは入れたライブラリのほうでした。
なお、HTTPで動かすサーバーならこの制約はありません。
標準出力に書いてもレスポンスとは別経路なので、通常どおりログを出せます。
【手順5】ビルドしてクライアントに登録する
コードが書けたら、TypeScriptをビルドします。
この工程を飛ばすと、設定を正しく書いてもサーバーは起動しません。
npm run build
次に、クライアント側の設定ファイルにサーバーを登録します。
Claude Desktopならclaude_desktop_config.jsonのmcpServersキーに追記します。
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/絶対パス/my-mcp-server/build/index.js"]
}
}
}
パスは必ず絶対パスで書いてください。
相対パスにすると、クライアントの作業ディレクトリ次第で見つからなくなります。
登録でつまずいたときに見るところ
npm run buildを実行したか(build/index.jsが存在するか)- 設定ファイルのパスが絶対パスになっているか
- JSONの構文が壊れていないか(カンマの付け忘れが多い)
- クライアントを再起動したか
保存したらクライアントを再起動します。
ツール一覧に自作のサーバー名が出れば、ここまでは成功です。
Claude Codeから使う場合は、設定ファイルを直接編集する代わりにコマンドで登録できます。
その手順はクライアント側の話になるので、設定の詳細は既存の解説記事のほうが詳しいです。
ここまでで、自作したサーバーがAIから呼べる状態になりました。
あとは、本当に意図どおり動いているかを確かめる工程です。
自作したMCPサーバーの動作確認とデバッグ
つながったように見えても、ツールが呼ばれない・返り値が空になる、といった詰まり方をよくします。
切り分けの道具と順番を持っておくと、ここで消耗しなくて済みます。
自作したMCPサーバーを確かめる方法
- MCP Inspectorでサーバー単体を直接叩く
- ログの出し方を間違えないようにする
- つながらないときに見る順番を決めておく
MCP Inspectorでサーバー単体を叩く
公式が用意しているデバッグツールがあります。
MCP Inspectorは、クライアントを経由せずにサーバーへ直接リクエストを投げられるツールです。
インストールは不要で、起動コマンドを渡すだけで立ち上がります。
npx @modelcontextprotocol/inspector node build/index.js
起動すると、ブラウザで開けるUIが出てきます。
既定のポートはUIが6274番、内部のプロキシが6277番です。
Inspectorで確認できること
- ツールが一覧に出るか(登録できているか)
- 説明文と入力スキーマが意図どおりに見えているか
- 引数を手で入れて呼んだときの返り値
- エラー時にどんなメッセージが返るか
画面にツール一覧が出てくれば、サーバー自体は正しく動いています。
この状態でClaude側から呼べないなら、原因はサーバーではなく登録側です。
この「サーバー単体で叩ける」という状態を最初に作っておくのが、いちばん効きます。
クライアント越しに試していると、どちらが悪いのか永遠にわからなくなるからです。
ログの出し方を間違えない
デバッグの基本はログですが、MCPサーバーでは出し方に制約がありました。
stdioで動かしている間は、標準出力が通信路として埋まっているからです。
だからconsole.log()は封印して、console.error()に統一します。
これだけでも、処理がどこまで進んだかは十分に追えます。
もう少し本格的にやるなら、ログをファイルに書き出す構成にしてください。
標準エラー出力はクライアントによって扱いが違い、そもそも見られないこともあります。
ログまわりで注意すること
- ツールの引数をそのままログに出すと、機密情報が混ざる場合がある
- 例外を握りつぶすと、LLM側には「何も返ってこない」としか見えない
- 依存ライブラリが標準出力へ書いていないかを確認する
2つめは地味に厄介です。
失敗したなら失敗したと返さないと、LLMは同じツールを何度も呼び直します。
返す内容に迷ったら、「同僚にSlackで報告するならどう書くか」を考えると決まりやすいです。
つながらないときに見る順番
原因はだいたい決まった場所にあります。
思いついた順に試すより、上から順に潰すほうが速いです。
| 順番 | 確認すること | よくある原因 |
|---|---|---|
| 1 | ビルド済みか | コード修正後にビルドしていない |
| 2 | Inspectorで動くか | サーバー本体の実装ミス |
| 3 | 設定ファイルのパス | 相対パス・打ち間違い・JSONの構文エラー |
| 4 | クライアント再起動 | 設定変更が読み込まれていない |
| 5 | nodeのパス | バージョン管理ツール経由でnodeが見つからない |
5番は環境によって刺さります。
nodenvやnvmを使っていると、クライアントから起動したときだけnodeが見つからないことがありました。
その場合はcommandにnodeの絶対パスを書いてしまうのが手っ取り早いです。
行儀は良くないのですが、動かないまま悩む時間よりはずっとましでした。
この順番で潰していけば、どこまでが正常なのかが必ずどこかで確定します。
逆に、心当たりのある場所からいきなり触ると、直したのか壊したのかがわからなくなりました。
MCPサーバーを自作するときの設計と安全対策
動くところまで来たら、次は使い続けられる形に整える段階です。
ここを飛ばすと、ツールが増えたときに一気に扱いにくくなります。
自作サーバーで押さえておく設計の勘どころ
- ツールの粒度と説明文の書き方
- 入力バリデーションと権限の絞り方
- トランスポートの選び方と最新仕様の影響
ツールの粒度と説明文の書き方
ツールは1つにつき1つの目的に割ってください。
「記事を検索して更新もできる」ようなツールを作ると、LLMがどちらの意図で呼んだのか判別できなくなります。
逆に細かく割りすぎるのも困りものです。
ツールが20個並ぶと、LLMは選ぶだけで迷い、関係のないものを呼び始めます。
感覚としては、1つのサーバーにツールは5〜7個までが扱いやすいところでした。
それ以上になるなら、用途ごとにサーバーを分けたほうが結果的に安定します。
説明文に書くと精度が上がる要素
- どういう場面で使うツールなのか(使う条件)
- 何を返すのか(形式ではなく中身)
- 使ってはいけない場面があるならその条件
- 似た名前の別ツールとの違い
4つめは、ツールが増えてきたときに効いてきます。
「こちらは下書き専用、更新は別ツール」と書いておくだけで、呼び間違いがはっきり減りました。
入力バリデーションと権限の絞り方
zodで書く入力スキーマは、型定義であると同時に防御線でもあります。
ここを緩くすると、LLMが生成した想定外の値がそのまま処理に流れ込みます。
相手が言語モデルである以上、渡ってくる値は常に「それらしいけれど間違っているかもしれないもの」です。
人が操作するフォームより、むしろ厳しめに見ておくくらいでちょうどよかったです。
セキュリティ面で最低限やること
- ファイルパスを受け取るなら、扱えるディレクトリを固定して外に出さない
- SQLを組み立てるなら、値を直接埋め込まずプレースホルダを使う
- シェルコマンドを受け取る設計そのものを避ける
- APIキーは環境変数から読み、コードにも引数にも置かない
いちばん効く対策は、そもそも危険な権限を持たせないことです。
読み取りだけで用が足りるなら、書き込みや削除のツールは作らなければいい。
削除が必要な場面でも、いきなり消すのではなくアーカイブに移す設計にしておけば、事故が起きても取り返しがつきます。
MCPサーバーは、AIに直接手を動かさせる仕組みです。
権限は「あとから足す」前提で、最初はできるだけ狭く始めてください。
トランスポートの選び方と最新仕様の影響
サーバーとクライアントの通信路には、大きく2種類あります。
手元で動かすstdioと、ネットワーク越しに使うStreamable HTTPです。
自分だけが使うなら、stdioで十分でした。
クライアントがプロセスを起動してくれるので、サーバーを常駐させる必要も、認証を考える必要もありません。
チームで共有したい、社内のどこからでも使いたい、という段階になって初めてHTTPを検討します。
そのぶん、認証・アクセス制御・運用が全部乗ってきます。
トランスポートを決める基準
- 自分の端末で完結する → stdio
- 複数人・複数端末から使う → Streamable HTTP
- 迷ったらstdioで作り、必要になってから移す
なお、2026-07-28版のプロトコル仕様では、Streamable HTTPからプロトコルレベルのセッションとMcp-Session-Idヘッダーが削除されました。
つまり、サーバー側で接続ごとの状態を持たない設計が前提になったということです。
リモートで動かすサーバーを書くなら、状態はサーバーの外に置く方針で組んでください。
自作したMCPサーバーを配布して使ってもらう
自分だけで使うなら、ここから先は読まなくても大丈夫です。
誰かに渡す予定があるなら、配布の形を先に決めておくと後戻りが減ります。
自作サーバーを他の人に渡すまでの流れ
- npxで動く形にする
- 環境変数とシークレットの渡し方を決める
- 公開前に確認する項目を潰す
npxで動く形にする
配布のいちばん楽な形は、npmに公開してnpxで起動できるようにすることです。
使う側はインストールすら不要になります。
必要なのはpackage.jsonの設定だけで、手順1で書いたbinとfilesがそのまま効いてきます。
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"bin": { "my-mcp-server": "./build/index.js" },
"files": ["build"]
}
ファイルの先頭にシバン(#!/usr/bin/env node)を入れるのを忘れないでください。
これがないと、npxから起動したときに実行ファイルとして認識されません。
npxで配るときに設定する項目
bin:コマンド名と実行ファイルの対応files:npmに含めるディレクトリ(buildだけでよい)- シバン:
build/index.jsの先頭行 version:破壊的変更のたびに上げる
公開後は、使う側の設定がぐっと短くなります。
{
"mcpServers": {
"my-mcp-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"]
}
}
}
社内限定で配りたいなら、npmに出さずGitHubのリポジトリを直接指定する手もあります。
公開範囲を絞りたいときは、こちらのほうが気楽でした。
環境変数とシークレットの渡し方
APIキーやトークンが必要なサーバーは、値をコードに書かないでください。
設定ファイル側から環境変数として渡す形にします。
{
"mcpServers": {
"my-mcp-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"],
"env": { "MY_API_KEY": "xxxxx" }
}
}
}
サーバー側ではprocess.env.MY_API_KEYで読むだけです。
値が入っていないときは、起動時にエラーを出して止めるほうが親切でした。
気づかないまま動いて、呼ぶたびに認証エラーが返る状態がいちばん困ります。
起動時に落としておけば、設定漏れだとすぐ気づけます。
シークレットの扱いで気をつけること
- 設定ファイル自体をリポジトリにコミットしない
- キーの値をログやエラーメッセージに含めない
- 権限は必要な範囲だけに絞ったキーを発行してもらう
3つめは、渡す相手にも伝えておいてください。
読み取りだけで足りるツールに、全権限のキーを設定されると意味がなくなります。
キーの再発行が必要になったときの手順も、READMEに一行だけ書いておくと親切でした。
渡したあとの問い合わせは、たいていこのあたりに集中します。
公開前に確認すること
最後に、渡す前のチェックです。
READMEに何を書くかで、問い合わせの量がまるで変わります。
READMEに書いておく最小項目
- このサーバーが何をするのか(ツール一覧と用途)
- 必要なNode.jsのバージョンと環境変数
- 設定ファイルへの記載例(コピペできる形で)
- 動かないときの確認手順
あわせて、破壊的な操作には歯止めを入れておきます。
削除や更新を含むツールなら、対象を限定する引数を必須にする、件数の上限を設ける、といった作り込みです。
LLMが呼ぶ以上、想定していない順番で実行される前提で組んでおくのが安全でした。
バージョンは、ツールの引数や返す形を変えたら必ず上げてください。
使う側が「昨日まで動いていたのに」となったとき、原因を追える唯一の手がかりになります。
ここまで整えておけば、自作したサーバーは自分以外の環境でも動きます。
渡した相手が設定ファイルに数行足すだけで使い始められる、という状態がゴールです。
MCPサーバーの自作と作り方に関するよくある質問
Q:v1で書いた既存のサーバーは動かなくなりますか?
A:すぐ止まるわけではありません。v1系にもしばらく修正が提供されるので、動いているものを急いで書き換える必要はないです。
ただ、これから新しく書くものはv2に寄せておいてください。
社内に2種類の書き方が並走すると保守が面倒になります。移行するときはcodemodで一括変換できます。
Q:Pythonでも同じことができますか?
A:できます。公式SDKは複数言語で提供されていて、Pythonでも同じ機能のサーバーを書けます。
データ処理や既存の解析スクリプトを流用したいならPythonのほうが早いです。
設計の考え方(ツールの粒度・説明文・入力スキーマ)は言語が変わっても同じなので、片方を理解すればもう片方にも移せます。
Q:Claude以外のクライアントでも使えますか?
A:使えます。MCPは特定の製品に紐づかないプロトコルなので、対応しているクライアントであれば同じサーバーをそのまま接続できます。
設定ファイルの置き場所や書式はクライアントごとに違うので、そこだけは各ツールのドキュメントを確認してください。
サーバー側のコードは変更不要です。
Q:MCPサーバーの自作に費用はかかりますか?
A:サーバーを作ること自体は無料です。
SDKもInspectorも無償で使えます。
費用が発生するとしたら、サーバーが呼ぶ外部APIの利用料と、LLM側の利用料の2つです。
ツールを呼ぶたびに返した内容がコンテキストに載るので、返す文字数を絞っておくと消費を抑えられます。
まとめ
MCPサーバーの自作でつまずく原因の多くは、実装の難しさではなく情報の鮮度でした。
TypeScript SDKはv2でパッケージが分割され、ツール登録もregisterToolに変わっています。
作り方そのものは、実のところシンプルです。
サーバーを1つ作り、ツールを登録し、stdioで起動して、クライアントに絶対パスで教える。それだけです。
最初の1本で押さえること
- インストールするのは
@modelcontextprotocol/server(/sdkではない) - stdioサーバーで
console.log()を使わない - Inspectorでサーバー単体を叩ける状態を先に作る
AIエージェント全体の組み立て方は【初心者向け】AIエージェントの始め方を7つのステップで解説!にまとめてあります。
自作したツールが初めてAIから呼ばれた瞬間は、けっこう感動します。
今の仕事の中で、AIに任せたいのに任せられていない手順は何でしょうか。

