The body-shape gate rejected sentry.dev's 401 because its body is a spec-compliant RFC 6750 OAuth error envelope (`{error:"invalid_token", ...}`), not JSON-RPC. Sentry advertises full RFC 9728/8414 metadata via the `resource_metadata=` attribute of WWW-Authenticate, which is the MCP authorization spec's mandated signal. Accept `resource_metadata=` as sufficient for MCP+auth on 401, falling back to the JSON-RPC body check only for bare Bearer challenges (the cubic.dev / static-API-key path). Railway-style OAuth-protected non-MCP endpoints still get rejected because they don't include `resource_metadata=` and don't return JSON-RPC bodies.
Unified · Split
| @@ -95,6 +95,35 @@ describe("probeMcpEndpointShape", () => { · 94 unmodified lines | ||
| 95 | 95 | ), |
| 96 | 96 | ); |
| 97 | 97 | |
| 98 | + // mcp.sentry.dev/mcp/ shape: spec-compliant `resource_metadata=` | |
| 99 | + // attribute, body is RFC 6750 OAuth-shape (`{error: "invalid_token", | |
| 100 | + // ...}`), not JSON-RPC. The `resource_metadata=` attribute alone is | |
| 101 | + // enough to classify as MCP — the body-shape gate is for the bare-Bearer | |
| 102 | + // case where we have no other signal. | |
| 103 | + it.effect("classifies 401 with resource_metadata + OAuth error body as MCP+auth", () => | |
| 104 | + withServer( | |
| 105 | + () => | |
| 106 | + HttpServerResponse.jsonUnsafe( | |
| 107 | + { | |
| 108 | + error: "invalid_token", | |
| 109 | + error_description: "Missing or invalid access token", | |
| 110 | + }, | |
| 111 | + { | |
| 112 | + status: 401, | |
| 113 | + headers: { | |
| 114 | + "www-authenticate": | |
| 115 | + 'Bearer realm="OAuth", resource_metadata="https://mcp.example/.well-known/oauth-protected-resource/mcp/", error="invalid_token"', | |
| 116 | + }, | |
| 117 | + }, | |
| 118 | + ), | |
| 119 | + (endpoint) => | |
| 120 | + Effect.gen(function* () { | |
| 121 | + const result = yield* probeMcpEndpointShape(endpoint); | |
| 122 | + expect(result).toEqual({ kind: "mcp", requiresAuth: true }); | |
| 123 | + }), | |
| 124 | + ), | |
| 125 | + ); | |
| 126 | + | |
| 98 | 127 | // cubic.dev/api/mcp shape: bare `Bearer` challenge, no resource_metadata. |
| 99 | 128 | // The JSON-RPC error body is what tells us this is MCP rather than some |
| 100 | 129 | // other OAuth/API-key protected service. |
| @@ -191,15 +191,30 @@ export const probeMcpEndpointShape = ( · 190 unmodified lines | ||
| 191 | 191 | reason: "401 without Bearer WWW-Authenticate — not an MCP auth challenge", |
| 192 | 192 | } as const; |
| 193 | 193 | } |
| 194 | + // Spec-compliant MCP signal: the auth spec mandates a | |
| 195 | + // `resource_metadata=` attribute pointing at the server's | |
| 196 | + // RFC 9728 document. Real OAuth-protected MCP servers | |
| 197 | + // (sentry.dev, etc.) include it. This attribute is rare on | |
| 198 | + // unrelated OAuth services and is the cleanest accept signal | |
| 199 | + // we have when the 401 body is RFC 6750 OAuth-shape rather | |
| 200 | + // than JSON-RPC. | |
| 201 | + if (/(?:^|[\s,])resource_metadata\s*=/i.test(wwwAuth)) { | |
| 202 | + return { kind: "mcp", requiresAuth: true } as const; | |
| 203 | + } | |
| 194 | 204 | // SSE responses can't carry a JSON-RPC error envelope; accept the |
| 195 | 205 | // Bearer challenge alone in that case (rare but spec-permissible). |
| 196 | 206 | if (isSse) return { kind: "mcp", requiresAuth: true } as const; |
| 207 | + // Fallback for non-OAuth MCP servers that use static API | |
| 208 | + // keys and don't publish RFC 9728 metadata (cubic.dev). The | |
| 209 | + // JSON-RPC body shape is what separates them from | |
| 210 | + // OAuth-protected non-MCP services that also issue bare | |
| 211 | + // Bearer challenges (Railway-style GraphQL, etc.). | |
| 197 | 212 | const body = yield* readBody(response); |
| 198 | 213 | if (!isJsonRpcEnvelope(body)) { |
| 199 | 214 | return { |
| 200 | 215 | kind: "not-mcp", |
| 201 | 216 | category: "auth-required", |
| 202 | − reason: "401 + Bearer but body is not a JSON-RPC envelope", | |
| 217 | + reason: "401 + Bearer without resource_metadata or JSON-RPC body", | |
| 203 | 218 | } as const; |
| 204 | 219 | } |
| 205 | 220 | return { kind: "mcp", requiresAuth: true } as const; |