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

MCP サーバーのデプロイ方法:リモートホスティング完全ガイド(2026)

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

MCP サーバーのデプロイ方法を解説。Streamable HTTP、仕様に沿った OAuth 認証、Cloudflare Workers・コンテナ・サーバーレスでのホスティング、スケーリング、公開前のセキュリティチェックリストまで。

MCP サーバーをデプロイするには、Streamable HTTP トランスポートで単一の HTTPS エンドポイント(通常は /mcp)から提供し、Cloudflare Workers、任意のクラウド上のコンテナ、サーバーレス関数など、Web ハンドラーを実行できるプラットフォームにホストします。その前段に、MCP の認可仕様が定めるとおり OAuth のベアラートークン検証を置きます。現行の 2026-07-28 仕様ではプロトコルがステートレスなので、デプロイしたサーバーは通常のロードバランサーの背後で水平スケールできます。

このガイドは、すでに動作するサーバーがあることを前提にしています。まだない場合は、MCP サーバーの作り方(TypeScript)または Python MCP サーバーガイドから始めてください。サンプルは公式 SDK v2 のリリース、@modelcontextprotocol/server 2.3.1 と mcp 2.3.0 を使っています。

サーバーをリモートにすると何が変わるか

ローカルサーバーは、1 つのクライアントが stdio で起動する子プロセスです。リモートサーバーは、多数のクライアントが共有する URL です。次の 4 つがあなたの責任になります。

  1. トランスポート:Streamable HTTP。すべてのメッセージは 1 つのエンドポイントへの HTTP POST で、サーバーは JSON か SSE ストリームで応答します。旧来の 2 エンドポイント構成の HTTP+SSE トランスポートは非推奨です。詳しくは MCP の SSE は非推奨かを参照してください。
  2. 認証:インターネット上の誰でも URL にアクセスできるため、本物のアクセス制御が必要です。
  3. ホスティングとスケーリング:プロセスのライフタイム、タイムアウト、複数インスタンス。
  4. ハードニング:Host/Origin の検証、レート制限、ログ、シークレット管理。

ホスティング方法を選ぶ

方法向いているケース注意点
Cloudflare Workers(または Deno/Bun)小規模でステートレスなサーバー、グローバルなエッジ、無料枠リクエストごとの CPU/時間制限。有効化しない限り Node API なし
コンテナ(Cloud Run、ECS/Fargate、Fly.io、Kubernetes、VM)任意の言語、長時間実行のツール、プライベートネットワークイメージ、TLS、スケーリング設定は自分で管理
サーバーレス関数(AWS Lambda、Vercel、Netlify)変動の大きいトラフィック、呼び出し課金関数のタイムアウトで長いストリームが切れる
マネージド MCP プラットフォーム認証・監査・レジストリなどのゲートウェイ機能が欲しいチームベンダーロックイン。MCP ゲートウェイとプロキシの違いで比較を

新しい仕様にはセッションがないため、4 つとも動き方は同じです。リクエストが来て、レスポンスを返すだけです。MCP の都合ではなく、データや依存関係がどこにあるかで選んでください。

具体例:Cloudflare Workers

TypeScript SDK の createMcpHandler は Web 標準の fetch ハンドラーを返します。これは Worker がエクスポートする形そのものです。作り方ガイドの createServer ファクトリを使い、wrangler dev(Wrangler 4.151)でローカル検証しました。nodejs_compat フラグは不要です。

src/worker.ts:

import {
  createMcpHandler,
  hostHeaderValidationResponse,
} from '@modelcontextprotocol/server';

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

const handler = createMcpHandler(createServer);
const ALLOWED_HOSTS = [
  'npm-info-mcp.your-subdomain.workers.dev',
  'localhost',
  '127.0.0.1',
];

export default {
  async fetch(request: Request): Promise<Response> {
    if (new URL(request.url).pathname !== '/mcp')
      return new Response('Not found', { status: 404 });
    const rejected = hostHeaderValidationResponse(request, ALLOWED_HOSTS);
    return rejected ?? handler.fetch(request);
  },
};

wrangler.jsonc:

{
  "name": "npm-info-mcp",
  "main": "src/worker.ts",
  "compatibility_date": "2026-10-01"
}

実行とデプロイ:

npm install -D wrangler
npx wrangler dev        # http://localhost:8787/mcp
npx wrangler deploy     # prints https://npm-info-mcp.<your-subdomain>.workers.dev

MCP Inspector で本番エンドポイントを確認します。

npx @modelcontextprotocol/inspector --cli https://npm-info-mcp.your-subdomain.workers.dev/mcp \
  --transport http --method tools/list

API キーは npx wrangler secret put NAME で保存し、コードには絶対に書かないでください。Cloudflare は Agents SDK に独自の MCP ツールも用意しており、Durable Objects ベースの状態が必要な場合に便利です。

コンテナとその他のランタイム

Node では、同じハンドラーを Express にマウントします。0.0.0.0 にバインドすると localhost 限定のデフォルト保護が外れるため、createMcpExpressApp で Host の許可リストを明示します。

import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler } from '@modelcontextprotocol/server';

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

const app = createMcpExpressApp({
  host: '0.0.0.0',
  allowedHosts: ['mcp.example.com'],
});
const node = toNodeHandler(createMcpHandler(createServer));
app.all('/mcp', (req, res) => void node(req, res, req.body));
app.listen(Number(process.env.PORT ?? 3000), '0.0.0.0');

Python では、許可リスト付きで ASGI アプリを作り、任意の ASGI サーバーで実行します。許可リストがないと、実際のホスト名の背後ではすべてのリクエストが 421 Misdirected Request になります。

from mcp.server.transport_security import TransportSecuritySettings

app = mcp.streamable_http_app(
    transport_security=TransportSecuritySettings(allowed_hosts=["mcp.example.com", "mcp.example.com:*"])
)
# uvicorn server:app --host 0.0.0.0 --workers 4 --proxy-headers --forwarded-allow-ips='<proxy ip>'

TLS をロードバランサーで終端する場合、--proxy-headers を付けるとリダイレクト先が http:// になるのを防げます。どちらのアプリも通常の Dockerfile でパッケージし、ほかの Web サービスと同じようにデプロイできます。SDK にはヘルスチェック用ルートが含まれていないので、自分で追加してください(Python では @mcp.custom_route("/health", methods=["GET"]))。

仕様どおりに認証を追加する

MCP の認可仕様では、あなたのサーバーは OAuth 2.1 のリソースサーバーとして扱われます。トークンを検証する側であり、発行は別の認可サーバー(Auth0、Okta、Entra ID、Keycloak、WorkOS など)が行います。流れは次のとおりです。

  1. 有効なトークンのないリクエストには 401 を返し、WWW-Authenticate: Bearer ヘッダーで Protected Resource Metadata(保護リソースメタデータ) ドキュメント(RFC 9728)を指し示します。
  2. そのドキュメントに認可サーバーが記載されています。クライアントはそれを見つけて登録し(2026-07-28 仕様は Client ID Metadata Documents を推奨し、動的クライアント登録を非推奨にしています)、PKCE でユーザーにログインしてもらい、トークン付きで再試行します。
  3. サーバーはトークンを検証します。audience(オーディエンス) の確認も含みます。別のリソース向けに発行されたトークンは拒否しなければなりません。

fetch ランタイム上の TypeScript SDK では、ゲートは次のようになります(verifyAccessToken は自前の JWT 検証またはイントロスペクション)。

import {
  createMcpHandler,
  requireBearerAuth,
} from '@modelcontextprotocol/server';

const mcpServerUrl = new URL('https://mcp.example.com/mcp');
const gate = requireBearerAuth({
  verifier: { verifyAccessToken },
  requiredScopes: ['mcp'],
  expectedResource: mcpServerUrl,
});
const handler = createMcpHandler(createServer);

export default {
  async fetch(request: Request): Promise<Response> {
    const auth = await gate(request);
    if (auth instanceof Response) return auth; // 401/403 with the WWW-Authenticate challenge
    return handler.fetch(request, { authInfo: auth });
  },
};

oauthMetadataResponse が well-known のメタデータドキュメントを返し、ツールハンドラーは ctx.http.authInfo で呼び出し元を読み取れます。マシン専用の社内サーバーなら、ヘッダーに静的な API キーを入れる方式でも構いません。その場合もシークレットとして扱い、定期的にローテーションしてください。図解付きの全体の流れは MCP の認証と OAuth にあります。

スケーリングと状態管理

  • 新しいクライアント(2026-07-28) は自己完結したリクエストを送ります。initialize ハンドシェイクも Mcp-Session-Id もありません。どのインスタンスでもどのリクエストでも処理できるため、スティッキーセッションは不要です。
  • 古いクライアント(2025 年の改訂版) はセッションを前提にすることがあります。TypeScript のハンドラーはデフォルトでそれらをステートレスに処理します(legacy: 'stateless')。Python ではレガシーセッションが 1 つのワーカーのメモリに保持されるため、スティッキーセッションを有効にするか、stateless_http=True で実行してください。
  • 呼び出しをまたいで残す必要がある状態は、プロセスのメモリではなく自前のストレージ(データベース、KV、Redis)に置き、サーバーが払い出してツール引数として受け渡す ID で参照します。
  • マルチラウンドトリップのツール(呼び出し中にユーザーに質問するもの)は、封印された requestState をクライアントに渡します。Python では全インスタンスに同じ RequestStateSecurity(keys=[...]) と同じサーバー名を設定してください。そうしないと、別のワーカーに届いた再試行が失敗します。
  • 変更通知(subscriptions/listen)は 1 つのインスタンスに固定される長寿命のストリームです。共有の pub/sub バスでイベントを各インスタンスに配信し、プラットフォームのタイムアウトが長いストリームを許容するか確認してください。

公開前のセキュリティチェックリスト

  • HTTPS のみを使い、Host と Origin を検証する(前述の SDK ヘルパーを使用)。
  • ヘルスチェックと OAuth メタデータドキュメント以外のすべてのルートで認証を必須にし、トークンのオーディエンスと有効期限を検証する。
  • クライアントのトークンを上流 API にそのまま渡さない。自前の認証情報かトークン交換を使う。
  • 上流の認証情報は最小権限にし、シークレットマネージャーに保管する。
  • ユーザー単位・IP 単位のレート制限とリクエストサイズの上限を設ける(Python SDK のデフォルトはボディ 4 MiB まで)。
  • 破壊的なツールは明確に示し、実際に読み取り専用のツールには読み取り専用アノテーションを付ける。
  • ツール呼び出しは呼び出し元の ID とともに記録し、シークレットやペイロード全体は記録しない。
  • MCP セキュリティのベストプラクティスのチェックリストを確認し、本番 URL に対して Inspector でテストする。

次のステップ

サーバーが公開されたら、公式 MCP Registry にその URL を remotes エントリ(タイプは streamable-http)として登録し、MCP.so に投稿して MCP サーバーディレクトリで見つけてもらえるようにしましょう。よく運用されているリモートサーバーの例としては、Sentry や Notion の公式サーバーが参考になります。リモート MCP サーバーガイドもあわせてどうぞ。

よくある質問

MCP サーバーはどうやってデプロイしますか?

Streamable HTTP トランスポートで /mcp などの単一の HTTPS エンドポイントから提供し、Cloudflare Workers、コンテナ、サーバーレス関数にホストして、前段に OAuth のベアラートークン検証を置きます。その後、本番 URL を MCP Inspector でテストします。

MCP サーバーは無料でデプロイできますか?

できます。小規模でステートレスなサーバーなら Cloudflare Workers の無料枠に収まり、多くのコンテナやサーバーレスのプラットフォームにも無料枠があります。トラフィックや CPU 時間が増えてから料金が発生します。

リモート MCP サーバーにスティッキーセッションは必要ですか?

2026-07-28 仕様のクライアントには不要です。各リクエストが自己完結しており、どのインスタンスでも応答できるからです。2025 年版の古いクライアントはセッションを使う場合があるので、ステートレスに処理するかセッションアフィニティを有効にしてください。

デプロイした MCP サーバーはどう保護すればよいですか?

HTTPS を使い、Host と Origin ヘッダーを検証し、OAuth トークンを必須にしてオーディエンスと有効期限を確認します。さらにレート制限を設け、上流の認証情報はシークレットマネージャーに保管し、クライアントのトークンを他の API にそのまま渡さないでください。