mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(gateway): support Streamable HTTP MCP transport (GET/SSE + DELETE)
The MCP loopback server generates config with `"type": "http"` for Claude Code, but only handled POST requests. Claude Code's Streamable HTTP client sends GET to open an SSE notification channel before completing initialization. The 405 rejection on GET caused Claude Code to hang at "still connecting" indefinitely. - Accept GET /mcp with bearer auth, return text/event-stream (idle SSE) - Accept DELETE /mcp for session termination (spec compliance) - Add Mcp-Session-Id header to POST responses (spec requirement) - Update 405 Allow header to reflect supported methods
This commit is contained in:
@@ -139,13 +139,43 @@ export function validateMcpLoopbackRequest(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params.req.method === "GET") {
|
||||
const authHeader = getHeader(params.req, "authorization") ?? "";
|
||||
const ownerTokenMatched = safeEqualSecret(authHeader, `Bearer ${params.ownerToken}`);
|
||||
const nonOwnerTokenMatched = safeEqualSecret(authHeader, `Bearer ${params.nonOwnerToken}`);
|
||||
if (!ownerTokenMatched && !nonOwnerTokenMatched) {
|
||||
params.res.writeHead(401, { "Content-Type": "application/json" });
|
||||
params.res.end(JSON.stringify({ error: "unauthorized" }));
|
||||
return null;
|
||||
}
|
||||
logMcpLoopbackHttp("sse-open", { method: "GET", path: url.pathname });
|
||||
params.res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
});
|
||||
params.req.on("close", () => {
|
||||
if (!params.res.writableEnded) {
|
||||
params.res.end();
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params.req.method === "DELETE") {
|
||||
logMcpLoopbackHttp("session-delete", { method: "DELETE", path: url.pathname });
|
||||
params.res.writeHead(200);
|
||||
params.res.end();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (params.req.method !== "POST") {
|
||||
logMcpLoopbackHttp("reject", {
|
||||
reason: "method_not_allowed",
|
||||
method: params.req.method ?? "",
|
||||
path: url.pathname,
|
||||
});
|
||||
params.res.writeHead(405, { Allow: "POST" });
|
||||
params.res.writeHead(405, { Allow: "POST, GET, DELETE" });
|
||||
params.res.end();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ export async function startMcpLoopbackServer(port = 0): Promise<{
|
||||
}> {
|
||||
const ownerToken = crypto.randomBytes(32).toString("hex");
|
||||
const nonOwnerToken = crypto.randomBytes(32).toString("hex");
|
||||
const mcpSessionId = crypto.randomUUID();
|
||||
const toolCache = new McpLoopbackToolCache();
|
||||
|
||||
const httpServer = createHttpServer((req, res) => {
|
||||
@@ -226,7 +227,10 @@ export async function startMcpLoopbackServer(port = 0): Promise<{
|
||||
const payload = Array.isArray(parsed)
|
||||
? JSON.stringify(responses)
|
||||
: JSON.stringify(responses[0]);
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "application/json",
|
||||
"Mcp-Session-Id": mcpSessionId,
|
||||
});
|
||||
res.end(payload);
|
||||
} catch (error) {
|
||||
logWarn(`mcp loopback: request handling failed: ${formatErrorMessage(error)}`);
|
||||
|
||||
Reference in New Issue
Block a user