ローカルMCPとリモートMCPの違い
stdioとStreamable HTTPを実行・通信・運用から比較する
MCP Serverを導入するとき、「ローカルMCPかリモートMCPか」という言い方をよくする。実際の設計では、Serverをどこで動かすかと、Clientがどう接続するかを分けて考えた方がよい。
MCP仕様が標準transportとして定めるのはstdioとStreamable HTTPである。stdioではClientがServerを子プロセスとして起動し、標準入出力で通信する。Streamable HTTPでは独立したServerへHTTPで接続する。この実行モデルの差が、プロセス寿命、状態管理、認証、障害、デプロイ方法の違いにつながる。
連載第5回の「Pythonで小さなMCPサーバーを作る」では、ローカルノートを読むstdio Serverを作った。今回は同じServerを共有サービスへ広げる場面を想定し、transport選定で何が変わるかを整理する。
本稿はMCP仕様2025-11-25版を基準にする。特定SDKの設定値ではなく仕様上の動作を中心に扱い、SDK固有の例はその旨を分けて記載する。
結論を先に
個人の端末にあるファイルや開発ツールへ、その端末のHostからアクセスするなら、まずstdioが候補になる。組織で一つのサービスを共有する、Serverを集中管理する、複数端末から同じ機能を使うならStreamable HTTPが候補になる。
| 観点 | stdio | Streamable HTTP |
|---|---|---|
| 典型的な配置 | Hostと同じ端末 | 独立したローカルプロセスまたはリモート環境 |
| 起動 | Clientが子プロセスとして起動 | Serverを別途起動・運用 |
| 通信 | stdin / stdout |
HTTP POST / GET、必要に応じてSSE |
| 利用者 | 起動したClientが中心 | 複数Clientを扱える |
| 認証 | 通常は環境やOSの境界 | HTTP向けMCP認可仕様を適用可能 |
| 状態 | 子プロセス内に持ちやすい | statelessとstatefulを選ぶ |
| 障害範囲 | 一つのHost接続に閉じやすい | 共有利用者へ波及し得る |
| 主な運用対象 | コマンド、パス、環境変数 | TLS、認証、Origin、負荷分散、監視 |
ただし、stdioなら安全、HTTPなら危険という単純な分類ではない。stdio Serverはユーザー権限でローカルファイルを読めることがある。Streamable HTTPは認証とネットワーク境界を適切に設計すれば、Server側へ権限を集約できる。選ぶ基準は「近いか遠いか」より、誰がプロセスと資格情報と状態を管理するかである。
「ローカル・リモート」と「transport」は同じ軸ではない
stdioは通常、同じ端末上の子プロセスとの通信に使う。Streamable HTTPは通常、ネットワーク越しの共有Serverに使う。そのため便宜上、stdioをローカルMCP、Streamable HTTPをリモートMCPと呼ぶことが多い。
しかし、仕様上の概念は次のように分かれている。
flowchart TB
Placement["Serverをどこで動かすか"]
Transport["Clientとどう通信するか"]
Lifecycle["誰が起動・停止するか"]
Authority["誰の権限で実行するか"]
Placement --> Local["同じ端末"]
Placement --> Remote["別ホスト / クラウド"]
Transport --> Stdio["stdio"]
Transport --> HTTP["Streamable HTTP"]
Lifecycle --> Spawn["Clientが子プロセス起動"]
Lifecycle --> Service["独立サービスとして運用"]
Authority --> User["端末ユーザーの権限"]
Authority --> ServiceAccount["サービス用権限"]
たとえば、同じ端末の127.0.0.1でStreamable HTTP Serverを動かす構成もある。この場合は配置がローカルでも、ServerはHostの子プロセスとは限らず、独立したサービスになる。
反対に、SSHなど独自のラッパーで遠隔プロセスの標準入出力へつなぐことも技術的には可能だが、MCPの標準的なリモート構成として認証・再接続・共有運用を解決してくれるわけではない。
stdioはClientがServerプロセスを所有する
stdio transportでは、ClientがMCP Serverをサブプロセスとして起動する。Serverは改行で区切られたUTF-8のJSON-RPCメッセージをstdinから読み、stdoutへ返す。
flowchart TB
Host["MCP Host"]
Client["MCP Client"]
Process["Server子プロセス"]
Tool["ローカルTool / Data"]
Host --> Client
Client -->|"起動・停止"| Process
Client -->|"stdin"| Process
Process -->|"stdout"| Client
Process --> Tool
この構成では接続とプロセス寿命が近い。Hostを終了した、設定を無効にした、Clientが子プロセスを停止した、といった操作でServerも終了する設計にしやすい。Server内のメモリ状態も、そのプロセスとともに消える。
設定で渡す主な情報は、実行コマンド、引数、作業ディレクトリ、環境変数である。Serverを配布する側は、利用者のOS、ランタイム、依存関係、実行ファイルのパスを考慮する必要がある。
stdioが扱いやすい場面
ローカルのリポジトリ、ファイル、開発用DB、CLIを、その端末の利用者だけが扱う場合に向いている。第5回のノートServerもこの例である。
ネットワークの待受ポートを開けず、別途TLSやHTTP Serverを用意しなくてよい。HostごとにServerプロセスが分かれるので、利用者ごとの状態も自然に分離しやすい。
stdioで残る運用課題
ネットワーク公開しないことは、権限が小さいことを意味しない。ServerはHostを動かしているOSユーザーの権限や、設定から渡された資格情報を使える場合がある。
また、利用端末ごとにランタイムとServerを配布・更新する必要がある。100人が同じToolを使うなら、100台の依存関係と設定を揃える運用になる。Serverの修正を一カ所へデプロイすれば終わるリモートサービスとは更新モデルが異なる。
標準出力が通信路なので、ログをstdoutへ混ぜられない点もstdio固有の注意事項である。ログはstderrへ出す。
Streamable HTTPは独立したServerへ接続する
Streamable HTTPでは、ServerはClientから独立したプロセスとして動き、複数のClient接続を扱える。Serverは一つのMCP endpointを提供し、ClientはJSON-RPCメッセージごとにHTTP POSTを送る。
Serverはリクエストにapplication/jsonで一つの応答を返すことも、text/event-streamでSSEストリームを開いて複数のメッセージを返すこともできる。Clientからの先行リクエストなしにServer側の通知やリクエストを送る必要があれば、Clientは同じendpointへHTTP GETを行い、SSEストリームを開ける。
sequenceDiagram
participant C as MCP Client
participant S as MCP Server
C->>S: POST /mcp InitializeRequest
S-->>C: JSONまたはSSEでInitializeResult
C->>S: POST /mcp InitializedNotification
S-->>C: 202 Accepted
C->>S: POST /mcp tools/call
S-->>C: JSONまたはSSEで結果
C->>S: GET /mcp(任意)
S-->>C: SSEで通知・Serverからのrequest
SSEはStreamable HTTPとは別の第三の標準transportではない。Streamable HTTPが応答やServer発のメッセージをストリーミングするときに利用できる仕組みである。単純なServerはJSON応答だけを使い、GETに405 Method Not Allowedを返すことも仕様上可能だ。
独立サービスになることで増えるもの
Serverを一カ所で更新し、複数のHostから同じ機能を利用できる。データや外部APIの資格情報をServer側へ置き、利用端末へ直接配布しない構成も取れる。
その代わり、一般のWebサービスと同様の運用が必要になる。
- HTTPSと証明書
- 認証・認可とトークン管理
- DNS、ロードバランサー、タイムアウト
- 複数Clientの同時実行制御
- メトリクス、ログ、トレース
- Server更新時の互換性と段階的リリース
- 障害時の再接続、重複実行、部分失敗への対応
MCPがHTTPメッセージ形式を標準化しても、これらの運用を自動で肩代わりするわけではない。
セッションとプロセスを混同しない
stdioでは、一つのClient接続と一つの子プロセスを対応させやすい。Streamable HTTPでは、複数のHTTPリクエストが一つの論理的なMCP sessionを構成する場合がある。
Serverがstateful sessionを使う場合、初期化応答のMCP-Session-Idヘッダーでsession IDを発行できる。Clientは以降のHTTPリクエストに同じヘッダーを付ける。Serverがsessionを終了し、そのIDに対して404 Not Foundを返した場合、Clientはsession IDなしで再度初期化する。
flowchart TB
Init["initialize"]
SessionId["MCP-Session-Idを受領"]
Requests["後続のPOST / GET"]
End["DELETEまたはServer側で終了"]
Missing["404 Not Found"]
Reinit["新しいinitialize"]
Init --> SessionId
SessionId --> Requests
Requests --> End
Requests --> Missing
Missing --> Reinit
ここでいうsessionはTCP接続そのものではない。POSTごとにHTTP接続が変わっても、session IDによって論理的な状態を関連付けられる。
stateless Serverも選べる
すべてのStreamable HTTP Serverがsession IDを発行する必要はない。各リクエストを独立して処理できるなら、Serverをstatelessにすると水平スケールしやすくなる。
一方、ServerがClientごとの購読状態、長時間処理、Serverからの通知などを扱うなら、stateful sessionが必要になることがある。その場合は、session状態を一つのプロセスのメモリだけに置くのか、共有ストアへ置くのか、ロードバランサーで同じinstanceへ固定するのかを決める。
Python SDKなどがstateful・statelessの実装支援を提供していても、どの状態を保持し、いつ破棄するかはアプリケーション設計である。
再開は「Toolを安全に再実行する仕組み」ではない
Streamable HTTPは、SSE eventにIDを付け、切断後のGETにLast-Event-IDを付けることで、Serverが未達メッセージを再送する仕組みを定義している。これはストリームの再開とメッセージ欠落の軽減に使える。
ただし、ClientがTool呼び出しの結果を受け取る前に切断した場合、Toolの副作用が実行済みかどうかは別問題である。再接続したからといって、同じtools/callを無条件に送り直してよいとは限らない。
たとえば「Issueを作成する」Toolを再実行すれば、Issueが二重に作られる可能性がある。書き込みToolでは、冪等性キー、操作ID、実行状態の照会、重複排除などをServer側のAPI設計として用意する必要がある。
| 問題 | Streamable HTTPの再開で扱えるか |
|---|---|
| SSE切断後の未達メッセージ再送 | event IDとLast-Event-IDで支援できる |
| MCP sessionの識別 | MCP-Session-Idで支援できる |
| Toolの副作用が実行済みかの判定 | Tool・業務API側で設計が必要 |
| 同じ書き込みの重複防止 | 冪等性設計が必要 |
transportの信頼性と、業務操作のexactly-once実行を同一視しない方がよい。
旧HTTP+SSEとStreamable HTTPの違い
検索すると、MCP Serverに/sse endpointと別のPOST endpointを用意する記事やコードが見つかる。これはプロトコル2024-11-05版のHTTP+SSE transportである。
Streamable HTTPはこの旧方式を置き換えた。大きな違いは、ClientからServerへのPOSTと、ServerからClientへのストリームを一つのMCP endpointへまとめた点にある。
| 観点 | 旧HTTP+SSE | Streamable HTTP |
|---|---|---|
| 仕様上の位置づけ | 旧transport | 現行の標準transport |
| endpoint | SSE接続とPOST送信が分かれる | 一つのMCP endpointがGET/POSTを扱う |
| 応答 | SSE中心 | JSON応答またはSSEを選べる |
| 後方互換 | 古いClient・Server向け | まずPOST initializeを試す |
現行仕様は、後方互換が必要なServerに旧endpointを新endpointと並べて提供する方法を示している。Client側はまず指定URLへPOSTで初期化を試し、特定の4xx応答なら旧SSE接続へフォールバックできる。
ここで「SSEは廃止された」と表現すると誤解を招く。廃止方向なのは二つのendpointを使う旧HTTP+SSE transportであり、SSE自体はStreamable HTTPのストリーミングにも使われている。
認証の違い
MCPの認可仕様はHTTP-based transportを対象にしている。認可を実装する場合、保護されたMCP ServerはOAuthのresource serverとしてaccess tokenを検証し、Clientはresource ownerに代わってtokenを取得・送信する。
一方、仕様はstdio transportにこのHTTP向け認可フローを適用せず、資格情報を環境から取得するよう示している。stdioだから資格情報が不要なのではなく、渡し方と信頼境界が異なる。
flowchart TB
subgraph Local["stdio"]
Host["Host / Client"]
Env["環境・OSの資格情報"]
LocalServer["Server子プロセス"]
Host --> LocalServer
Env --> LocalServer
end
subgraph Remote["Streamable HTTP"]
RemoteClient["MCP Client"]
Auth["Authorization Server"]
RemoteServer["MCP Resource Server"]
RemoteClient --> Auth
Auth --> RemoteClient
RemoteClient -->|"Bearer token"| RemoteServer
end
リモートServerでは、session IDを認証情報の代わりにしてはいけない。MCP-Session-Idは論理sessionを関連付ける識別子であり、HTTPリクエストごとのBearer token検証を省略する根拠にはならない。現行の認可仕様も、同じsessionに属する場合を含め、すべてのHTTPリクエストに認可情報を含めるよう定めている。
認証・認可の詳細は次回のセキュリティ回で扱う。
Streamable HTTPで必要なネットワーク防御
HTTP endpointを公開すると、MCPを理解していない一般のWebクライアントからも到達可能になる。現行のtransport仕様は、ServerにOriginヘッダーの検証を求めている。ローカルでHTTP Serverを動かす場合も、0.0.0.0ではなく127.0.0.1へbindすることを推奨している。
これはDNS rebindingによって、悪意あるWebページからローカルMCP Serverへ到達される可能性があるためだ。CORSの設定だけを追加して完了とはならず、Origin検証、認証、bind先を組み合わせる。
外部公開ではHTTPSを前提にし、reverse proxyで認証を済ませたつもりでも、MCP Serverが信頼するヘッダーとproxyからの到達経路を確認する。Serverへ直接到達できれば、proxyの制御を迂回される。
デプロイとスケールで変わる判断
stdio Serverは、Hostの設定が実質的なデプロイ先になる。利用者の端末で起動でき、必要なローカルデータへ到達できることが重要である。
Streamable HTTP ServerはWebサービスとしてデプロイする。複数instanceへ増やす場合、statelessならリクエストを分散しやすい。statefulならsession stateとSSEの再開方法が制約になる。
| 設計項目 | stateless HTTP | stateful HTTP |
|---|---|---|
| Client固有状態 | 原則保持しない | sessionに関連付けて保持 |
| 水平スケール | 比較的容易 | 共有stateまたはsession affinityが必要 |
| 再起動の影響 | 小さくしやすい | session消失・復元を設計する |
| Server発通知 | 単純構成では限定的 | SSEとsession管理を使いやすい |
| 適する例 | 独立した検索・変換Tool | 購読、長時間処理、進捗通知 |
どちらでも、Toolが接続する下流APIやDBの負荷は残る。MCP Serverだけを水平スケールしても、同じDBへ過剰な同時実行を流せば全体は安定しない。
transport選定の判断手順
最初に「新しいからStreamable HTTP」と決めるのではなく、利用者と権限の置き場所から判断する。
1. データはどこにあるか
利用者の端末にしかないファイルやGit worktreeを読むならstdioが自然である。社内DBやSaaS APIを一元管理するなら、リモートServerへ集約する価値がある。
2. 誰がServerを使うか
一人・一端末なら、共有サービスの認証・監視を持ち込む必要は薄い。複数ユーザー・複数端末で使うなら、端末ごとの配布よりリモートServerの方が更新しやすい場合がある。
3. 資格情報をどこへ置くか
利用者自身の資格情報でローカル操作するのか、組織のサービスアカウントをServer側で使うのかを決める。後者では、利用者ごとの認可と監査が必要になる。
4. 状態と通知が必要か
一回のTool呼び出しで完結するならstatelessに寄せられる。長時間処理、購読、Serverからの通知が必要なら、sessionと再開を設計する。
5. どの障害を引き受けられるか
stdioは端末ごとの環境差と更新を引き受ける。Streamable HTTPはネットワーク、認証、共有障害、サービス運用を引き受ける。問題が消えるのではなく、管理場所が変わる。
典型的な選択例
| ユースケース | 第一候補 | 理由 |
|---|---|---|
| 開いているリポジトリの検索 | stdio | ローカルの作業treeへ直接アクセスする |
| 個人ノートの読み取り | stdio | 一人の端末内で完結しやすい |
| 社内ナレッジ検索 | Streamable HTTP | データ・認可・更新を集中管理しやすい |
| SaaS操作を組織で共有 | Streamable HTTP | token、scope、監査をServer側で統制しやすい |
| ローカルHTTPで重いモデルを常駐 | Streamable HTTP | 複数Hostから独立プロセスを再利用できる |
| オフライン環境のCLI連携 | stdio | ネットワークサービスを前提にしない |
複合構成もあり得る。たとえばローカルstdio Serverが端末内のGit情報を読み、組織のリモートMCP Serverがチケットシステムへ接続する。Hostは複数のServerへClient接続を持てるため、すべてを一つの万能Serverへまとめる必要はない。
まとめ
stdioとStreamable HTTPの差は、単なる接続文字列の違いではない。
- stdioではClientがServerを子プロセスとして起動し、接続とプロセス寿命が近い
- Streamable HTTPではServerが独立し、複数Client、ネットワーク、認証、サービス運用を扱う
- HTTP sessionは接続そのものではなく、複数リクエストを関連付ける論理状態である
- SSEによる再開はメッセージ配送を助けるが、Toolの副作用を自動的に冪等にはしない
- 旧HTTP+SSE transportと、Streamable HTTP内で使われるSSEは区別する
- transportは、データ・資格情報・状態・運用責任をどこに置くかで選ぶ
リモート化は、ローカルServerをネットワークへ露出するだけの作業ではない。利用者の識別、最小権限、token、session、監査、障害時の扱いまで含めて初めて共有サービスになる。
次回の「MCPサーバーを安全に運用する」では、MCPの権限境界、OAuth、prompt injection、情報流出、承認、監査をまとめる。