コードリーディングその2 - 外部プロセスとSSE通信を仲介するセッション型中継サーバ

このコード(mcp-stdio-proxy
)は、「Model Context Protocol (MCP) に準拠した外部プロセスと、HTTP経由で通信するクライアントを接続するための中継サーバ」です。以下のような構成になっています。
🗂️ 概要
機能 | 内容 |
---|---|
外部接続 | 任意の外部コマンド(LLMやMCP対応アプリ)を起動 |
通信方式 | クライアント→HTTP POST、サーバ→SSE(Server-Sent Events) |
セッション管理 | UUIDで接続セッションを管理。セッションごとに外部プロセスが立ち上がる |
Graceful Shutdown | SIGTERMでの終了通知、Lameduck猶予、timeout付きの安全な停止 |
🔁 全体の流れ(クライアントから見た場合)
-
クライアントは
GET /sse
で接続し、セッションが開始 - 外部コマンドが1つ起動し、その標準入力・出力がHTTPと接続
-
POST /messages?sessionId=...
にリクエストを送ると、外部プロセスの stdin に流れる - 外部プロセスの stdout がSSEでクライアントに送信される
🧵 セッションごとのプロセス生成とIO中継
cmd := exec.Command(name, arg...)
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
cmd.Start()
- 各クライアント(
GET /sse
)のたびにプロセスが生成されます。 -
bufio.Scanner(stdout)
で標準出力を1行ずつ取り出してresponseQueue
に送信。 -
requestQueue
→stdin.Write(data)
によってクライアントの入力をプロセスに流し込む。
/messages
→ stdin(POST)
📩 var rawMessage json.RawMessage
json.NewDecoder(r.Body).Decode(&raw)
s.requestQueue <- raw
s.requestQueue <- []byte("\n")
- クライアントからのPOSTリクエストは JSON-RPC 形式。
- パースして method を取り出し、必要ならログ出力。
- 改行で終端することで、標準入力のパーサが1行ずつ処理可能。
/sse
→ stdout(SSE)
📤 fmt.Fprintf(w, "event: message\ndata: %s\n\n", data)
flusher.Flush()
- stdoutから受け取った行は
event: message
というSSE形式でクライアントに送られる。 - 最初にセッション専用のエンドポイントを
event: endpoint
で送信。
🛑 Graceful Shutdown 処理
<-quit
time.Sleep(time.Duration(lameduck) * time.Second)
ctx, cancel := context.WithTimeout(...)
server.Shutdown(ctx)
-
SIGTERM
を受け取るとlameduck
秒だけ待ち、クライアントに処理完了の猶予を与える。 - その後
http.Server.Shutdown()
を呼び、安全に終了。
✅ 利用用途の想定
- LLMなどの「標準入出力ベース」でやり取りするAPIやアプリをHTTPサーバ化したい場合
- MCPプロトコル準拠のプロセスと簡単に通信したい場合
- Webクライアントとの非同期通信(SSE)とPOST APIを同時に提供したい場合
💡 改善・応用アイデア
観点 | 改善内容 |
---|---|
冗長なプロセス起動 | セッションごとのプロセスではなく、プロセスプールで再利用可能に |
標準エラー出力の取り扱い |
stderrPipe を追加してエラーログを記録 |
クォータ制限 | リクエストサイズ、セッション数の制限機構 |
WebSocket対応 | SSEの代わりに WebSocket による双方向通信対応も可 |
もう少し詳しく
🧱 1. グローバル定義・構造体定義
🔹 MCPMethod 定義
type MCPMethod string
const (
InitializeRequest MCPMethod = "initialize"
...
)
- MCPプロトコルにおける メソッド名の列挙子
- JSON-RPCメッセージの
"method"
に対応 - 型安全のため
MCPMethod
型にしている
🔹 MCPMessage 構造体
type MCPMessage struct {
JSONRPC string `json:"jsonrpc"`
Method MCPMethod `json:"method"`
ID any `json:"id,omitempty"`
}
- クライアントが送るMCPメッセージの構造
-
id
は任意(通知なら省略可能)
🔹 session 構造体
type session struct {
id string
responseQueue chan []byte
requestQueue chan []byte
}
- セッション単位の送受信チャネルを管理
- 1接続ごとに外部プロセスと1セッションが紐付く
🧱 2. main関数:フラグ解析と外部コマンド設定
flag.StringVar(&address, "address", "0.0.0.0:8080", "")
...
flag.Parse()
- コマンドライン引数を解析(listenアドレス、終了猶予など)
-
flag.Args()
で 実行すべき外部MCP対応コマンドを取得(例:./github-mcp-server stdio
)
args := flag.Args()
name := args[0]
arg := args[1:]
🧱 3. HTTPハンドラ定義とセッションマップ
sessions := &sync.Map{}
mux := http.NewServeMux()
-
sync.Map
でスレッドセーフにセッションを管理 -
mux
は3つのエンドポイントを定義します:
GET /sse
✅ mux.HandleFunc("GET /sse", func(w http.ResponseWriter, r *http.Request) { ... })
処理の流れ:
- SSE用ヘッダーを設定
-
exec.Command(name, arg...)
で外部プロセスを起動 -
stdin
/stdout
にパイプ接続 -
session
を作成しsessions
に保存 - stdout を逐次
responseQueue
に流す Goroutine 起動 - 初期レスポンスとして
"event: endpoint"
を返す(POST先を知らせる)
w.Header().Set("Content-Type", "text/event-stream")
_, _ = fmt.Fprintf(w, "event: endpoint\ndata: %s\n\n",
fmt.Sprintf("http://%s/messages?sessionId=%s", r.Host, s.id))
flusher.Flush()
-
メインループ:
-
requestQueue
→ stdin へ書き込み(クライアント → 外部プロセス) -
responseQueue
→ SSEで返送(外部プロセス → クライアント)
-
select {
case data := <-s.requestQueue:
stdin.Write(data)
case data := <-s.responseQueue:
fmt.Fprintf(w, "event: message\ndata: %s\n\n", data)
flusher.Flush()
case <-r.Context().Done():
return
}
この select
ブロックは、セッションが生きている間、ずっとI/O中継を続けるループです。
分岐 | 処理内容 |
---|---|
s.requestQueue にデータが来た場合 |
→ 外部プロセス(stdin)に書き込む(クライアント→プロセス) |
s.responseQueue にデータが来た場合 |
→ SSEでクライアントに送る(プロセス→クライアント) |
r.Context().Done() |
クライアントがSSE接続を切断したらループ終了 |
終了時の後始末:
defer func() {
sessions.Delete(s.id)
_ = cmd.Process.Kill()
_ = cmd.Wait()
close(s.requestQueue)
close(s.responseQueue)
}()
POST /messages
✅ mux.HandleFunc("POST /messages", func(w http.ResponseWriter, r *http.Request) { ... })
処理の流れ:
- URLの
sessionId
を取得 - 該当セッションを
sessions.Load()
で取得 - リクエストボディから MCPメッセージを取得(JSON-RPC)
- ログ出力(オプション)
-
requestQueue
にrawMessage
を送信し、さらに改行を追加(改行区切りの入力)
s.requestQueue <- rawMessage
s.requestQueue <- []byte("\n")
- リクエストボディをそのまま外部プロセス(stdin)に流すため、キューに入れる。
- MCPの
stdio
トランスポート仕様により、メッセージは改行で区切る必要があるため、末尾に\n
を追加。
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
// http.StatusText(202) この関数は、HTTPステータスコードに対応する人間向けのメッセージを返します。
// http.StatusText(http.StatusAccepted) // => "Accepted"
_, _ = w.Write([]byte(http.StatusText(http.StatusAccepted))) // つまり []byte("Accepted")
- ステータスコード:202 Accepted
- Content-Type:application/json
- レスポンスボディ:Accepted
余談
この w.Write([]byte(http.StatusText(...))) は あってもなくても動作上は問題ない ことが多いです。
なぜなら:
HTTPクライアントは 202 のステータスコードで処理の受理を判断する
Accepted という文字列がボディにあっても、たいていは無視される
GET /healthz
✅ mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { ... })
- ヘルスチェック用。常に
200 OK
を返す。
🧱 4. HTTPサーバの起動
listener, err := net.Listen("tcp", address)
server := &http.Server{ Handler: mux }
- 通常のHTTPサーバを起動
go func() {
if err := server.Serve(listener); ... // パニック保護付き
}()
🧱 5. Graceful Shutdown 処理
signal.Notify(quit, syscall.SIGTERM)
<-quit
time.Sleep(time.Duration(lameduck) * time.Second)
-
SIGTERM
を受けたら:-
lameduck
秒だけ新規処理停止猶予を与える -
context.WithTimeout()
で安全にShutdown()
-
ctx, cancel := context.WithTimeout(...)
server.Shutdown(ctx)
✅ まとめ:このプログラムの本質
機能 | 説明 |
---|---|
外部コマンド起動 | MCP準拠のサーバ(例: github-mcp-server stdio )を起動 |
入力 | クライアントから POST /messages によるMCPメッセージ受信 |
出力 | 外部プロセスの stdout を SSE でクライアントへ配信 |
セッション管理 | クライアントごとにセッション+プロセスが紐づく |
安全な終了 | SIGTERM → lameduck → graceful shutdown 対応 |

github-mcp-server
とは何か?
🧠
GitHub が定義・採用している Model Context Protocol (MCP) に準拠したサーバプロセス
であり、MCP対応のクライアントや中継(今回の mcp-stdio-proxy
)と接続して、GitHubに関する操作を行う バックエンドエージェント です。
stdio
🔧 動作モードとしての このサーバは複数の起動モード(インターフェース)をサポートしています:
モード | 説明 |
---|---|
stdio |
標準入出力で通信(今回使用) |
http / grpc
|
ネットワーク経由で通信 |
$ ./github-mcp-server stdio
と実行すると:
-
stdin
から受け取った JSON-RPC メッセージ(MCP形式)を解釈し、 -
stdout
に結果(または通知)を出力します。
🔄 実際のやり取りのイメージ
クライアントが POST /messages
で以下のようなメッセージを送る:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list"
}
↓
mcp-stdio-proxy
が stdin
に流し込む
↓
github-mcp-server
は GitHub API を呼び、結果を stdout
に出力:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": [
{ "id": "pr:123", "title": "Fix bug", "kind": "pull_request", ... }
]
}
}
↓
mcp-stdio-proxy
が SSE を通じてブラウザに返す
github-mcp-server
+ mcp-stdio-proxy
を使うのか?
✅ なぜ この構成によって:
- GitHubの機能(PR一覧、issue取得など)を、LLMから安全・抽象的に使える
- スタンドアロンのプロセスとして安全に分離
- WebクライアントからHTTPで簡単に接続できる(SSEとPOST)
というメリットがあります。

記事内
FROM ghcr.io/lifull/keel/mcp-stdio-proxy:main AS mcp-stdio-proxy
FROM ghcr.io/github/github-mcp-server@sha256:fdf04e33b437c523d2f091b7bf8dc3724c88dbf9913d6568f12f0fcf48aaff95
COPY --link --from=mcp-stdio-proxy /usr/local/bin/mcp-stdio-proxy /usr/local/bin/mcp-stdio-proxy
ENTRYPOINT ["/usr/local/bin/mcp-stdio-proxy", "/server/github-mcp-server", "stdio"]
ENTRYPOINT ["/usr/local/bin/mcp-stdio-proxy", "/server/github-mcp-server", "stdio"]
これはコンテナ起動時に、以下のコマンドが実行されることを意味します:
/usr/local/bin/mcp-stdio-proxy /server/github-mcp-server stdio
cmd
の name
と arg
に何が渡るか?
🔍 つまり、Goコード内のこの部分:
args := flag.Args()
name := args[0]
arg := args[1:]
cmd := exec.Command(name, arg...)
に当てはめると、次のようになります:
変数 | 値 |
---|---|
args |
["/server/github-mcp-server", "stdio"] |
name |
"/server/github-mcp-server" |
arg |
["stdio"] |
そしてこの結果、以下の外部プロセスが起動されます:
exec.Command("/server/github-mcp-server", "stdio")
つまり、
mcp-stdio-proxy
が/server/github-mcp-server stdio
を起動し、その stdin/stdout を中継する
という構成です。
stdio
の意味は?
💡 この "stdio"
は、github-mcp-server
側が 「通信を stdin/stdout 経由で行う」 モードに切り替えるための引数です。
多くの MCP 対応プログラムは、以下のようなモード切替を持っています:
-
stdio
: 標準入出力で通信する(mcp-stdio-proxy との連携用) -
http
: 自分で HTTP サーバとして立ち上がる(プロキシ不要)
mcp-stdio-proxy
は stdio
モードを前提としているので、外部ツールに対して "stdio"
の引数を渡す必要があります。
✅ まとめ
-
cmd := exec.Command(name, arg...)
に渡るのは:-
name
:/server/github-mcp-server
-
arg
:["stdio"]
-
-
結果として
github-mcp-server
が stdio モードで起動され、プロキシがその I/O を中継します。

脱線その1
クライアントとは
✅ 質問1:「クライアント」とは誰か?
ここでいう「クライアント」とは、MCPプロトコルに従って mcp-stdio-proxy
に HTTP リクエストを送る存在です。
具体的には、次のようなものです:
🔸 実体の例
クライアントの種類 | 実装例 |
---|---|
AIエディタ | Cursor, Windsurf, Cline など |
Web UI | ブラウザ上の開発支援ダッシュボード(内部ツールなど) |
CLIツール | MCP対応のコマンドラインツール(例: keel ) |
LLMエージェント | MCPクライアントを内部に持つAIチャットボット/LangChain系 |
これらはすべて、POST /messages?sessionId=...
というHTTP APIを呼び出すHTTPクライアントです。
✅ 質問2:「JSON-RPCフォーマットにしているのは誰か?」
結論:上記のクライアント自身が、JSON-RPC形式にして送信しています。
🔸 なぜか?
mcp-stdio-proxy
は、単なる 中継・変換レイヤ です。
以下の変換しかしていません:
HTTP POST (JSON-RPC message) → stdin(外部プロセス)
stdout(レスポンス) → SSEでクライアントへ
したがって、JSON-RPCのメッセージ構造はクライアントが責任を持って作る必要があります。
✅ JSON-RPCの構造はどこで定義されているのか?
それは、Model Context Protocol (MCP) の仕様によって定められています。
- MCPは、JSON-RPC 2.0をベースとしたプロトコルです
- その上で、次のような method 名やパラメータ構造を定義しています:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list"
}
🔁 実際の処理の流れ(まとめ)
-
クライアント(例:Cursor)が
POST /messages?sessionId=...
にリクエストを送る - ボディには MCPに従ったJSON-RPCメッセージが入っている
-
mcp-stdio-proxy
がそのままstdin
に流す - 外部プロセス(例:
github-mcp-server
)がそのメッセージを処理 - 結果が
stdout
に出力され、SSEでクライアントに戻る
🔍 参考図
[ AIエディタ / クライアント ]
│
│ HTTP POST /messages
│ Body: {"jsonrpc": "2.0", "method": "resources/list", ...}
▼
[mcp-stdio-proxy] ⇔ [ 外部プロセス(github-mcp-server) ]
stdin stdout
▼ ▲
実行結果をSSEで返却
✅ まとめ
観点 | 説明 |
---|---|
クライアントとは | MCP形式のJSON-RPCをHTTP経由で送る実行側(Cursor等) |
JSON-RPCを構築するのは | クライアント自身 |
mcp-stdio-proxy の責務 |
JSON-RPCを中継するだけ。解釈・バリデーションはしない |
MCPメッセージ形式の定義 | Model Context Protocolのスキーマ に従う |
JSON-RPCを構築して送るのは誰か
この文脈で「JSON-RPCを構築して送るのは誰か?」という質問は、
実は「MCPに従って行動を決める“実行主体”は誰か?」という根本の問いにあたります。
結論から言えば:
✅ JSON-RPCを構築して送るのは、AIエディタ内の「AIエージェント」または「アシスタント」機構です。
🔍 具体的に言うと誰か?(例別)
ツール名 | JSON-RPCを構築する主体(≒エージェント) |
---|---|
Cursor |
Assistant 機構。モデル(Claudeなど)と対話し、MCP操作を判断して送信 |
Windsurf |
Agent クラス。LLMの応答に応じて tool_call を構築し、MCPサーバに送る |
Cline |
LLM Engine による対話ループ内で構築。MCP tools/call を送る |
🧠 流れを分解してみましょう(CursorのようなAIエディタ)
[ユーザー] → 「このPR、まとめて」
↓
[AIエージェント(LLM)]
→ ClaudeやGPTが推論:「resources/list を呼んでPR一覧を取得すべき」と判断
↓
[エージェントコード(例: TypeScript)]
→ JSON-RPCで以下を構築:
{
"jsonrpc": "2.0",
"method": "resources/list",
"id": 1
}
↓
[HTTP POST] → /messages に送信(mcp-stdio-proxy経由でgithub-mcp-serverへ)
✅ JSON-RPCを「構築する」ロジックはどこにある?
- AIモデル(Claudeなど)は「こういう操作をすべき」と提案する。
- **その提案を受けて、AIエディタ内部の「エージェント層(ツール呼び出し層)」がJSON-RPCを組み立てて送る。
ClaudeやGPTが「完全なJSON-RPC」を出すとは限りません。
多くの場合:
- LLMが「目的」や「使用すべきツール(tool名)」を出す
- エージェントがそれをもとにMCP用のJSON-RPCを組み立てる
🔧 たとえば Windsurf の例
LLMから以下のような出力がある:
{
"tool": "resources/list",
"input": {}
}
すると、Windsurfのコード側で以下のように構築します:
const jsonrpcPayload = {
jsonrpc: "2.0",
id: uuidv4(),
method: "resources/list",
params: {}
}
✅ 結論(もう一度)
- Claudeなどの LLMが「どのMCPメソッドを呼ぶべきか」を提案
- それをもとに、AIエディタ側のコード(エージェントロジック)がJSON-RPCを構築して送信
- つまり、「構築者」は人間とLLMの間にいる“AIエージェント層”
🧭 もし補足したい観点があれば
- Claudeにtool呼び出しを促すプロンプト設計
- LLMが生成するtool呼び出し形式の仕様(tool calling DSL)
- JSON-RPCメッセージとMCPスキーマの接続点
なども説明できます。どこを深掘りしましょうか?
「AIエージェント」または「アシスタント」とは何者?
とても本質的で鋭い質問です。
AIエディタ内に登場する「AIエージェント」「アシスタント機構」「Agentクラス」「LLM Engine」などは、曖昧に語られることが多いですが、**実体としては明確な「アーキテクチャ上の中間層」**です。
以下、それぞれ具体的に解説します。
✅ 一言でいうと
「AIエージェント」や「アシスタント機構」とは、LLMの出力をもとに“外部ツールを呼ぶ判断・実行”を行う、AIエディタの中核的な制御層です。
これは「LLMの応答」と「実際のアクション実行」をつなぐ、中間制御層です。
🧠 全体構造(イメージ)
[ユーザー]
↓ ⬅ プロンプト(自然言語)
[LLM (Claude, GPT-4 etc.)]
↓ ⬅ 「PR一覧を取得したほうがいい」
[AIエージェント / Assistant / Agentクラス]
↓ ⬅ JSON-RPCに変換
[MCPサーバ or 外部ツール(GitHub MCPなど)]
↓
[ユーザーに結果を返す(コード変更・説明 etc)]
📦 各エディタにおける実装の実体
### 1. Cursor
- Assistant 機構と呼ばれている。
- LLMとのやりとり、tool callの判定、呼び出し、結果反映、を行うクラス群。
- JSON-RPCやMCPの詳細は、バックエンドで
mcp-stdio-proxy
や GitHub MCPサーバに委譲される。
🔍 実装:
apps/assistant/src/agent.ts
,apps/assistant/src/session.ts
などに該当
2. Windsurf
-
Agent
クラスという実体があり、以下を担う:- LLMとのやりとり(messagesの蓄積・送信)
- LLMが
tool_call
を返した場合、それを構造的に解釈 - MCPの JSON-RPC を構築して
/messages
に送信
const message = await agent.next(userInput);
if (message.toolCall) {
await callToolViaMCP(message.toolCall);
}
🔍 実装例:
agent.ts
3. Cline
-
LLM Engine
という仕組みで、LLMとの対話ループ+ToolCallの判断を行う。 - Claudeなどの出力を読み取って、「GitHubのPR一覧が必要」と判断すれば
resources/list
を呼びに行く。
🔍 実装例:
cli/engine/llm.ts
✅ 共通点まとめ
機能 | 説明 |
---|---|
LLMとの会話 | Claudeなどとプロンプトを通じてやり取り |
推論結果の解釈 | LLMの出力が「Toolを呼ぶべき」と言っていたら判断 |
実行 | JSON-RPCのMCPメッセージを作って送る(POST /messages) |
状態管理 | ユーザーとの会話履歴、現在のセッション状態などを保持 |
💬 よくある誤解:LLMがすべてやってるわけではない
Claude や GPT は「実行すべきアクションの意図」を自然言語 or structured data(ToolCall)で出すだけです。
**実際に外部ツールを呼んだり、MCP形式に整形して送るのは「AIエージェント層」**の責務です。
✅ まとめ
概念 | 実体 | 役割 |
---|---|---|
AIエージェント |
Assistant (Cursor)、Agent (Windsurf)、LLM Engine (Cline)など |
LLMの出力を解釈し、MCPや外部ツールを実行する制御層 |
LLM | Claude, GPTなど | 意図・アクションを自然言語またはstructured dataで提案する |
MCPサーバ | github-mcp-serverなど | 外部世界(GitHub)に実際にアクセスする |
必要であれば、Claudeなどが返す ToolCallの例 や、**「エージェント層がどうやってtoolを呼ぶべきと判断するか」**などの内部ロジックも説明できます。興味ありますか?

脱線その2
./github-mcp-server stdioの起動タイミング考察
Cursor
や Cline
, Windsurf
などの AIエディタ系ツールが ./github-mcp-server stdio
のような MCP準拠の stdio バックエンド をどのタイミングで起動するかについて、現在わかっている範囲で実行モデルと設計意図を解説します。
✅ 結論:セッション開始時 or 必要時にプロセスを起動
たとえば:
- ファイルを開いたとき
- GitHubと連携する操作をしたとき(例: PRの要約生成・diff説明・レビュー提案)
- 会話エージェント(Assistant)を初めて起動したとき
などの**「セッション開始タイミング」で、mcpプロセスを起動**するのが基本です。
🧠 背景:なぜ毎回起動しないのか?
-
./github-mcp-server stdio
は長寿命プロセスとして動く想定 - 通信は JSON-RPC over stdio なので、ステートフルなセッションが成り立つ
- 必要なときに起動 → セッション中は維持 → 不要になったら kill
🎯 MCPサーバの起動のトリガーとなる処理
実際のAIエディタ(Cursor等)では、次のようなイベントがトリガーになりえます:
トリガー | 説明 |
---|---|
ユーザーが GitHub 連携機能を呼び出す | ex: PRリスト表示・レビュー内容生成 |
.cursor/config.json に GitHub MCP の設定がある |
起動時にバックエンド起動を予約する |
アシスタントが GitHubコンテキストを求める | ex: resources/list で現在のPRを取得 |
ファイル変更に関連する外部ツール呼び出し | GitHubのレビューコメントをもとに変更提案など |
🔁 起動方法(プロセス管理)
多くのツールは以下のような管理を行っています:
- 初回リクエスト時にプロセス起動(遅延初期化)
-
stdin
/stdout
を非同期で接続(パイプやスレッド) - プロセスが落ちた場合は再起動
- エディタ終了時 or セッション切れ時に
kill
この部分は内部で goroutine や Node.js の child_process / subprocess モジュールなどで制御されています。
🧪 実装イメージ(疑似コード)
if (!githubMcpProcess) {
githubMcpProcess = spawn("./github-mcp-server", ["stdio"]);
setupStdIO(githubMcpProcess);
}
sendToStdin({ jsonrpc: "2.0", method: "resources/list", id: 1 });
🛠️ Windsurf や Cline の場合(例)
これらのツールは設定で どの MCP サーバを使うか を指定可能です:
{
"mcp": {
"tools": [
{
"command": "./github-mcp-server",
"args": ["stdio"],
"kind": "github"
}
]
}
}
この設定により、github
タイプのMCPメッセージが飛んできたときだけ対象サーバを stdio
モードで起動する、という仕組みになっています。
✅ まとめ
観点 | 内容 |
---|---|
起動タイミング | ユーザー操作またはセッション開始時に起動(on-demand) |
接続方式 | stdio 経由で JSON-RPC を読み書き |
MCPプロセス管理 | ツール側で起動・接続・再起動・終了処理を持つ |
対象ツール | Cursor / Cline / Windsurf などの AIエディタが対象 |

以下は、Model Context Protocol(MCP)仕様「Transports」(プロトコル改訂: 2024-11-05)の日本語要約です:
🚛 MCPのトランスポート方式(通信方式)
MCPでは、クライアントとサーバの通信手段として次の2つを標準定義しています:
stdio
(標準入出力を用いた通信)
1. - クライアントがMCPサーバをサブプロセスとして起動
- stdin にJSON-RPCメッセージを書き込み、stdoutでレスポンスを受け取る
- メッセージは改行区切り
-
stderr
はログ用途として任意で使用可能 - stdin/stdout にはMCPメッセージ以外を書き込んではならない
シーケンス図(抜粋)
Client起動 → サーバのstdinに書き込み → サーバはstdoutに返答
🔶 MCPクライアントは、可能であれば
stdio
をサポートすべき(SHOULD)
HTTP with SSE
(HTTP + Server-Sent Events)
2. -
サーバは独立プロセスとして複数のクライアントと接続可能
-
2つのエンドポイントを提供する必要がある:
- SSE用GETエンドポイント(クライアントが接続し、サーバからの通知を受け取る)
- HTTP POST用エンドポイント(クライアントがMCPメッセージを送信)
接続の流れ:
- クライアントが
/sse
に接続 - サーバが
endpoint
イベントを送り、POST先のURIを通知 - 以後、クライアントは通知された
/messages?sessionId=xxx
にPOST - サーバはレスポンスを
event: message
としてSSEで送信
シーケンス図(抜粋)
Client → GET /sse
Server → event: endpoint(POST先を通知)
Client → POST /messages
Server → SSE message
セキュリティ上の注意:
- DNSリバインディング攻撃対策として
Origin
ヘッダーの検証を 必須(MUST) - ローカル環境では
0.0.0.0
ではなく127.0.0.1
にバインドすべき(SHOULD) - 適切な認証も導入すべき(SHOULD)
Custom Transports
(独自の通信手段)
3. -
実装者は、要件に応じて独自の通信方式を実装してもよい(MAY)
-
条件:
- JSON-RPCフォーマットとMCPのメッセージライフサイクルを維持すること
- 接続方式やメッセージ交換方法をドキュメント化して互換性を保つこと
✅ 要点まとめ
項目 | 内容 |
---|---|
標準方式 |
stdio (LLMやCLI向け)、HTTP + SSE (Web向け) |
クライアントに推奨 |
stdio を優先(SHOULD) |
HTTP + SSE の構成 |
/sse → 接続、/messages → メッセージ送信 |
セキュリティ | Originチェック、localhostバインド、認証を推奨 |
カスタム対応 | 可能。JSON-RPC形式を守ることが条件 |

/sse
と /messages
通信のまとめ
✅ Model Context Protocol(MCP)における 1. 🎯 関係者の役割分担
構成要素 | 実体例 | 役割 |
---|---|---|
LLM(AIモデル) | Claude, GPT-4, Gemini など | ユーザーの指示を解釈し、「どのツールを使うべきか」を自然言語 or structured outputで出力 |
AIエージェント層 | CursorのAssistant 、WindsurfのAgent など |
LLM出力を受け取り、MCPメッセージを組み立て、HTTPリクエストを行うロジック層 |
クライアントアプリ(UI) | AIエディタ、CLIなどの実行アプリケーション |
/sse への接続、SSEの受信、/messages へのPOSTを制御 |
MCPサーバ | github-mcp-serverなど | MCPメッセージを受け取り、外部リソース(GitHub等)と連携して応答 |
mcp-stdio-proxy | 中継サーバ |
POST とSSE の橋渡し(HTTP ↔ 外部プロセスのstdin/stdout) |
2. 🔁 通信の全体フロー(ステップ順)
① セッション開始(/sse)
タイミング | 処理内容 |
---|---|
ユーザーが何らかの操作を行う | クライアント(AIエディタなど)が GET /sse を発行 |
mcp-stdio-proxyが応答 |
event: endpoint として /messages?sessionId=abc123 を返す |
クライアント | このセッションID付きの /messages を記憶し、今後のPOST先にする |
② ツール呼び出し判断とメッセージ送信(/messages)
タイミング | 処理内容 |
---|---|
ユーザーが自然言語で命令(例:「このPRのレビューを書いて」) | |
クライアントがLLMに問い合わせ | ClaudeやGPTが「resources/list を呼ぶべき」と推論(tool_use出力など) |
AIエージェントが判断 | 出力をJSON-RPC形式に変換し、POST /messages?sessionId=abc123 に送信 |
③ 結果の受信(SSE)
タイミング | 処理内容 |
---|---|
MCPサーバが処理結果を出力 |
mcp-stdio-proxy が stdout を読み取り、SSEで event: message を送信 |
クライアント | SSEで受信し、UIに反映(レビュー一覧・要約・編集案など) |
/sse
と /messages
の仕様的根拠(MCP公式)
3. 🔧 エンドポイント | 説明 |
---|---|
/sse |
MCP仕様により「セッション開始」「イベント受信」のための標準GETエンドポイント |
/messages |
MCP仕様により、endpoint イベントで通知されたセッション専用POST先 |
4. 🧠 補足:LLMと実行の役割の境界
処理 | 誰が行うか |
---|---|
ツールを使うべきかの判断 | LLM(Claude, GPTなど) |
具体的なエンドポイントの使用、リクエスト送信 | AIエージェント層(コード) |
セッション管理、SSE接続維持 | クライアントアプリ(UI) |
✅ 全体図(簡略化)
User
↓
LLM(Claudeなど)
↓ ↙ toolを使え
AIエージェント(Assistantなど)
↓ ↙ セッションID
GET /sse ← event: endpoint (/messages?sessionId=X)
↓
POST /messages?sessionId=X ← JSON-RPC形式で送信
↓
MCPサーバ(github-mcp-server)
↓
SSE: event: message
↓
クライアントUIに表示
✔️ まとめ:各ステップの対応表
ステップ | 誰が何をするか | 通信手段 |
---|---|---|
セッション開始 | クライアントが /sse に接続 |
HTTP GET(SSE) |
POST先の受信 | サーバが event: endpoint を送信 |
SSE |
メッセージ送信 | AIエージェントが /messages にPOST |
HTTP POST |
結果受信 | サーバが event: message を送信 |
SSE |

flusher.Flush()
は、HTTPレスポンスのバッファを強制的にクライアントに送信する処理です。
特に Server-Sent Events(SSE) のようなストリーミング通信で重要になります。
flusher.Flush()
の意味
✅ GoのHTTPレスポンスは通常バッファリングされるため、fmt.Fprint(w, ...)
などで w
に書いても、すぐにはクライアントに届きません。
そこで:
flusher, ok := w.(http.Flusher)
flusher.Flush()
とすることで:
- 現在までに
w.Write()
された内容を即時クライアントに送信する - 「強制送信」=「Flush(洗い流す)」という動作になる
🧠 なぜ必要か?(SSEでの必須処理)
Server-Sent Events(SSE)はクライアントと常時接続された1本のHTTPストリームです。
クライアントは、イベントが流れてくるたびに即座に受け取りたい。
ところがGoのHTTPサーバはデフォルトでレスポンスをバッファリングするため、Flush()
を明示的に呼ばないと 送信が遅延する or 届かない という問題になります。
🔁 使用例
fmt.Fprintf(w, "event: message\ndata: hello\n\n")
flusher.Flush() // ← ここで即送信
この組み合わせで、クライアント(ブラウザやエディタ側)は即座にSSEイベントを受け取れます。
✅ 補足:使う前に型チェックする理由
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusBadRequest)
return
}
これは、w
が http.Flusher
インタフェースを実装していない場合(≒Flushできないレスポンス)に備えるためです。
✅ まとめ
項目 | 内容 |
---|---|
何をする? |
http.ResponseWriter に書き込まれた内容をすぐ送信する |
どんな時使う? | SSEやチャンク転送など「逐次ストリーミング」が必要なとき |
なぜ必要? | GoはHTTPレスポンスをバッファリングするため、明示的にFlushしないと送信されない |
補足が必要であれば:
-
Flush()
が効かないケース(例:Content-Length
の影響) - WebSocketとの違い
- SSEでのブラウザ側の受信例
なども解説できます。ご希望ありますか?
/messagesのときw.Write() だけで Flush() していないのはなぜ?
➤ 答え:
Flush()
が必要なのは「ストリーミング通信(SSEなど)」のときだけです。
📦 通常の HTTP レスポンスの場合
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Accepted"))
このような処理では、**レスポンス全体が一度にまとめて送られる(バッファリングされた)**ため、Flush()
を明示的に呼ぶ必要はありません。
🔹 理由:
- 通常のリクエスト/レスポンス型通信では、GoのHTTPサーバはレスポンス全体が書き終わったと判断すると、自動的にクライアントに送信します。
-
Flush()
を使わなくてもw.Write()
の終了後にバッファは流れます。
Flush()
が必要になるケース
🌊 逆に 状況 | 必要 |
---|---|
SSE(Server-Sent Events) | ✅ 必要(都度送信したい) |
チャンク転送(Transfer-Encoding: chunked) | ✅ 必要(逐次部分送信) |
長時間レスポンスを保持する場合 | ✅ 必要(一定間隔で送らないとタイムアウト) |
通常のHTTPレスポンス(全体で返す) | ❌ 不要(Goが自動で送信) |
Flush()
が必要かどうかの判断
🧪 まとめ:処理 |
Flush() 必要? |
理由 |
---|---|---|
w.Write() + w.WriteHeader() で一括レスポンス |
❌ | 自動送信される |
fmt.Fprintf(w, "event: message") in /sse
|
✅ | 明示的にクライアントへ即送信する必要がある |
✅ まとめ
項目 | 説明 |
---|---|
w.Write(...) |
レスポンスにデータを書き込む(バッファされる) |
Flush() |
バッファを即座にクライアントに送信する |
Flush() が必要な場面 |
SSEやチャンク応答など、都度送信が必要なとき |
なぜ /messages では不要? |
普通のHTTP POSTレスポンスだから(レスポンス全体で一度に送る) |
もし Flush()
の裏側の動作(バッファの実装やチャンクの構造など)にも興味あれば、さらに深堀りできます。どうしますか?