MCP SSE(Bun + Hono)實作筆記:Header 認證與 Inspector 測試
MCP SSE(Bun + Hono)實作筆記:Header 認證與 Inspector 測試
這篇文章記錄一個最小化 MCP SSE Server 的實作與除錯流程,包含:
- MCP 的 SSE 傳輸(
GET /sse+POST /messages) - 用 Header 做簡單的 API key / Bearer token 驗證
- 用官方 Inspector 連線測試(以及為什麼沒填
X-API-Key也可能連得上)
本文對應的程式在:
server.ts:SSE Serverclient.ts:SSE Client(Node/Bun 執行)README.md:快速操作指引
1. MCP SSE 傳輸流程是什麼?
這個範例使用「SSE 收訊息 + HTTP POST 傳訊息」的模式:
- Client 先
GET /sse建立長連線 - Server 在 SSE 連線建立後,先送一個
endpoint事件,裡面帶sessionId - Client 之後把所有 JSON-RPC 訊息
POST /messages?sessionId=...回來
你可以在 [server.ts](file:///home/iantp/GitHub/2601-mcp-test-07/server.ts#L49-L63) 看到 endpoint 事件是怎麼送出的:
await this._stream.writeSSE({
event: "endpoint",
data: relativeUrl,
});
relativeUrl 會長得像:
/messages?sessionId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
2. Server 端:為什麼需要 sessionId?
因為每個 SSE 連線代表一個「會話」,Server 需要把後續 POST /messages 的訊息送回正確的 Transport。
這個範例用一個 Map 來記住 sessionId 對應的 transport(以及當時的 auth token):
const transports = new Map<string, { transport: HonoSSEServerTransport; authToken: string | null }>();
當 Client POST /messages?sessionId=... 時,Server 會先用 sessionId 找到 transport,然後把 JSON-RPC message 丟進 transport:
entry.transport.handlePostMessage(body);
你可以在 [server.ts](file:///home/iantp/GitHub/2601-mcp-test-07/server.ts#L192-L219) 看到整段處理流程。
3. Header Authentication:驗證到底做了什麼?
這個 repo 的認證是「最小可用」的 header token 檢查:
- 透過環境變數設定 server 端的密鑰
MCP_API_KEY=...(建議:搭配X-API-Key)MCP_BEARER_TOKEN=...(相容:搭配Authorization: Bearer ...)
- 若 server 沒設定任何 token,就等於「不需要認證」
- 若有設定 token,就必須提供正確 token 才能連線
3.1 token 從哪裡取?
token 取值邏輯寫在 [extractAuthToken](file:///home/iantp/GitHub/2601-mcp-test-07/server.ts#L145-L161),順序是:
X-API-KeyAuthorization- 如果是
Bearer xxx取xxx - 否則直接把
Authorization的值當 token
- 如果是
?auth=...(query param 的備援做法)
程式碼(節錄):
const apiKey = c.req.header("x-api-key")?.trim();
if (apiKey) return apiKey;
const authHeader = c.req.header("authorization")?.trim();
if (authHeader) {
const match = authHeader.match(/^Bearer\s+(.+)$/i);
if (match?.[1]) return match[1].trim();
return authHeader;
}
return c.req.query("auth") ?? null;
3.2 為什麼你在 Inspector 沒填 X-API-Key 也能連上?
假設你用這樣啟動:
MCP_API_KEY=secret-123 bun run server
如果你在 Inspector 的 custom header 填:
Authorization: secret-123
因為程式允許「非 Bearer 的 Authorization 也當 token」,
所以 Authorization: secret-123 會被視為 token,剛好等於 MCP_API_KEY,因此驗證通過。
這也是為什麼比較建議用較明確的方式:
X-API-Key: secret-123
或:
Authorization: Bearer secret-123
4. Client 端:怎麼把 Header 帶進 SSE?
在 SSE 模式下比較麻煩的是:SSE 建立連線時不一定能直接「用純 header 配置」帶上 Header(不同 runtime/implementation 會有差異)。
此 repo 的 client.ts 是用 SDK 的 SSEClientTransport,並透過自訂 fetch 方式,確保:
- SSE 建連的 request 會帶上 header
- 後續的 POST request 也會帶上 header
對應程式在 [client.ts](file:///home/iantp/GitHub/2601-mcp-test-07/client.ts)。
5. 使用官方 Inspector 測試(SSE)
- 先啟動 server:
MCP_API_KEY=secret-123 bun run server
- 在另一個終端機啟動 Inspector:
npx @modelcontextprotocol/inspector
或(Bun):
bunx @modelcontextprotocol/inspector
- 在 Inspector UI 裡:
- Transport 選 SSE
- URL 輸入:
http://localhost:3000/sse
- 若 server 開啟了認證,請加上 header(建議二選一):
X-API-Key: secret-123Authorization: Bearer secret-123
- 連線成功後:
- list tools 應該會看到
echo_tool - 呼叫
echo_tool,參數message="Hello from Inspector!",預期回Server received via SSE: Hello from Inspector!
6. curl 觀察 SSE(快速確認 endpoint event)
curl -N -H 'X-API-Key: secret-123' http://127.0.0.1:3000/sse
成功時,你會看到 server 先送一個 endpoint 事件(包含 sessionId):
event: endpoint
data: /messages?sessionId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- ← Previous
HTTP Authorization Header 深入解析:為什麼一定要加 Bearer? - Next →
OAuth 2.0 測試實戰(Mock Provider + Inspector):用 Bearer Token 保護 MCP SSE