Showing the changes to apps (2 of 6 files). Show all
Unified · Split
| @@ -45,6 +45,25 @@ const jsonRpcResponse = ( · 44 unmodified lines | |||
| 45 | ? jsonRpcErrorBody(status, code, message) | 45 | ? jsonRpcErrorBody(status, code, message) |
| 46 | : jsonRpcErrorBody(status, code, message, { challenge }); | 46 | : jsonRpcErrorBody(status, code, message, { challenge }); |
| 47 | 47 | ||
| 48 | /** | ||
| 49 | * A dead session id answers by request method. POST/DELETE keep the 404 that | ||
| 50 | * tells a compliant client to re-initialize. A standalone GET gets 405: the | ||
| 51 | * v1 SDK treats that as "no SSE stream offered" and stops retrying quietly, | ||
| 52 | * which breaks the reconnect loops of pre-cutover always-on deployments — | ||
| 53 | * their GET-404 path never re-initialized, it just retried forever. | ||
| 54 | */ | ||
| 55 | const deadSessionResponse = (method: string, message: string): Response => | ||
| 56 | method === "GET" | ||
| 57 | ? new Response(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message }, id: null }), { | ||
| 58 | status: 405, | ||
| 59 | headers: { | ||
| 60 | "content-type": "application/json", | ||
| 61 | allow: "POST, DELETE", | ||
| 62 | "access-control-allow-origin": "*", | ||
| 63 | }, | ||
| 64 | }) | ||
| 65 | : jsonRpcResponse(404, -32001, message); | ||
| 66 | |||
| 48 | const renderAuthError = ( | 67 | const renderAuthError = ( |
| 49 | auth: McpAuthProvider["Service"], | 68 | auth: McpAuthProvider["Service"], |
| 50 | request: Request, | 69 | request: Request, |
| @@ -217,7 +236,7 @@ export const makeCloudMcpAgentHandler = () => { · 166 unmodified lines | |||
| 217 | 236 | ||
| 218 | const existingSession = sessionId ? mcpSessionStub(env.MCP_SESSION, sessionId) : null; | 237 | const existingSession = sessionId ? mcpSessionStub(env.MCP_SESSION, sessionId) : null; |
| 219 | if (sessionId && !existingSession) { | 238 | if (sessionId && !existingSession) { |
| 220 | return jsonRpcResponse(404, -32001, "Session not found"); | 239 | return deadSessionResponse(request.method, "Session not found"); |
| 221 | } | 240 | } |
| 222 | if (existingSession) { | 241 | if (existingSession) { |
| 223 | const owner = await existingSession.validateMcpSessionOwner({ | 242 | const owner = await existingSession.validateMcpSessionOwner({ |
| @@ -225,13 +244,13 @@ export const makeCloudMcpAgentHandler = () => { · 1 unmodified line | |||
| 225 | organizationId: outcome.principal.organizationId, | 244 | organizationId: outcome.principal.organizationId, |
| 226 | }); | 245 | }); |
| 227 | if (owner === "not_found") { | 246 | if (owner === "not_found") { |
| 228 | return jsonRpcResponse(404, -32001, "Session not found"); | 247 | return deadSessionResponse(request.method, "Session not found"); |
| 229 | } | 248 | } |
| 230 | if (owner === "terminated") { | 249 | if (owner === "terminated") { |
| 231 | // DELETE-condemned but the deferred destroy alarm hasn't wiped storage | 250 | // DELETE-condemned but the deferred destroy alarm hasn't wiped storage |
| 232 | // yet. Same envelope as the post-destroy race below: the client must | 251 | // yet. Same envelope as the post-destroy race below: the client must |
| 233 | // treat the id as dead and reconnect. | 252 | // treat the id as dead and reconnect. |
| 234 | return jsonRpcResponse(404, -32001, "Session timed out, please reconnect"); | 253 | return deadSessionResponse(request.method, "Session timed out, please reconnect"); |
| 235 | } | 254 | } |
| 236 | if (owner === "forbidden") { | 255 | if (owner === "forbidden") { |
| 237 | return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer"); | 256 | return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer"); |
| @@ -267,7 +286,7 @@ export const makeCloudMcpAgentHandler = () => { · 29 unmodified lines | |||
| 267 | // client to be told to reconnect, matching a timed-out session). | 286 | // client to be told to reconnect, matching a timed-out session). |
| 268 | // oxlint-disable-next-line executor/no-unknown-error-message -- adapter boundary: the abort reason is a plain runtime Error whose message IS the signal | 287 | // oxlint-disable-next-line executor/no-unknown-error-message -- adapter boundary: the abort reason is a plain runtime Error whose message IS the signal |
| 269 | if (Predicate.isError(error) && error.message === "destroyed") { | 288 | if (Predicate.isError(error) && error.message === "destroyed") { |
| 270 | return jsonRpcResponse(404, -32001, "Session timed out, please reconnect"); | 289 | return deadSessionResponse(request.method, "Session timed out, please reconnect"); |
| 271 | } | 290 | } |
| 272 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: rethrow anything that isn't the condemned-DO abort to the Workers runtime unchanged | 291 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: rethrow anything that isn't the condemned-DO abort to the Workers runtime unchanged |
| 273 | throw error; | 292 | throw error; |
| @@ -41,6 +41,25 @@ const jsonRpcResponse = ( · 40 unmodified lines | |||
| 41 | ? jsonRpcErrorBody(status, code, message) | 41 | ? jsonRpcErrorBody(status, code, message) |
| 42 | : jsonRpcErrorBody(status, code, message, { challenge }); | 42 | : jsonRpcErrorBody(status, code, message, { challenge }); |
| 43 | 43 | ||
| 44 | /** | ||
| 45 | * A dead session id answers by request method. POST/DELETE keep the 404 that | ||
| 46 | * tells a compliant client to re-initialize. A standalone GET gets 405: the | ||
| 47 | * v1 SDK treats that as "no SSE stream offered" and stops retrying quietly, | ||
| 48 | * which breaks the reconnect loops of pre-cutover always-on deployments — | ||
| 49 | * their GET-404 path never re-initialized, it just retried forever. | ||
| 50 | */ | ||
| 51 | const deadSessionResponse = (method: string, message: string): Response => | ||
| 52 | method === "GET" | ||
| 53 | ? new Response(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message }, id: null }), { | ||
| 54 | status: 405, | ||
| 55 | headers: { | ||
| 56 | "content-type": "application/json", | ||
| 57 | allow: "POST, DELETE", | ||
| 58 | "access-control-allow-origin": "*", | ||
| 59 | }, | ||
| 60 | }) | ||
| 61 | : jsonRpcResponse(404, -32001, message); | ||
| 62 | |||
| 44 | const renderAuthError = ( | 63 | const renderAuthError = ( |
| 45 | auth: McpAuthProvider["Service"], | 64 | auth: McpAuthProvider["Service"], |
| 46 | request: Request, | 65 | request: Request, |
| @@ -145,7 +164,7 @@ export const makeCloudflareMcpAgentHandler = (config: CloudflareConfig) => { · 98 unmodified lines | |||
| 145 | 164 | ||
| 146 | const existingSession = sessionId ? mcpSessionStub(env.MCP_SESSION, sessionId) : null; | 165 | const existingSession = sessionId ? mcpSessionStub(env.MCP_SESSION, sessionId) : null; |
| 147 | if (sessionId && !existingSession) { | 166 | if (sessionId && !existingSession) { |
| 148 | return jsonRpcResponse(404, -32001, "Session not found"); | 167 | return deadSessionResponse(request.method, "Session not found"); |
| 149 | } | 168 | } |
| 150 | if (existingSession) { | 169 | if (existingSession) { |
| 151 | const owner = await existingSession.validateMcpSessionOwner({ | 170 | const owner = await existingSession.validateMcpSessionOwner({ |
| @@ -153,12 +172,12 @@ export const makeCloudflareMcpAgentHandler = (config: CloudflareConfig) => { · 1 unmodified line | |||
| 153 | organizationId: outcome.principal.organizationId, | 172 | organizationId: outcome.principal.organizationId, |
| 154 | }); | 173 | }); |
| 155 | if (owner === "not_found") { | 174 | if (owner === "not_found") { |
| 156 | return jsonRpcResponse(404, -32001, "Session not found"); | 175 | return deadSessionResponse(request.method, "Session not found"); |
| 157 | } | 176 | } |
| 158 | if (owner === "terminated") { | 177 | if (owner === "terminated") { |
| 159 | // DELETE-condemned but the deferred destroy alarm hasn't wiped storage | 178 | // DELETE-condemned but the deferred destroy alarm hasn't wiped storage |
| 160 | // yet; the terminated id must read as dead immediately. | 179 | // yet; the terminated id must read as dead immediately. |
| 161 | return jsonRpcResponse(404, -32001, "Session timed out, please reconnect"); | 180 | return deadSessionResponse(request.method, "Session timed out, please reconnect"); |
| 162 | } | 181 | } |
| 163 | if (owner === "forbidden") { | 182 | if (owner === "forbidden") { |
| 164 | return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer"); | 183 | return jsonRpcResponse(403, -32003, "MCP session does not belong to the current bearer"); |