{"record":{"id":"87783941c7301b88","repo":"can1357/oh-my-pi","slug":"legacy-sse-endpoint-origin-mismatch-expected-co","errorCode":null,"errorMessage":"Legacy SSE endpoint origin mismatch: expected ${configuredUrl.origin}, received ${endpointUrl.origin}","messagePattern":"Legacy SSE endpoint origin mismatch: expected (.+?), received (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/transports/sse.ts","lineNumber":124,"sourceCode":"\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync #readSSEStream(\n\t\tbody: ReadableStream<Uint8Array>,\n\t\toperation: MCPTimeoutOperation,\n\t\tendpointReady: PromiseWithResolvers<void>,\n\t): Promise<void> {\n\t\tconst signal = operation.signal ?? getNeverAbortSignal();\n\t\tlet endpointReceived = false;\n\t\ttry {\n\t\t\tfor await (const event of readSseEvents(body, signal)) {\n\t\t\t\tif (event.event === \"endpoint\") {\n\t\t\t\t\tif (!this.#endpointUrl) {\n\t\t\t\t\t\tconst endpointUrl = new URL(event.data, this.#config.url);\n\t\t\t\t\t\tconst configuredUrl = new URL(this.#config.url);\n\t\t\t\t\t\tif (endpointUrl.origin !== configuredUrl.origin) {\n\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t`Legacy SSE endpoint origin mismatch: expected ${configuredUrl.origin}, received ${endpointUrl.origin}`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.#endpointUrl = endpointUrl.href;\n\t\t\t\t\t\tthis.#connected = true;\n\t\t\t\t\t\tendpointReceived = true;\n\t\t\t\t\t\toperation.clear();\n\t\t\t\t\t\tendpointReady.resolve();\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (event.data === \"\" || event.data === \"[DONE]\") continue;\n\n\t\t\t\tlet payload: unknown;\n\t\t\t\ttry {\n\t\t\t\t\tpayload = JSON.parse(event.data) as unknown;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof SyntaxError) {","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/transports/sse.ts#L106-L142","documentation":"The legacy SSE spec requires the server's first event to announce the POST endpoint, which may be a relative URL. As a security measure the transport resolves it against the configured URL and rejects any endpoint whose origin differs from the configured server's origin, preventing a compromised or misbehaving server from redirecting JSON-RPC POSTs (and any credentials) to a different host. The mismatch throws and fails connect().","triggerScenarios":"Server sends a 'endpoint' event containing an absolute URL on another host/port (e.g. http://localhost:3001/messages while the client connected via https://mcp.example.com/sse, or a different port); a proxy rewrites the endpoint event to its own address; a misconfigured server advertising an internal hostname.","commonSituations":"Docker/container setups where the server advertises its internal address (localhost:PORT inside the container) while the client reaches it via a mapped port or hostname; TLS-terminating proxies where the server emits an http:// endpoint while the client uses https://; copied example configs with mismatched hosts.","solutions":["Fix the server to announce a relative endpoint path (e.g. '/messages') in the endpoint event, as the spec recommends, so it always resolves to the same origin.","If the server cannot be changed, connect the client using the same origin the server advertises (same scheme, host, and port).","Update reverse-proxy configuration so it does not rewrite or inject absolute URLs into the SSE stream.","In containerized environments, align the advertised endpoint with the externally reachable address.","As a last resort for trusted same-machine servers, bind client and server to matching origins — do not bypass the check in the client."],"exampleFix":"// server (before): absolute internal URL leaks the container host\nres.write(`event: endpoint\\ndata: http://localhost:3001/messages\\n\\n`);\n// after: relative path resolves against the client's configured URL\nres.write(`event: endpoint\\ndata: /messages\\n\\n`);","handlingStrategy":"validation","validationCode":"// validate origins match before connecting\nconst configured = new URL(sseUrl);\nconst probe = await fetch(sseUrl, { headers: { Accept: 'text/event-stream' } });\n// read the first 'endpoint' event and check:\n// new URL(endpointData, sseUrl).origin === configured.origin\nawait probe.body?.cancel();","typeGuard":"function isSameOriginEndpoint(endpointData: string, configuredUrl: string): boolean {\n  try { return new URL(endpointData, configuredUrl).origin === new URL(configuredUrl).origin; }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  transport = await createSseTransport(config);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('origin mismatch')) {\n    logger.error('MCP server advertises a cross-origin endpoint; fix server to send a relative path');\n    throw new Error('Refusing cross-origin MCP endpoint (credential-leak risk)');\n  }\n  throw e;\n}","preventionTips":["Servers should announce relative endpoint paths per the legacy SSE spec.","Connect with the exact origin (scheme/host/port) the server advertises.","In containers, advertise externally reachable addresses, not internal ones.","Keep TLS-terminating proxies from rewriting endpoint events to absolute URLs.","Never weaken the client-side origin check for convenience."],"tags":["mcp","sse","security","origin-mismatch","legacy-transport"],"backgroundTag":"origin-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}