(
response: {
readonly status: number;
readonly headers: Readonly<Record<string, string>>;
readonly text: Effect.Effect<string, unknown>;
},
method: "GET" | "POST",
)
| 261 | ); |
| 262 | |
| 263 | const classify = ( |
| 264 | response: { |
| 265 | readonly status: number; |
| 266 | readonly headers: Readonly<Record<string, string>>; |
| 267 | readonly text: Effect.Effect<string, unknown>; |
| 268 | }, |
| 269 | method: "GET" | "POST", |
| 270 | ): Effect.Effect<McpShapeProbeResult | null> => |
| 271 | Effect.gen(function* () { |
| 272 | const contentType = readHeader(response.headers, "content-type") ?? ""; |
| 273 | const isSse = /^\s*text\/event-stream\b/i.test(contentType); |
| 274 | |
| 275 | if (response.status === 401) { |
| 276 | const wwwAuth = readHeader(response.headers, "www-authenticate"); |
| 277 | if (!wwwAuth || !/^\s*bearer\b/i.test(wwwAuth)) { |
| 278 | // Spec-non-compliant 401 (no `Bearer` challenge). Before |
| 279 | // giving up, check whether the server still publishes |
| 280 | // RFC 9728 protected-resource metadata for this path — |
| 281 | // some real MCP servers (Datadog) do exactly this. |
| 282 | if (yield* probeProtectedResourceMetadata(client, url, timeoutMs)) { |
| 283 | return { kind: "mcp", requiresAuth: true } as const; |
| 284 | } |
| 285 | return { |
| 286 | kind: "not-mcp", |
| 287 | category: "auth-required", |
| 288 | reason: "401 without Bearer WWW-Authenticate — not an MCP auth challenge", |
| 289 | } as const; |
| 290 | } |
| 291 | // Spec-compliant MCP signal: the auth spec mandates a |
| 292 | // `resource_metadata=` attribute pointing at the server's |
| 293 | // RFC 9728 document. Real OAuth-protected MCP servers |
| 294 | // (sentry.dev, etc.) include it. This attribute is rare on |
| 295 | // unrelated OAuth services and is the cleanest accept signal |
| 296 | // we have when the 401 body is RFC 6750 OAuth-shape rather |
| 297 | // than JSON-RPC. |
| 298 | if (/(?:^|[\s,])resource_metadata\s*=/i.test(wwwAuth)) { |
| 299 | return { kind: "mcp", requiresAuth: true } as const; |
| 300 | } |
| 301 | // Looser RFC 6750 §3.1 signal: the Bearer challenge carries |
| 302 | // `error=` / `error_description=` auth-params. Real MCP |
| 303 | // servers (Supabase, GitHub Copilot, Vercel, Neon, Tavily, |
| 304 | // Replicate, ...) include this even when they omit |
| 305 | // `resource_metadata=`. The body alone isn't enough for |
| 306 | // those — Supabase, e.g., returns `{"message":"Unauthorized"}` |
| 307 | // which is neither JSON-RPC nor RFC 6750. The `error=` |
| 308 | // auth-param is the tiebreaker. |
| 309 | if (/(?:^|[\s,])error\s*=/i.test(wwwAuth)) { |
| 310 | return { kind: "mcp", requiresAuth: true } as const; |
| 311 | } |
| 312 | // SSE responses can't carry a JSON-RPC error envelope; accept the |
| 313 | // Bearer challenge alone in that case (rare but spec-permissible). |
| 314 | if (isSse) return { kind: "mcp", requiresAuth: true } as const; |
| 315 | // Fallback for MCP servers whose 401 omits |
| 316 | // `resource_metadata=`. Two body shapes count: |
| 317 | // - JSON-RPC error (cubic.dev: API-key auth, JSON-RPC |
| 318 | // errors end-to-end). |
| 319 | // - RFC 6750 OAuth Bearer error envelope `{error: |
| 320 | // "invalid_token", ...}` without GraphQL `{errors:[...]}` |
no test coverage detected