@stigmer/runner 3.1.6 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.js +23 -0
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/shared/mcp-oauth-detect.d.ts +53 -0
- package/dist/shared/mcp-oauth-detect.js +99 -0
- package/dist/shared/mcp-oauth-detect.js.map +1 -0
- package/package.json +2 -2
- package/src/activities/discover-mcp-server.ts +25 -0
- package/src/shared/__tests__/mcp-oauth-detect.test.ts +147 -0
- package/src/shared/mcp-oauth-detect.ts +112 -0
package/dist/.build-fingerprint
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"hash":"
|
|
1
|
+
{"hash":"4c57edff0c688075","builtAt":"2026-07-07T15:43:06.051Z","fileCount":244}
|
|
@@ -23,6 +23,7 @@ import { activityStarted, activityFinished } from "../idle-watchdog.js";
|
|
|
23
23
|
import { StigmerClient } from "../client/stigmer-client.js";
|
|
24
24
|
import { mcpServerToResolved } from "../shared/mcp-resolver.js";
|
|
25
25
|
import { toMcpClientConfig } from "../shared/mcp-manager.js";
|
|
26
|
+
import { detectOAuthChallenge } from "../shared/mcp-oauth-detect.js";
|
|
26
27
|
const SESSION_INIT_TIMEOUT_MS = 270_000;
|
|
27
28
|
const PLATFORM_INJECTABLE_MAP = {
|
|
28
29
|
STIGMER_SERVER_ADDRESS: "STIGMER_MCP_PUBLIC_ENDPOINT",
|
|
@@ -228,6 +229,16 @@ async function connectAndDiscover(slug, connectionConfig) {
|
|
|
228
229
|
}
|
|
229
230
|
});
|
|
230
231
|
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
// The MCP client surfaces a 401 OAuth challenge as an opaque aggregate
|
|
234
|
+
// ("unhandled errors in a TaskGroup"). For HTTP servers, re-probe once to
|
|
235
|
+
// see if the endpoint is actually asking for OAuth and, if so, replace the
|
|
236
|
+
// useless error with an actionable one. Non-OAuth failures rethrow as-is.
|
|
237
|
+
const oauthError = await classifyHttpOAuthFailure(slug, connectionConfig);
|
|
238
|
+
if (oauthError)
|
|
239
|
+
throw oauthError;
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
231
242
|
finally {
|
|
232
243
|
await client.close().catch((err) => {
|
|
233
244
|
console.warn(`[DiscoverMcpServer] Error closing MCP client for '${slug}': ` +
|
|
@@ -236,6 +247,18 @@ async function connectAndDiscover(slug, connectionConfig) {
|
|
|
236
247
|
}
|
|
237
248
|
return { tools, resourceTemplates };
|
|
238
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* If the server uses HTTP transport, probe its endpoint to classify a discovery
|
|
252
|
+
* failure as an OAuth challenge. Returns the actionable error to throw, or
|
|
253
|
+
* `null` for stdio servers and non-OAuth failures (caller rethrows original).
|
|
254
|
+
*/
|
|
255
|
+
async function classifyHttpOAuthFailure(slug, connectionConfig) {
|
|
256
|
+
const connection = connectionConfig[slug];
|
|
257
|
+
if (!connection || connection.transport !== "http" || !connection.url) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return detectOAuthChallenge(connection.url, connection.headers, slug);
|
|
261
|
+
}
|
|
239
262
|
async function withTimeout(ms, serverSlug, fn) {
|
|
240
263
|
return new Promise((resolve, reject) => {
|
|
241
264
|
const timer = setTimeout(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover-mcp-server.js","sourceRoot":"","sources":["../../src/activities/discover-mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"discover-mcp-server.js","sourceRoot":"","sources":["../../src/activities/discover-mcp-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth-challenge classification for HTTP MCP endpoints.
|
|
3
|
+
*
|
|
4
|
+
* When an HTTP MCP endpoint requires OAuth (per the MCP Authorization spec /
|
|
5
|
+
* RFC 9728), an unauthenticated or statically-tokened request is answered with
|
|
6
|
+
* `401` and a `WWW-Authenticate: Bearer ...` header pointing at OAuth protected
|
|
7
|
+
* resource metadata. The MCP client SDK surfaces this only as an opaque
|
|
8
|
+
* aggregate error ("unhandled errors in a TaskGroup"), which tells the user
|
|
9
|
+
* nothing.
|
|
10
|
+
*
|
|
11
|
+
* This module turns that opaque failure into a precise, actionable signal: it
|
|
12
|
+
* re-probes the endpoint once (only on the failure path, so the happy path pays
|
|
13
|
+
* nothing) and, if it sees an OAuth challenge, returns an {@link
|
|
14
|
+
* OAuthRequiredError} whose message tells the user to connect via OAuth instead
|
|
15
|
+
* of a manual token.
|
|
16
|
+
*
|
|
17
|
+
* Kept transport-library-agnostic (uses global `fetch`) so it is reusable by
|
|
18
|
+
* connect-time discovery, execution-time error enrichment, and any other caller
|
|
19
|
+
* that holds an MCP endpoint URL + headers.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Raised when an HTTP MCP endpoint answers with an OAuth authentication
|
|
23
|
+
* challenge. The message is self-contained and user-facing: it survives the
|
|
24
|
+
* Temporal boundary and is shown by the connect error wrappers verbatim.
|
|
25
|
+
*
|
|
26
|
+
* The literal phrase "requires OAuth" is a stable marker the Go/Java connect
|
|
27
|
+
* wrappers match to avoid appending a generic "check your credentials" suffix.
|
|
28
|
+
*/
|
|
29
|
+
export declare class OAuthRequiredError extends Error {
|
|
30
|
+
readonly serverSlug: string;
|
|
31
|
+
readonly resourceMetadataUrl?: string | undefined;
|
|
32
|
+
constructor(serverSlug: string, resourceMetadataUrl?: string | undefined);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a `WWW-Authenticate` header value is an OAuth challenge.
|
|
36
|
+
*
|
|
37
|
+
* The MCP auth spec emits `Bearer realm="OAuth", resource_metadata="..."`. We
|
|
38
|
+
* require the `Bearer` scheme plus either an OAuth realm or a resource-metadata
|
|
39
|
+
* pointer, so a plain `Bearer` 401 from a static-token API (an invalid key, not
|
|
40
|
+
* an OAuth requirement) is not misclassified.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isOAuthChallenge(wwwAuthenticate: string): boolean;
|
|
43
|
+
/** Extract the `resource_metadata` URL from a `WWW-Authenticate` value, if present. */
|
|
44
|
+
export declare function parseResourceMetadataUrl(wwwAuthenticate: string): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Probe an HTTP MCP endpoint once to decide whether its failure is an OAuth
|
|
47
|
+
* challenge. Returns an {@link OAuthRequiredError} to throw, or `null` when the
|
|
48
|
+
* endpoint is not asking for OAuth (so the caller rethrows the original error).
|
|
49
|
+
*
|
|
50
|
+
* Never throws: any probe/network failure returns `null` so this classification
|
|
51
|
+
* step can never mask or replace the original discovery error.
|
|
52
|
+
*/
|
|
53
|
+
export declare function detectOAuthChallenge(url: string, headers: Record<string, string> | undefined, slug: string): Promise<OAuthRequiredError | null>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth-challenge classification for HTTP MCP endpoints.
|
|
3
|
+
*
|
|
4
|
+
* When an HTTP MCP endpoint requires OAuth (per the MCP Authorization spec /
|
|
5
|
+
* RFC 9728), an unauthenticated or statically-tokened request is answered with
|
|
6
|
+
* `401` and a `WWW-Authenticate: Bearer ...` header pointing at OAuth protected
|
|
7
|
+
* resource metadata. The MCP client SDK surfaces this only as an opaque
|
|
8
|
+
* aggregate error ("unhandled errors in a TaskGroup"), which tells the user
|
|
9
|
+
* nothing.
|
|
10
|
+
*
|
|
11
|
+
* This module turns that opaque failure into a precise, actionable signal: it
|
|
12
|
+
* re-probes the endpoint once (only on the failure path, so the happy path pays
|
|
13
|
+
* nothing) and, if it sees an OAuth challenge, returns an {@link
|
|
14
|
+
* OAuthRequiredError} whose message tells the user to connect via OAuth instead
|
|
15
|
+
* of a manual token.
|
|
16
|
+
*
|
|
17
|
+
* Kept transport-library-agnostic (uses global `fetch`) so it is reusable by
|
|
18
|
+
* connect-time discovery, execution-time error enrichment, and any other caller
|
|
19
|
+
* that holds an MCP endpoint URL + headers.
|
|
20
|
+
*/
|
|
21
|
+
const OAUTH_PROBE_TIMEOUT_MS = 10_000;
|
|
22
|
+
/**
|
|
23
|
+
* Raised when an HTTP MCP endpoint answers with an OAuth authentication
|
|
24
|
+
* challenge. The message is self-contained and user-facing: it survives the
|
|
25
|
+
* Temporal boundary and is shown by the connect error wrappers verbatim.
|
|
26
|
+
*
|
|
27
|
+
* The literal phrase "requires OAuth" is a stable marker the Go/Java connect
|
|
28
|
+
* wrappers match to avoid appending a generic "check your credentials" suffix.
|
|
29
|
+
*/
|
|
30
|
+
export class OAuthRequiredError extends Error {
|
|
31
|
+
serverSlug;
|
|
32
|
+
resourceMetadataUrl;
|
|
33
|
+
constructor(serverSlug, resourceMetadataUrl) {
|
|
34
|
+
super(`MCP server '${serverSlug}' requires OAuth: its endpoint returned an ` +
|
|
35
|
+
`authentication challenge (HTTP 401). A manually-entered API token will ` +
|
|
36
|
+
`not work here — connect it with the OAuth "Sign in" flow instead.`);
|
|
37
|
+
this.serverSlug = serverSlug;
|
|
38
|
+
this.resourceMetadataUrl = resourceMetadataUrl;
|
|
39
|
+
this.name = "OAuthRequiredError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a `WWW-Authenticate` header value is an OAuth challenge.
|
|
44
|
+
*
|
|
45
|
+
* The MCP auth spec emits `Bearer realm="OAuth", resource_metadata="..."`. We
|
|
46
|
+
* require the `Bearer` scheme plus either an OAuth realm or a resource-metadata
|
|
47
|
+
* pointer, so a plain `Bearer` 401 from a static-token API (an invalid key, not
|
|
48
|
+
* an OAuth requirement) is not misclassified.
|
|
49
|
+
*/
|
|
50
|
+
export function isOAuthChallenge(wwwAuthenticate) {
|
|
51
|
+
const value = wwwAuthenticate.toLowerCase();
|
|
52
|
+
if (!value.includes("bearer"))
|
|
53
|
+
return false;
|
|
54
|
+
return value.includes("oauth") || value.includes("resource_metadata");
|
|
55
|
+
}
|
|
56
|
+
/** Extract the `resource_metadata` URL from a `WWW-Authenticate` value, if present. */
|
|
57
|
+
export function parseResourceMetadataUrl(wwwAuthenticate) {
|
|
58
|
+
const match = /resource_metadata="([^"]+)"/i.exec(wwwAuthenticate);
|
|
59
|
+
return match?.[1];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Probe an HTTP MCP endpoint once to decide whether its failure is an OAuth
|
|
63
|
+
* challenge. Returns an {@link OAuthRequiredError} to throw, or `null` when the
|
|
64
|
+
* endpoint is not asking for OAuth (so the caller rethrows the original error).
|
|
65
|
+
*
|
|
66
|
+
* Never throws: any probe/network failure returns `null` so this classification
|
|
67
|
+
* step can never mask or replace the original discovery error.
|
|
68
|
+
*/
|
|
69
|
+
export async function detectOAuthChallenge(url, headers, slug) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
Accept: "application/json, text/event-stream",
|
|
76
|
+
...(headers ?? {}),
|
|
77
|
+
},
|
|
78
|
+
// A minimal MCP initialize request — enough to trigger the endpoint's
|
|
79
|
+
// auth check without establishing a session.
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
id: 1,
|
|
83
|
+
method: "initialize",
|
|
84
|
+
params: {},
|
|
85
|
+
}),
|
|
86
|
+
signal: AbortSignal.timeout(OAUTH_PROBE_TIMEOUT_MS),
|
|
87
|
+
});
|
|
88
|
+
if (response.status !== 401)
|
|
89
|
+
return null;
|
|
90
|
+
const wwwAuthenticate = response.headers.get("www-authenticate") ?? "";
|
|
91
|
+
if (!isOAuthChallenge(wwwAuthenticate))
|
|
92
|
+
return null;
|
|
93
|
+
return new OAuthRequiredError(slug, parseResourceMetadataUrl(wwwAuthenticate));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=mcp-oauth-detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-oauth-detect.js","sourceRoot":"","sources":["../../src/shared/mcp-oauth-detect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEzB;IACA;IAFlB,YACkB,UAAkB,EAClB,mBAA4B;QAE5C,KAAK,CACH,eAAe,UAAU,6CAA6C;YACpE,yEAAyE;YACzE,mEAAmE,CACtE,CAAC;QAPc,eAAU,GAAV,UAAU,CAAQ;QAClB,wBAAmB,GAAnB,mBAAmB,CAAS;QAO5C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,eAAuB;IACtD,MAAM,KAAK,GAAG,eAAe,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC;AACxE,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,wBAAwB,CACtC,eAAuB;IAEvB,MAAM,KAAK,GAAG,8BAA8B,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACnE,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,GAAW,EACX,OAA2C,EAC3C,IAAY;IAEZ,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,MAAM,EAAE,qCAAqC;gBAC7C,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;aACnB;YACD,sEAAsE;YACtE,6CAA6C;YAC7C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,EAAE,EAAE,CAAC;gBACL,MAAM,EAAE,YAAY;gBACpB,MAAM,EAAE,EAAE;aACX,CAAC;YACF,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,sBAAsB,CAAC;SACpD,CAAC,CAAC;QAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAEzC,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,EAAE,CAAC;QACvE,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpD,OAAO,IAAI,kBAAkB,CAC3B,IAAI,EACJ,wBAAwB,CAAC,eAAe,CAAC,CAC1C,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/runner",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
4
4
|
"description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
86
86
|
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
|
87
87
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
88
|
-
"@stigmer/protos": "3.1.
|
|
88
|
+
"@stigmer/protos": "3.1.7",
|
|
89
89
|
"@temporalio/activity": "^1.11.0",
|
|
90
90
|
"@temporalio/client": "^1.11.0",
|
|
91
91
|
"@temporalio/common": "^1.11.0",
|
|
@@ -24,6 +24,7 @@ import { activityStarted, activityFinished } from "../idle-watchdog.js";
|
|
|
24
24
|
import { StigmerClient } from "../client/stigmer-client.js";
|
|
25
25
|
import { mcpServerToResolved } from "../shared/mcp-resolver.js";
|
|
26
26
|
import { toMcpClientConfig } from "../shared/mcp-manager.js";
|
|
27
|
+
import { detectOAuthChallenge } from "../shared/mcp-oauth-detect.js";
|
|
27
28
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
28
29
|
import type { Config } from "../config.js";
|
|
29
30
|
|
|
@@ -374,6 +375,14 @@ async function connectAndDiscover(
|
|
|
374
375
|
);
|
|
375
376
|
}
|
|
376
377
|
});
|
|
378
|
+
} catch (err) {
|
|
379
|
+
// The MCP client surfaces a 401 OAuth challenge as an opaque aggregate
|
|
380
|
+
// ("unhandled errors in a TaskGroup"). For HTTP servers, re-probe once to
|
|
381
|
+
// see if the endpoint is actually asking for OAuth and, if so, replace the
|
|
382
|
+
// useless error with an actionable one. Non-OAuth failures rethrow as-is.
|
|
383
|
+
const oauthError = await classifyHttpOAuthFailure(slug, connectionConfig);
|
|
384
|
+
if (oauthError) throw oauthError;
|
|
385
|
+
throw err;
|
|
377
386
|
} finally {
|
|
378
387
|
await client.close().catch((err: unknown) => {
|
|
379
388
|
console.warn(
|
|
@@ -386,6 +395,22 @@ async function connectAndDiscover(
|
|
|
386
395
|
return { tools, resourceTemplates };
|
|
387
396
|
}
|
|
388
397
|
|
|
398
|
+
/**
|
|
399
|
+
* If the server uses HTTP transport, probe its endpoint to classify a discovery
|
|
400
|
+
* failure as an OAuth challenge. Returns the actionable error to throw, or
|
|
401
|
+
* `null` for stdio servers and non-OAuth failures (caller rethrows original).
|
|
402
|
+
*/
|
|
403
|
+
async function classifyHttpOAuthFailure(
|
|
404
|
+
slug: string,
|
|
405
|
+
connectionConfig: ReturnType<typeof toMcpClientConfig>,
|
|
406
|
+
): Promise<Error | null> {
|
|
407
|
+
const connection = connectionConfig[slug];
|
|
408
|
+
if (!connection || connection.transport !== "http" || !connection.url) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return detectOAuthChallenge(connection.url, connection.headers, slug);
|
|
412
|
+
}
|
|
413
|
+
|
|
389
414
|
async function withTimeout<T>(
|
|
390
415
|
ms: number,
|
|
391
416
|
serverSlug: string,
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the HTTP MCP OAuth-challenge classifier.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `detectOAuthChallenge` recognizes a real MCP OAuth challenge
|
|
5
|
+
* (401 + `WWW-Authenticate: Bearer ...` per RFC 9728), and does NOT misfire on
|
|
6
|
+
* a plain invalid-token 401, a success, a non-OAuth error, or a network fault —
|
|
7
|
+
* so it can only ever turn an opaque failure into a clearer one, never mask a
|
|
8
|
+
* different cause.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
OAuthRequiredError,
|
|
14
|
+
detectOAuthChallenge,
|
|
15
|
+
isOAuthChallenge,
|
|
16
|
+
parseResourceMetadataUrl,
|
|
17
|
+
} from "../mcp-oauth-detect.js";
|
|
18
|
+
|
|
19
|
+
function mockFetchOnce(response: {
|
|
20
|
+
status: number;
|
|
21
|
+
wwwAuthenticate?: string;
|
|
22
|
+
}): void {
|
|
23
|
+
const headers = new Headers();
|
|
24
|
+
if (response.wwwAuthenticate !== undefined) {
|
|
25
|
+
headers.set("www-authenticate", response.wwwAuthenticate);
|
|
26
|
+
}
|
|
27
|
+
vi.stubGlobal(
|
|
28
|
+
"fetch",
|
|
29
|
+
vi.fn(async () =>
|
|
30
|
+
new Response(null, { status: response.status, headers }),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.unstubAllGlobals();
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("isOAuthChallenge", () => {
|
|
41
|
+
it("matches the MCP auth spec Bearer + OAuth realm challenge", () => {
|
|
42
|
+
expect(
|
|
43
|
+
isOAuthChallenge('Bearer realm="OAuth", resource_metadata="https://x/.well-known/oauth-protected-resource"'),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("matches a Bearer challenge that only advertises resource_metadata", () => {
|
|
48
|
+
expect(
|
|
49
|
+
isOAuthChallenge('Bearer resource_metadata="https://x/.well-known/oauth-protected-resource"'),
|
|
50
|
+
).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not match a plain Bearer 401 (invalid API key, not an OAuth requirement)", () => {
|
|
54
|
+
expect(isOAuthChallenge('Bearer error="invalid_token"')).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("does not match a non-Bearer scheme", () => {
|
|
58
|
+
expect(isOAuthChallenge('Basic realm="oauth"')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("is empty-safe", () => {
|
|
62
|
+
expect(isOAuthChallenge("")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("parseResourceMetadataUrl", () => {
|
|
67
|
+
it("extracts the resource_metadata URL", () => {
|
|
68
|
+
expect(
|
|
69
|
+
parseResourceMetadataUrl(
|
|
70
|
+
'Bearer realm="OAuth", resource_metadata="https://mcp.notion.com/.well-known/oauth-protected-resource/mcp"',
|
|
71
|
+
),
|
|
72
|
+
).toBe("https://mcp.notion.com/.well-known/oauth-protected-resource/mcp");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns undefined when absent", () => {
|
|
76
|
+
expect(parseResourceMetadataUrl('Bearer realm="OAuth"')).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("detectOAuthChallenge", () => {
|
|
81
|
+
it("returns an OAuthRequiredError for a 401 OAuth challenge", async () => {
|
|
82
|
+
mockFetchOnce({
|
|
83
|
+
status: 401,
|
|
84
|
+
wwwAuthenticate:
|
|
85
|
+
'Bearer realm="OAuth", resource_metadata="https://mcp.notion.com/.well-known/oauth-protected-resource/mcp"',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = await detectOAuthChallenge("https://mcp.notion.com/mcp", {}, "notion");
|
|
89
|
+
|
|
90
|
+
expect(result).toBeInstanceOf(OAuthRequiredError);
|
|
91
|
+
expect(result?.serverSlug).toBe("notion");
|
|
92
|
+
expect(result?.resourceMetadataUrl).toBe(
|
|
93
|
+
"https://mcp.notion.com/.well-known/oauth-protected-resource/mcp",
|
|
94
|
+
);
|
|
95
|
+
// The message is self-contained and carries the "requires OAuth" marker the
|
|
96
|
+
// connect wrappers key on.
|
|
97
|
+
expect(result?.message).toContain("requires OAuth");
|
|
98
|
+
expect(result?.message).toContain("notion");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns null for a 401 that is NOT an OAuth challenge (invalid static token)", async () => {
|
|
102
|
+
mockFetchOnce({ status: 401, wwwAuthenticate: 'Bearer error="invalid_token"' });
|
|
103
|
+
expect(
|
|
104
|
+
await detectOAuthChallenge("https://api.example.com/mcp", {}, "example"),
|
|
105
|
+
).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns null on a non-401 response", async () => {
|
|
109
|
+
mockFetchOnce({ status: 200 });
|
|
110
|
+
expect(
|
|
111
|
+
await detectOAuthChallenge("https://api.example.com/mcp", {}, "example"),
|
|
112
|
+
).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns null (never throws) when the probe itself fails", async () => {
|
|
116
|
+
vi.stubGlobal(
|
|
117
|
+
"fetch",
|
|
118
|
+
vi.fn(async () => {
|
|
119
|
+
throw new Error("network down");
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
expect(
|
|
123
|
+
await detectOAuthChallenge("https://api.example.com/mcp", {}, "example"),
|
|
124
|
+
).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("forwards the resolved headers to the probe request", async () => {
|
|
128
|
+
const fetchMock = vi.fn(
|
|
129
|
+
async (_url: string | URL, _init?: RequestInit) =>
|
|
130
|
+
new Response(null, { status: 200 }),
|
|
131
|
+
);
|
|
132
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
133
|
+
|
|
134
|
+
await detectOAuthChallenge(
|
|
135
|
+
"https://api.example.com/mcp",
|
|
136
|
+
{ Authorization: "Bearer stale-token" },
|
|
137
|
+
"example",
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
141
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
142
|
+
expect((init?.headers as Record<string, string>).Authorization).toBe(
|
|
143
|
+
"Bearer stale-token",
|
|
144
|
+
);
|
|
145
|
+
expect(init?.method).toBe("POST");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth-challenge classification for HTTP MCP endpoints.
|
|
3
|
+
*
|
|
4
|
+
* When an HTTP MCP endpoint requires OAuth (per the MCP Authorization spec /
|
|
5
|
+
* RFC 9728), an unauthenticated or statically-tokened request is answered with
|
|
6
|
+
* `401` and a `WWW-Authenticate: Bearer ...` header pointing at OAuth protected
|
|
7
|
+
* resource metadata. The MCP client SDK surfaces this only as an opaque
|
|
8
|
+
* aggregate error ("unhandled errors in a TaskGroup"), which tells the user
|
|
9
|
+
* nothing.
|
|
10
|
+
*
|
|
11
|
+
* This module turns that opaque failure into a precise, actionable signal: it
|
|
12
|
+
* re-probes the endpoint once (only on the failure path, so the happy path pays
|
|
13
|
+
* nothing) and, if it sees an OAuth challenge, returns an {@link
|
|
14
|
+
* OAuthRequiredError} whose message tells the user to connect via OAuth instead
|
|
15
|
+
* of a manual token.
|
|
16
|
+
*
|
|
17
|
+
* Kept transport-library-agnostic (uses global `fetch`) so it is reusable by
|
|
18
|
+
* connect-time discovery, execution-time error enrichment, and any other caller
|
|
19
|
+
* that holds an MCP endpoint URL + headers.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const OAUTH_PROBE_TIMEOUT_MS = 10_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Raised when an HTTP MCP endpoint answers with an OAuth authentication
|
|
26
|
+
* challenge. The message is self-contained and user-facing: it survives the
|
|
27
|
+
* Temporal boundary and is shown by the connect error wrappers verbatim.
|
|
28
|
+
*
|
|
29
|
+
* The literal phrase "requires OAuth" is a stable marker the Go/Java connect
|
|
30
|
+
* wrappers match to avoid appending a generic "check your credentials" suffix.
|
|
31
|
+
*/
|
|
32
|
+
export class OAuthRequiredError extends Error {
|
|
33
|
+
constructor(
|
|
34
|
+
public readonly serverSlug: string,
|
|
35
|
+
public readonly resourceMetadataUrl?: string,
|
|
36
|
+
) {
|
|
37
|
+
super(
|
|
38
|
+
`MCP server '${serverSlug}' requires OAuth: its endpoint returned an ` +
|
|
39
|
+
`authentication challenge (HTTP 401). A manually-entered API token will ` +
|
|
40
|
+
`not work here — connect it with the OAuth "Sign in" flow instead.`,
|
|
41
|
+
);
|
|
42
|
+
this.name = "OAuthRequiredError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a `WWW-Authenticate` header value is an OAuth challenge.
|
|
48
|
+
*
|
|
49
|
+
* The MCP auth spec emits `Bearer realm="OAuth", resource_metadata="..."`. We
|
|
50
|
+
* require the `Bearer` scheme plus either an OAuth realm or a resource-metadata
|
|
51
|
+
* pointer, so a plain `Bearer` 401 from a static-token API (an invalid key, not
|
|
52
|
+
* an OAuth requirement) is not misclassified.
|
|
53
|
+
*/
|
|
54
|
+
export function isOAuthChallenge(wwwAuthenticate: string): boolean {
|
|
55
|
+
const value = wwwAuthenticate.toLowerCase();
|
|
56
|
+
if (!value.includes("bearer")) return false;
|
|
57
|
+
return value.includes("oauth") || value.includes("resource_metadata");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Extract the `resource_metadata` URL from a `WWW-Authenticate` value, if present. */
|
|
61
|
+
export function parseResourceMetadataUrl(
|
|
62
|
+
wwwAuthenticate: string,
|
|
63
|
+
): string | undefined {
|
|
64
|
+
const match = /resource_metadata="([^"]+)"/i.exec(wwwAuthenticate);
|
|
65
|
+
return match?.[1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Probe an HTTP MCP endpoint once to decide whether its failure is an OAuth
|
|
70
|
+
* challenge. Returns an {@link OAuthRequiredError} to throw, or `null` when the
|
|
71
|
+
* endpoint is not asking for OAuth (so the caller rethrows the original error).
|
|
72
|
+
*
|
|
73
|
+
* Never throws: any probe/network failure returns `null` so this classification
|
|
74
|
+
* step can never mask or replace the original discovery error.
|
|
75
|
+
*/
|
|
76
|
+
export async function detectOAuthChallenge(
|
|
77
|
+
url: string,
|
|
78
|
+
headers: Record<string, string> | undefined,
|
|
79
|
+
slug: string,
|
|
80
|
+
): Promise<OAuthRequiredError | null> {
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Accept: "application/json, text/event-stream",
|
|
87
|
+
...(headers ?? {}),
|
|
88
|
+
},
|
|
89
|
+
// A minimal MCP initialize request — enough to trigger the endpoint's
|
|
90
|
+
// auth check without establishing a session.
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
jsonrpc: "2.0",
|
|
93
|
+
id: 1,
|
|
94
|
+
method: "initialize",
|
|
95
|
+
params: {},
|
|
96
|
+
}),
|
|
97
|
+
signal: AbortSignal.timeout(OAUTH_PROBE_TIMEOUT_MS),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (response.status !== 401) return null;
|
|
101
|
+
|
|
102
|
+
const wwwAuthenticate = response.headers.get("www-authenticate") ?? "";
|
|
103
|
+
if (!isOAuthChallenge(wwwAuthenticate)) return null;
|
|
104
|
+
|
|
105
|
+
return new OAuthRequiredError(
|
|
106
|
+
slug,
|
|
107
|
+
parseResourceMetadataUrl(wwwAuthenticate),
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|