MCP.so
ログイン
ブログに戻る

Build MCP Server:TypeScript SDK v2 で MCP サーバーを作る手順

2026年10月15日MCP.so TeamMCP.so Team

MCP サーバーをゼロから構築(build mcp server):良いツール設計、stdio と Streamable HTTP に対応した TypeScript サーバーの実装、テスト、Claude Code への接続、npm と MCP Registry への公開まで解説します。

MCP サーバーを構築するには、公式 SDK を選び、型付きの入力と分かりやすい説明を持つツールをいくつか登録して、stdio(ローカルクライアント向け)または Streamable HTTP(リモートクライアント向け)で提供します。TypeScript SDK v2 なら約 40 行のコードで済みます。MCP Inspector でテストし、Claude Code や Cursor などのクライアントに追加し、npm と MCP Registry に公開すれば、ほかの人もインストールできるようになります。

このガイドでは、小さいながら実用的なサーバーを最初から最後まで作ります。すべてのコードは @modelcontextprotocol/server 2.3.1(2026 年 10 月時点の安定版)で実際に動作確認しています。このバージョンは 2026-07-28 版 MCP 仕様 を実装しつつ、古いクライアントにも応答します。概念に馴染みがない場合は、先に MCP サーバーとは を読んでください。

最初に知っておくこと

MCP サーバーは AI アプリケーションに 3 種類のものを公開します。

プリミティブ使うかどうかを決めるのは典型例
Tools(ツール)モデルAPI 呼び出し、データベース検索、ファイル書き込み
Resources(リソース)アプリケーションファイルの内容、スキーマ、ドキュメント
Prompts(プロンプト)ユーザーユーザーがメニューから選ぶ再利用可能なテンプレート

ほとんどのサーバーは主にツールで構成されます。ツールとリソースの違いは MCP リソースとツールの違い で解説しています。同じサーバーコードを 2 つのトランスポートで動かせます。

  • stdio:クライアントがサーバーを子プロセスとして起動し、stdin と stdout で通信します。ローカルツールのデフォルトです。
  • Streamable HTTP:サーバーは複数のクライアントが接続する Web エンドポイント(通常は /mcp)になります。リモートサーバーはこの方式で提供します。古い HTTP+SSE トランスポートは非推奨です。詳しくは MCP の SSE は非推奨か を参照してください。

ステップ 1:コードを書く前にツールを設計する

モデルに見えるのは、各ツールの名前・説明・入力スキーマだけです。明確なツールは正しく呼ばれ、曖昧なツールは無視されるか、誤った引数で呼ばれます。どの言語でも通用するルールをいくつか挙げます。

  • エンドポイントではなくタスクをモデル化する。 必要なフィールドを返す get-package 1 つのほうが、生の API ルートを薄くラップした 3 つのツールより優れています。ツールが少なければトークン消費も減ります。MCP ツールディスカバリー も参照してください。
  • 説明はモデルに向けて書く。 ツールが何をするのか、いつ使うのかを書きます。例:「依存関係を推奨またはインストールする前に使ってください。」
  • すべての入力に型と制約を付ける。 列挙型、最小値・最大値、説明内の例があれば、ハンドラーが動く前にほとんどの誤った呼び出しを防げます。
  • モデルが次の行動に移れるエラーを返す。 「パッケージが見つかりません。スペルを確認してください」ならモデルは再試行できますが、スタックトレースでは無理です。
  • 読み取り専用のツールを明示する。 readOnlyHint などのヒントは、先にユーザーに確認すべきかどうかをクライアントが判断する助けになります。ただしヒントであって、セキュリティ機構ではありません。
  • シークレットを引数に含めない。 API キーは環境変数か OAuth から取得し、決してモデルから受け取らないでください。

サンプルサーバーのツールは get-package の 1 つだけです。npm パッケージの最新バージョンと週間ダウンロード数を調べるもので、API キーは不要です。

ステップ 2:プロジェクトをセットアップする

Node.js 20 以降が必要です。

mkdir npm-info && cd npm-info
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod
npm install -D typescript tsx @types/node
mkdir src

v2 では SDK が複数のパッケージに分かれています。サーバー用の @modelcontextprotocol/server、クライアント用の @modelcontextprotocol/client、そして @modelcontextprotocol/node や @modelcontextprotocol/express などのオプションのアダプターです。以前の単一パッケージ @modelcontextprotocol/sdk は v1 系です。アップグレードする場合は 公式の移行ガイド に従ってください。

tsconfig.json を追加します。SDK の README によると、TypeScript 6 以降では "types": ["node"] を明示的に設定する必要があります。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "build",
    "rootDir": "src",
    "strict": true,
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["src"]
}

ステップ 3:サーバーを書く

サーバーは、新しい McpServer を生成するファクトリー関数にまとめます。両方のトランスポートがこのファクトリーを共有します。src/server.ts を作成します。

import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

export function createServer(): McpServer {
  const server = new McpServer({ name: 'npm-info', version: '1.0.0' });

  server.registerTool(
    'get-package',
    {
      title: 'Look up an npm package',
      description:
        'Get the latest version, description and weekly downloads of an npm package. Use this before recommending or installing a dependency.',
      inputSchema: z.object({
        name: z
          .string()
          .min(1)
          .describe(
            'Exact npm package name, e.g. "zod" or "@modelcontextprotocol/server"'
          ),
      }),
      annotations: { readOnlyHint: true, openWorldHint: true },
    },
    async ({ name }) => {
      const meta = await fetch(
        `https://registry.npmjs.org/${name.replace('/', '%2F')}/latest`
      );
      if (!meta.ok) {
        return {
          content: [
            {
              type: 'text',
              text: `Package "${name}" not found on npm (HTTP ${meta.status}). Check the spelling.`,
            },
          ],
          isError: true,
        };
      }
      const pkg = (await meta.json()) as {
        version: string;
        description?: string;
      };
      const dl = await fetch(
        `https://api.npmjs.org/downloads/point/last-week/${name}`
      );
      const downloads = dl.ok
        ? ((await dl.json()) as { downloads: number }).downloads
        : undefined;
      return {
        content: [
          {
            type: 'text',
            text: `${name}@${pkg.version}: ${pkg.description ?? 'no description'}. Weekly downloads: ${downloads ?? 'unknown'}.`,
          },
        ],
      };
    }
  );

  return server;
}

書くスキーマは Zod スキーマだけです。SDK はこれをモデルに見せる JSON Schema に変換し、各呼び出しの引数を検証し、ハンドラーの引数の型も推論します。検証に失敗した呼び出しには isError の結果が返り、モデルはそれを読んで修正できます。

ステップ 4:stdio で提供する

src/stdio.ts を作成します。

#!/usr/bin/env node
import { serveStdio } from '@modelcontextprotocol/server/stdio';

import { createServer } from './server.js';

void serveStdio(createServer);
console.error('npm-info MCP server running on stdio');

stdio では stdout がプロトコルそのものを運びます。console.log が 1 つあるだけで JSON-RPC のストリームが壊れるので、ログは console.error で出力してください。

ステップ 5:Streamable HTTP で提供する

リモートエンドポイントにするには、同じファクトリーを createMcpHandler でラップします。これは Web 標準の fetch ハンドラーを返し、リクエストごとに新しいサーバーインスタンスを生成します。Node ではアダプターをインストールし(npm install @modelcontextprotocol/node)、src/http.ts を作成します。

import { createServer as createHttpServer } from 'node:http';
import {
  localhostHostValidation,
  localhostOriginValidation,
  toNodeHandler,
} from '@modelcontextprotocol/node';
import { createMcpHandler } from '@modelcontextprotocol/server';

import { createServer } from './server.js';

const handler = createMcpHandler(createServer);
const nodeHandler = toNodeHandler(handler);
const validateHost = localhostHostValidation();
const validateOrigin = localhostOriginValidation();

createHttpServer((req, res) => {
  if (!validateHost(req, res) || !validateOrigin(req, res)) return;
  void nodeHandler(req, res);
}).listen(3000, '127.0.0.1', () =>
  console.error('npm-info MCP server on http://127.0.0.1:3000/mcp')
);

ハンドラー自体は Host ヘッダーも Origin ヘッダーもトークンもチェックしないため、前段の 2 つの localhost ガードで DNS リバインディング攻撃を防ぎます。npx tsx src/http.ts を実行し、curl で確認します。

curl -s -X POST http://127.0.0.1:3000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

get-package の定義を含む SSE の message イベントが 1 つ返ってきます。ハンドラーはステートレスなので、どんなロードバランサーの背後でも水平にスケールします。OAuth を前段に置いて公開ホスティングする方法は MCP サーバーのデプロイ方法 で解説しています。

ステップ 6:MCP Inspector でテストする

MCP Inspector は公式のテストクライアントです。stdio のエントリーポイントに対して Web UI を開きます。

npx @modelcontextprotocol/inspector npx tsx src/stdio.ts

Connect をクリックし、Tools を開いて zod を指定して get-package を実行します。続いて無効な名前を試し、エラー時の挙動を確認します。CI では CLI モードを使います。

npx @modelcontextprotocol/inspector --cli npx tsx src/stdio.ts \
  --method tools/call --tool-name get-package --tool-arg name=@modelcontextprotocol/server

ステップ 7:Claude Code と Cursor に接続する

ローカルクライアントに必要なのは起動コマンド 1 つだけです。Claude Code では、起動コマンドをすべて -- の後ろに置きます。

claude mcp add npm-info -- npx tsx /absolute/path/to/npm-info/src/stdio.ts

セッション内で /mcp を実行し、接続されていることを確認します。Cursor では .cursor/mcp.json に追加します。

{
  "mcpServers": {
    "npm-info": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/npm-info/src/stdio.ts"]
    }
  }
}

HTTP 版を使う場合は、クライアントに URL を指定します。例:claude mcp add --transport http npm-info http://127.0.0.1:3000/mcp。クライアントにツールが表示されない場合は、まず起動コマンドを手で実行してください。stderr に 1 行出力して待機するのが正常です。クラッシュする場合や、stdout に何かが出力される場合はそれが原因です。

ステップ 8:npm と MCP Registry に公開する

誰でも npx でサーバーを実行できるようにするには、コンパイルして bin エントリーを追加します。package.json は次のとおりです。

{
  "name": "@your-scope/npm-info-mcp",
  "version": "1.0.0",
  "type": "module",
  "bin": { "npm-info-mcp": "build/stdio.js" },
  "files": ["build"],
  "mcpName": "io.github.your-username/npm-info",
  "scripts": { "build": "tsc" }
}

npm run build と npm publish --access public を実行します。これでユーザーは npx -y @your-scope/npm-info-mcp で起動できます。

公式 MCP Registry はメタデータだけを保存し、mcpName フィールドで npm パッケージの所有者であることを検証します。Registry のクイックスタート に沿って進めます。

brew install mcp-publisher        # or download a release binary
mcp-publisher init                # generates server.json
mcp-publisher login github        # namespace io.github.your-username/*
mcp-publisher publish

server.json の name は mcpName と一致させる必要があり、description は 100 文字までです。リモート専用のサーバーは npm を完全に省略し、代わりにタイプが streamable-http の remotes エントリーを記載できます。

ステップ 9:MCP.so に掲載する

最後に、サーバーを MCP.so に登録 して、MCP サーバーディレクトリ を見ている人が見つけられるようにします。インストールコマンド、必要な環境変数、ツール一覧を書いた分かりやすい README を用意してください。ユーザーが サーバーが信頼できるか確認する ときに読むのがこの README です。

ほかの言語

同じ設計ルールとトランスポートは、すべての公式 SDK に当てはまります。最も広く使われているのは TypeScript と Python です。C#、Go、Rust、Ruby も Tier 1 で、Java、Swift、PHP、Kotlin も利用できます(SDK 一覧)。Python については、公式 SDK の MCPServer クラスと uv を使う Python MCP サーバーガイド を参照してください。

次のステップ

よくある質問

自分で MCP サーバーを作るにはどうすればいいですか?

TypeScript や Python などの公式 SDK を選び、明確な説明と型付きの入力を持つツールをいくつか登録して、ローカル向けには stdio、リモート向けには Streamable HTTP で提供します。その後 MCP Inspector でテストし、Claude Code や Cursor などのクライアントに追加します。

MCP サーバーを作るのに最適な言語は何ですか?

最も広く使われ、サンプルも多いのは TypeScript と Python の公式 SDK です。C#、Go、Rust、Ruby も Tier 1 の公式 SDK なので、通常は既存のコードやチームが使っている言語を選ぶのが最善です。

MCP サーバーは stdio と Streamable HTTP のどちらを使うべきですか?

サーバーがユーザーのマシンで動き、クライアントが子プロセスとして起動する場合は stdio を使います。多くのユーザーが接続するエンドポイントをホストする場合は Streamable HTTP を使い、新しいサーバーでは非推奨の HTTP+SSE トランスポートは避けてください。

MCP サーバーをほかの人に使ってもらうにはどう公開しますか?

パッケージを npm または PyPI に公開してユーザーが npx や uvx で実行できるようにし、mcp-publisher CLI で公式 MCP Registry にメタデータを登録します。MCP.so などのディレクトリに登録すれば、さらに見つけてもらいやすくなります。