Ian Chou's Blog

MCP SSE(Bun + Hono)實作筆記:Header 認證與 Inspector 測試

MCP SSE(Bun + Hono)實作筆記:Header 認證與 Inspector 測試

這篇文章記錄一個最小化 MCP SSE Server 的實作與除錯流程,包含:

本文對應的程式在:


1. MCP SSE 傳輸流程是什麼?

這個範例使用「SSE 收訊息 + HTTP POST 傳訊息」的模式:

  1. Client 先 GET /sse 建立長連線
  2. Server 在 SSE 連線建立後,先送一個 endpoint 事件,裡面帶 sessionId
  3. 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 檢查:

3.1 token 從哪裡取?

token 取值邏輯寫在 [extractAuthToken](file:///home/iantp/GitHub/2601-mcp-test-07/server.ts#L145-L161),順序是:

  1. X-API-Key
  2. Authorization
    • 如果是 Bearer xxxxxx
    • 否則直接把 Authorization 的值當 token
  3. ?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 方式,確保:

對應程式在 [client.ts](file:///home/iantp/GitHub/2601-mcp-test-07/client.ts)。


5. 使用官方 Inspector 測試(SSE)

  1. 先啟動 server:
MCP_API_KEY=secret-123 bun run server
  1. 在另一個終端機啟動 Inspector:
npx @modelcontextprotocol/inspector

或(Bun):

bunx @modelcontextprotocol/inspector
  1. 在 Inspector UI 裡:
  1. 若 server 開啟了認證,請加上 header(建議二選一):
  1. 連線成功後:

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