@fractaal/pi-mcp 1.4.1 → 1.6.0

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +60 -5
  3. package/dist/config.d.ts +117 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +153 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/index.d.ts +5 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +5 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/input-schema.d.ts +7 -0
  12. package/dist/input-schema.d.ts.map +1 -0
  13. package/dist/input-schema.js +13 -0
  14. package/dist/input-schema.js.map +1 -0
  15. package/dist/naming.d.ts +18 -0
  16. package/dist/naming.d.ts.map +1 -0
  17. package/dist/naming.js +29 -0
  18. package/dist/naming.js.map +1 -0
  19. package/dist/oauth/auth-provider.d.ts +52 -0
  20. package/dist/oauth/auth-provider.d.ts.map +1 -0
  21. package/dist/oauth/auth-provider.js +97 -0
  22. package/dist/oauth/auth-provider.js.map +1 -0
  23. package/dist/oauth/callback.d.ts.map +1 -1
  24. package/dist/oauth/callback.js +5 -1
  25. package/dist/oauth/callback.js.map +1 -1
  26. package/dist/oauth/client-settings.d.ts +107 -0
  27. package/dist/oauth/client-settings.d.ts.map +1 -0
  28. package/dist/oauth/client-settings.js +114 -0
  29. package/dist/oauth/client-settings.js.map +1 -0
  30. package/dist/oauth/discovery.js +1 -1
  31. package/dist/oauth/discovery.js.map +1 -1
  32. package/dist/oauth/errors.d.ts +7 -0
  33. package/dist/oauth/errors.d.ts.map +1 -1
  34. package/dist/oauth/errors.js +10 -0
  35. package/dist/oauth/errors.js.map +1 -1
  36. package/dist/oauth/flow.d.ts +10 -0
  37. package/dist/oauth/flow.d.ts.map +1 -1
  38. package/dist/oauth/flow.js +31 -8
  39. package/dist/oauth/flow.js.map +1 -1
  40. package/dist/oauth/index.d.ts +5 -1
  41. package/dist/oauth/index.d.ts.map +1 -1
  42. package/dist/oauth/index.js +4 -1
  43. package/dist/oauth/index.js.map +1 -1
  44. package/dist/oauth/loopback.d.ts +51 -0
  45. package/dist/oauth/loopback.d.ts.map +1 -0
  46. package/dist/oauth/loopback.js +98 -0
  47. package/dist/oauth/loopback.js.map +1 -0
  48. package/dist/oauth/sign-in.d.ts +73 -0
  49. package/dist/oauth/sign-in.d.ts.map +1 -0
  50. package/dist/oauth/sign-in.js +182 -0
  51. package/dist/oauth/sign-in.js.map +1 -0
  52. package/dist/oauth/types.js +1 -1
  53. package/dist/oauth/types.js.map +1 -1
  54. package/dist/output-limit.d.ts +24 -0
  55. package/dist/output-limit.d.ts.map +1 -0
  56. package/dist/output-limit.js +58 -0
  57. package/dist/output-limit.js.map +1 -0
  58. package/dist/transports/lines.d.ts +34 -0
  59. package/dist/transports/lines.d.ts.map +1 -0
  60. package/dist/transports/lines.js +85 -0
  61. package/dist/transports/lines.js.map +1 -0
  62. package/dist/transports/pipe.d.ts +49 -0
  63. package/dist/transports/pipe.d.ts.map +1 -0
  64. package/dist/transports/pipe.js +96 -0
  65. package/dist/transports/pipe.js.map +1 -0
  66. package/dist/transports/stdio.d.ts +1 -4
  67. package/dist/transports/stdio.d.ts.map +1 -1
  68. package/dist/transports/stdio.js +20 -51
  69. package/dist/transports/stdio.js.map +1 -1
  70. package/dist/transports/streamable-http.d.ts.map +1 -1
  71. package/dist/transports/streamable-http.js +77 -12
  72. package/dist/transports/streamable-http.js.map +1 -1
  73. package/package.json +3 -4
  74. /package/{LICENSES → THIRD_PARTY_NOTICES}/modelcontextprotocol-typescript-sdk.txt +0 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The auth provider for connections to an OAuth-protected MCP server: it sends the stored access token
3
+ * and refreshes it, but never starts a browser flow. When the user has to sign in, it fails with
4
+ * `McpOAuthAuthorizationRequiredError` and the application runs `startMcpSignIn`.
5
+ */
6
+ import type { AuthProvider, McpFetch } from "../auth-provider.ts";
7
+ import { type McpOAuthClientOptions, type McpOAuthSettings } from "./client-settings.ts";
8
+ import { type OAuthFlowResult } from "./flow.ts";
9
+ import type { McpOAuthState } from "./provider.ts";
10
+ import type { OAuthChallenge } from "./types.ts";
11
+ /** An `AuthProvider` that can say when its refreshes have finished. */
12
+ export interface McpAuthProvider extends AuthProvider {
13
+ /** Resolves when no refresh is running, so shutdown does not drop rotated tokens before they are saved. */
14
+ settled(): Promise<void>;
15
+ }
16
+ /**
17
+ * Auth provider for MCP connections: sends the stored access token and refreshes it when it is about
18
+ * to expire or after a 401. Throws `McpOAuthAuthorizationRequiredError` when the user has to sign in,
19
+ * including when the server asks for more scope (`insufficient_scope`). `onChallenge` receives the
20
+ * server's `WWW-Authenticate` challenge so sign-in can use its resource metadata URL and scope.
21
+ * `settings` is only called when a refresh is needed, so a secret that fails to resolve fails the
22
+ * refresh instead of the whole connection setup.
23
+ *
24
+ * Many servers rotate refresh tokens, so two refreshes with the same refresh token lose the grant.
25
+ * Requests in this process share one refresh, and other processes are kept out by the store's
26
+ * refresh lock, held from reading the tokens to saving new ones. Tokens that changed meanwhile
27
+ * (another process refreshed them, or the user signed in) are used without refreshing.
28
+ */
29
+ export declare function createMcpAuthProvider(options: Omit<McpOAuthClientOptions, "settings"> & {
30
+ settings: () => McpOAuthSettings;
31
+ onChallenge: (challenge: OAuthChallenge) => void;
32
+ /**
33
+ * A redirect URI the application signs in with, such as its https callback route. Refreshing tokens
34
+ * from a sign-in with a Client ID Metadata Document needs a redirect URI the documents list; a
35
+ * refresh never redirects, so any listed one works. Default: the loopback redirect URI of `settings`.
36
+ */
37
+ redirectUrl?: string;
38
+ }): McpAuthProvider;
39
+ /**
40
+ * Refresh the tokens of `state` with the stored client (or the document it signed in with), or report
41
+ * `REDIRECT` when that is not possible. `options.redirectUrl` is a redirect URI the application signs in
42
+ * with, tried before the loopback ones of the settings and the stored client. Never registers a client or
43
+ * prepares an authorization request, so a pending sign-in stays usable. Call while holding the store's
44
+ * refresh lock. Not cancellable: a refresh the server answered may have rotated the refresh token, so its
45
+ * answer must be saved.
46
+ */
47
+ export declare function refreshStoredTokens(client: McpOAuthClientOptions, state: McpOAuthState, options?: {
48
+ fetch?: McpFetch;
49
+ challenge?: OAuthChallenge;
50
+ redirectUrl?: string;
51
+ }): Promise<OAuthFlowResult>;
52
+ //# sourceMappingURL=auth-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-provider.d.ts","sourceRoot":"","sources":["../../src/oauth/auth-provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAIN,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EAGrB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,uEAAuE;AACvE,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACpD,2GAA2G;IAC3G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAKD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAC,GAAG;IAClD,QAAQ,EAAE,MAAM,gBAAgB,CAAC;IACjC,WAAW,EAAE,CAAC,SAAS,EAAE,cAAc,KAAK,IAAI,CAAC;IACjD;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,GACC,eAAe,CA2CjB;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACxC,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,aAAa,EACpB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,QAAQ,CAAC;IAAC,SAAS,CAAC,EAAE,cAAc,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,OAAO,CAAC,eAAe,CAAC,CAiB1B"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The auth provider for connections to an OAuth-protected MCP server: it sends the stored access token
3
+ * and refreshes it, but never starts a browser flow. When the user has to sign in, it fails with
4
+ * `McpOAuthAuthorizationRequiredError` and the application runs `startMcpSignIn`.
5
+ */
6
+ import { createOAuthClientProvider, DEFAULT_REDIRECT_URL, loopbackSettings, registeredRedirectUrls, timedFetch, } from "./client-settings.js";
7
+ import { parseWwwAuthenticate } from "./discovery.js";
8
+ import { McpOAuthAuthorizationRequiredError } from "./errors.js";
9
+ import { authorizeMcp } from "./flow.js";
10
+ /** Access tokens this close to expiry are refreshed before they are sent. */
11
+ const REFRESH_SKEW_MS = 30_000;
12
+ /**
13
+ * Auth provider for MCP connections: sends the stored access token and refreshes it when it is about
14
+ * to expire or after a 401. Throws `McpOAuthAuthorizationRequiredError` when the user has to sign in,
15
+ * including when the server asks for more scope (`insufficient_scope`). `onChallenge` receives the
16
+ * server's `WWW-Authenticate` challenge so sign-in can use its resource metadata URL and scope.
17
+ * `settings` is only called when a refresh is needed, so a secret that fails to resolve fails the
18
+ * refresh instead of the whole connection setup.
19
+ *
20
+ * Many servers rotate refresh tokens, so two refreshes with the same refresh token lose the grant.
21
+ * Requests in this process share one refresh, and other processes are kept out by the store's
22
+ * refresh lock, held from reading the tokens to saving new ones. Tokens that changed meanwhile
23
+ * (another process refreshed them, or the user signed in) are used without refreshing.
24
+ */
25
+ export function createMcpAuthProvider(options) {
26
+ const { store, redirectUrl } = options;
27
+ let refreshing;
28
+ /** Replace `staleToken`, the access token that expired or was rejected. */
29
+ const refresh = (staleToken, fetch = globalThis.fetch, challenge) => {
30
+ refreshing ??= store
31
+ .withRefreshLock(async () => {
32
+ const state = await store.load();
33
+ if (state?.tokens?.access_token !== staleToken)
34
+ return;
35
+ if (!state?.tokens?.refresh_token)
36
+ throw new McpOAuthAuthorizationRequiredError();
37
+ const client = { ...options, settings: options.settings() };
38
+ const result = await refreshStoredTokens(client, state, { fetch, challenge, redirectUrl });
39
+ if (result === "REDIRECT")
40
+ throw new McpOAuthAuthorizationRequiredError();
41
+ })
42
+ .finally(() => {
43
+ refreshing = undefined;
44
+ });
45
+ return refreshing;
46
+ };
47
+ return {
48
+ token: async () => {
49
+ await refreshing?.catch(() => undefined);
50
+ const state = await store.load();
51
+ const token = state?.tokens?.access_token;
52
+ const expired = state?.tokensExpireAt !== undefined && state.tokensExpireAt - REFRESH_SKEW_MS <= Date.now();
53
+ if (!expired || !state?.tokens?.refresh_token)
54
+ return token;
55
+ // Failures fall through: the request goes out with the old token and a 401 decides what happens.
56
+ await refresh(token).catch(() => undefined);
57
+ return (await store.load())?.tokens?.access_token;
58
+ },
59
+ onUnauthorized: async (context) => {
60
+ const challenge = parseWwwAuthenticate(context.response.headers.get("www-authenticate"));
61
+ options.onChallenge(challenge);
62
+ // A refresh keeps the granted scope, so more scope needs a new sign-in.
63
+ if (challenge.error === "insufficient_scope")
64
+ throw new McpOAuthAuthorizationRequiredError();
65
+ await refresh(context.token, context.fetch, challenge);
66
+ },
67
+ settled: async () => {
68
+ await refreshing?.catch(() => undefined);
69
+ },
70
+ };
71
+ }
72
+ /**
73
+ * Refresh the tokens of `state` with the stored client (or the document it signed in with), or report
74
+ * `REDIRECT` when that is not possible. `options.redirectUrl` is a redirect URI the application signs in
75
+ * with, tried before the loopback ones of the settings and the stored client. Never registers a client or
76
+ * prepares an authorization request, so a pending sign-in stays usable. Call while holding the store's
77
+ * refresh lock. Not cancellable: a refresh the server answered may have rotated the refresh token, so its
78
+ * answer must be saved.
79
+ */
80
+ export async function refreshStoredTokens(client, state, options = {}) {
81
+ // Refreshing never redirects the user; the redirect URI only decides whether tokens without a stored
82
+ // client can be refreshed with a Client ID Metadata Document, so any URI the documents list works.
83
+ const candidates = [
84
+ options.redirectUrl,
85
+ loopbackSettings(client.settings).fixedRedirectUrl,
86
+ registeredRedirectUrls(state.clientInformation)[0],
87
+ ].filter((url) => url !== undefined);
88
+ const redirectUrl = candidates.find((url) => client.clientMetadataDocuments?.listsRedirectUrl(url)) ?? DEFAULT_REDIRECT_URL;
89
+ return authorizeMcp(createOAuthClientProvider(client, redirectUrl), {
90
+ serverUrl: client.serverUrl,
91
+ resourceMetadataUrl: options.challenge?.resourceMetadataUrl,
92
+ authorizationServerMetadataUrl: client.settings.authServerMetadataUrl,
93
+ fetch: timedFetch(options.fetch),
94
+ refreshOnly: true,
95
+ });
96
+ }
97
+ //# sourceMappingURL=auth-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-provider.js","sourceRoot":"","sources":["../../src/oauth/auth-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The auth provider for connections to an OAuth-protected MCP server: it sends the stored access token\n * and refreshes it, but never starts a browser flow. When the user has to sign in, it fails with\n * `McpOAuthAuthorizationRequiredError` and the application runs `startMcpSignIn`.\n */\n\nimport type { AuthProvider, McpFetch } from \"../auth-provider.ts\";\nimport {\n\tcreateOAuthClientProvider,\n\tDEFAULT_REDIRECT_URL,\n\tloopbackSettings,\n\ttype McpOAuthClientOptions,\n\ttype McpOAuthSettings,\n\tregisteredRedirectUrls,\n\ttimedFetch,\n} from \"./client-settings.ts\";\nimport { parseWwwAuthenticate } from \"./discovery.ts\";\nimport { McpOAuthAuthorizationRequiredError } from \"./errors.ts\";\nimport { authorizeMcp, type OAuthFlowResult } from \"./flow.ts\";\nimport type { McpOAuthState } from \"./provider.ts\";\nimport type { OAuthChallenge } from \"./types.ts\";\n\n/** An `AuthProvider` that can say when its refreshes have finished. */\nexport interface McpAuthProvider extends AuthProvider {\n\t/** Resolves when no refresh is running, so shutdown does not drop rotated tokens before they are saved. */\n\tsettled(): Promise<void>;\n}\n\n/** Access tokens this close to expiry are refreshed before they are sent. */\nconst REFRESH_SKEW_MS = 30_000;\n\n/**\n * Auth provider for MCP connections: sends the stored access token and refreshes it when it is about\n * to expire or after a 401. Throws `McpOAuthAuthorizationRequiredError` when the user has to sign in,\n * including when the server asks for more scope (`insufficient_scope`). `onChallenge` receives the\n * server's `WWW-Authenticate` challenge so sign-in can use its resource metadata URL and scope.\n * `settings` is only called when a refresh is needed, so a secret that fails to resolve fails the\n * refresh instead of the whole connection setup.\n *\n * Many servers rotate refresh tokens, so two refreshes with the same refresh token lose the grant.\n * Requests in this process share one refresh, and other processes are kept out by the store's\n * refresh lock, held from reading the tokens to saving new ones. Tokens that changed meanwhile\n * (another process refreshed them, or the user signed in) are used without refreshing.\n */\nexport function createMcpAuthProvider(\n\toptions: Omit<McpOAuthClientOptions, \"settings\"> & {\n\t\tsettings: () => McpOAuthSettings;\n\t\tonChallenge: (challenge: OAuthChallenge) => void;\n\t\t/**\n\t\t * A redirect URI the application signs in with, such as its https callback route. Refreshing tokens\n\t\t * from a sign-in with a Client ID Metadata Document needs a redirect URI the documents list; a\n\t\t * refresh never redirects, so any listed one works. Default: the loopback redirect URI of `settings`.\n\t\t */\n\t\tredirectUrl?: string;\n\t},\n): McpAuthProvider {\n\tconst { store, redirectUrl } = options;\n\tlet refreshing: Promise<void> | undefined;\n\n\t/** Replace `staleToken`, the access token that expired or was rejected. */\n\tconst refresh = (staleToken: string | undefined, fetch: McpFetch = globalThis.fetch, challenge?: OAuthChallenge) => {\n\t\trefreshing ??= store\n\t\t\t.withRefreshLock(async () => {\n\t\t\t\tconst state = await store.load();\n\t\t\t\tif (state?.tokens?.access_token !== staleToken) return;\n\t\t\t\tif (!state?.tokens?.refresh_token) throw new McpOAuthAuthorizationRequiredError();\n\t\t\t\tconst client = { ...options, settings: options.settings() };\n\t\t\t\tconst result = await refreshStoredTokens(client, state, { fetch, challenge, redirectUrl });\n\t\t\t\tif (result === \"REDIRECT\") throw new McpOAuthAuthorizationRequiredError();\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\trefreshing = undefined;\n\t\t\t});\n\t\treturn refreshing;\n\t};\n\n\treturn {\n\t\ttoken: async () => {\n\t\t\tawait refreshing?.catch(() => undefined);\n\t\t\tconst state = await store.load();\n\t\t\tconst token = state?.tokens?.access_token;\n\t\t\tconst expired = state?.tokensExpireAt !== undefined && state.tokensExpireAt - REFRESH_SKEW_MS <= Date.now();\n\t\t\tif (!expired || !state?.tokens?.refresh_token) return token;\n\t\t\t// Failures fall through: the request goes out with the old token and a 401 decides what happens.\n\t\t\tawait refresh(token).catch(() => undefined);\n\t\t\treturn (await store.load())?.tokens?.access_token;\n\t\t},\n\t\tonUnauthorized: async (context) => {\n\t\t\tconst challenge = parseWwwAuthenticate(context.response.headers.get(\"www-authenticate\"));\n\t\t\toptions.onChallenge(challenge);\n\t\t\t// A refresh keeps the granted scope, so more scope needs a new sign-in.\n\t\t\tif (challenge.error === \"insufficient_scope\") throw new McpOAuthAuthorizationRequiredError();\n\t\t\tawait refresh(context.token, context.fetch, challenge);\n\t\t},\n\t\tsettled: async () => {\n\t\t\tawait refreshing?.catch(() => undefined);\n\t\t},\n\t};\n}\n\n/**\n * Refresh the tokens of `state` with the stored client (or the document it signed in with), or report\n * `REDIRECT` when that is not possible. `options.redirectUrl` is a redirect URI the application signs in\n * with, tried before the loopback ones of the settings and the stored client. Never registers a client or\n * prepares an authorization request, so a pending sign-in stays usable. Call while holding the store's\n * refresh lock. Not cancellable: a refresh the server answered may have rotated the refresh token, so its\n * answer must be saved.\n */\nexport async function refreshStoredTokens(\n\tclient: McpOAuthClientOptions,\n\tstate: McpOAuthState,\n\toptions: { fetch?: McpFetch; challenge?: OAuthChallenge; redirectUrl?: string } = {},\n): Promise<OAuthFlowResult> {\n\t// Refreshing never redirects the user; the redirect URI only decides whether tokens without a stored\n\t// client can be refreshed with a Client ID Metadata Document, so any URI the documents list works.\n\tconst candidates = [\n\t\toptions.redirectUrl,\n\t\tloopbackSettings(client.settings).fixedRedirectUrl,\n\t\tregisteredRedirectUrls(state.clientInformation)[0],\n\t].filter((url) => url !== undefined);\n\tconst redirectUrl =\n\t\tcandidates.find((url) => client.clientMetadataDocuments?.listsRedirectUrl(url)) ?? DEFAULT_REDIRECT_URL;\n\treturn authorizeMcp(createOAuthClientProvider(client, redirectUrl), {\n\t\tserverUrl: client.serverUrl,\n\t\tresourceMetadataUrl: options.challenge?.resourceMetadataUrl,\n\t\tauthorizationServerMetadataUrl: client.settings.authServerMetadataUrl,\n\t\tfetch: timedFetch(options.fetch),\n\t\trefreshOnly: true,\n\t});\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,4DAA4D;AAC5D,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhG,MAAM,WAAW,0BAA0B;IAC1C,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,MAAM,CAAC;CACjD;AAOD,qBAAa,mBAAmB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAoD;IACtE,OAAO,CAAC,OAAO,CAQX;IAEJ,OAAO,eAYN;IAED,OAAa,MAAM,CAAC,OAAO,GAAE,0BAA+B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAuB1F;IAED;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CASpE;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAS3B;IAED,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,MAAM;CAwCd"}
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,4DAA4D;AAC5D,MAAM,MAAM,iBAAiB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhG,MAAM,WAAW,0BAA0B;IAC1C,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,MAAM,CAAC;CACjD;AAOD,qBAAa,mBAAmB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAoD;IACtE,OAAO,CAAC,OAAO,CAQX;IAEJ,OAAO,eAYN;IAED,OAAa,MAAM,CAAC,OAAO,GAAE,0BAA+B,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAuB1F;IAED;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CASpE;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAa3B;IAED,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,MAAM;CAwCd"}
@@ -58,9 +58,13 @@ export class OAuthCallbackServer {
58
58
  pending.reject(new Error("OAuth callback server closed"));
59
59
  }
60
60
  this.pending.clear();
61
- await new Promise((resolve, reject) => {
61
+ const closed = new Promise((resolve, reject) => {
62
62
  this.server.close((error) => (error ? reject(error) : resolve()));
63
63
  });
64
+ // `close()` waits for every connection, and a browser may keep an unused one open (a preconnect or a
65
+ // spare keep-alive socket) for minutes. Responses have been sent by now, so drop them.
66
+ this.server.closeAllConnections();
67
+ await closed;
64
68
  }
65
69
  reply(response, status, page) {
66
70
  if (this.renderPage) {
@@ -1 +1 @@
1
- {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"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","sourcesContent":["import { createServer, type Server, type ServerResponse } from \"node:http\";\n\nexport interface OAuthCallback {\n\tcode: string;\n\tstate: string;\n\tiss?: string;\n}\n\n/** Outcome shown on the browser page after the redirect. */\nexport type OAuthCallbackPage = { ok: true } | { ok: false; message: string; details?: string };\n\nexport interface OAuthCallbackServerOptions {\n\t/** Address to listen on. Default: `127.0.0.1`. */\n\thost?: string;\n\t/**\n\t * Host name in `redirectUrl`, for example `localhost` for a client registered with it while\n\t * listening on `127.0.0.1`. Default: `host`.\n\t */\n\tredirectHost?: string;\n\tport?: number;\n\tpath?: string;\n\t/** More paths that receive the callback, for example a server-specific path of a redirect URI. */\n\textraPaths?: string[];\n\ttimeoutMs?: number;\n\t/** Render the browser page as HTML. Default: a plain-text message. */\n\trenderPage?: (page: OAuthCallbackPage) => string;\n}\n\nfunction plainText(page: OAuthCallbackPage): string {\n\tif (page.ok) return \"Authorization complete. You may close this window.\";\n\treturn page.details ? `${page.message}\\n\\n${page.details}` : page.message;\n}\n\nexport class OAuthCallbackServer {\n\treadonly redirectUrl: string;\n\tprivate server: Server;\n\tprivate paths: string[];\n\tprivate timeoutMs: number;\n\tprivate renderPage: ((page: OAuthCallbackPage) => string) | undefined;\n\tprivate pending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tresolve: (callback: OAuthCallback) => void;\n\t\t\treject: (error: Error) => void;\n\t\t\ttimer: ReturnType<typeof setTimeout>;\n\t\t\tpath: string | undefined;\n\t\t}\n\t>();\n\n\tprivate constructor(\n\t\tserver: Server,\n\t\tredirectUrl: string,\n\t\tpaths: string[],\n\t\ttimeoutMs: number,\n\t\trenderPage: ((page: OAuthCallbackPage) => string) | undefined,\n\t) {\n\t\tthis.server = server;\n\t\tthis.redirectUrl = redirectUrl;\n\t\tthis.paths = paths;\n\t\tthis.timeoutMs = timeoutMs;\n\t\tthis.renderPage = renderPage;\n\t}\n\n\tstatic async listen(options: OAuthCallbackServerOptions = {}): Promise<OAuthCallbackServer> {\n\t\tconst host = options.host ?? \"127.0.0.1\";\n\t\tconst redirectHost = options.redirectHost ?? host;\n\t\tconst path = options.path ?? \"/callback\";\n\t\tlet instance: OAuthCallbackServer | undefined;\n\t\tconst server = createServer((request, response) => instance?.handle(request.url ?? \"/\", response));\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tserver.once(\"error\", reject);\n\t\t\tserver.listen(options.port ?? 0, host, () => {\n\t\t\t\tserver.off(\"error\", reject);\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\t\tconst address = server.address();\n\t\tif (!address || typeof address === \"string\") throw new Error(\"OAuth callback server did not bind to TCP\");\n\t\tinstance = new OAuthCallbackServer(\n\t\t\tserver,\n\t\t\t`http://${redirectHost.includes(\":\") ? `[${redirectHost}]` : redirectHost}:${address.port}${path}`,\n\t\t\t[path, ...(options.extraPaths ?? [])],\n\t\t\toptions.timeoutMs ?? 5 * 60_000,\n\t\t\toptions.renderPage,\n\t\t);\n\t\treturn instance;\n\t}\n\n\t/**\n\t * Wait for the authorization response with `state`. With `path`, a response on another path fails, so\n\t * a server-specific redirect URI can tell authorization servers apart (RFC 9700 section 4.4.2.2).\n\t */\n\twaitForCallback(state: string, path?: string): Promise<OAuthCallback> {\n\t\tif (this.pending.has(state)) throw new Error(\"OAuth state is already pending\");\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(state);\n\t\t\t\treject(new Error(\"OAuth callback timed out\"));\n\t\t\t}, this.timeoutMs);\n\t\t\tthis.pending.set(state, { resolve, reject, timer, path });\n\t\t});\n\t}\n\n\tasync close(): Promise<void> {\n\t\tfor (const pending of this.pending.values()) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tpending.reject(new Error(\"OAuth callback server closed\"));\n\t\t}\n\t\tthis.pending.clear();\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tthis.server.close((error) => (error ? reject(error) : resolve()));\n\t\t});\n\t}\n\n\tprivate reply(response: ServerResponse, status: number, page: OAuthCallbackPage): void {\n\t\tif (this.renderPage) {\n\t\t\tresponse.writeHead(status, { \"content-type\": \"text/html; charset=utf-8\", \"cache-control\": \"no-store\" });\n\t\t\tresponse.end(this.renderPage(page));\n\t\t} else {\n\t\t\tresponse.writeHead(status, { \"content-type\": \"text/plain; charset=utf-8\" }).end(plainText(page));\n\t\t}\n\t}\n\n\tprivate handle(rawUrl: string, response: ServerResponse): void {\n\t\tconst url = new URL(rawUrl, this.redirectUrl);\n\t\tif (!this.paths.includes(url.pathname)) {\n\t\t\tthis.reply(response, 404, { ok: false, message: \"Not found\" });\n\t\t\treturn;\n\t\t}\n\t\tconst state = url.searchParams.get(\"state\");\n\t\tconst pending = state ? this.pending.get(state) : undefined;\n\t\tif (!state || !pending) {\n\t\t\tthis.reply(response, 400, { ok: false, message: \"Invalid or expired OAuth state\" });\n\t\t\treturn;\n\t\t}\n\t\tclearTimeout(pending.timer);\n\t\tthis.pending.delete(state);\n\t\tif (pending.path !== undefined && url.pathname !== pending.path) {\n\t\t\tpending.reject(new Error(\"The authorization response arrived on another redirect URI\"));\n\t\t\tthis.reply(response, 400, { ok: false, message: \"Unexpected redirect URI\" });\n\t\t\treturn;\n\t\t}\n\t\tconst error = url.searchParams.get(\"error\");\n\t\tif (error) {\n\t\t\tconst description = url.searchParams.get(\"error_description\") ?? error;\n\t\t\tpending.reject(new Error(description));\n\t\t\tthis.reply(response, 200, {\n\t\t\t\tok: false,\n\t\t\t\tmessage: \"Authorization failed. You may close this window.\",\n\t\t\t\tdetails: description,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst code = url.searchParams.get(\"code\");\n\t\tif (!code) {\n\t\t\tpending.reject(new Error(\"OAuth callback did not include an authorization code\"));\n\t\t\tthis.reply(response, 400, { ok: false, message: \"Missing authorization code\" });\n\t\t\treturn;\n\t\t}\n\t\tconst iss = url.searchParams.get(\"iss\");\n\t\tpending.resolve({ code, state, ...(iss ? { iss } : {}) });\n\t\tthis.reply(response, 200, { ok: true });\n\t}\n}\n"]}
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"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","sourcesContent":["import { createServer, type Server, type ServerResponse } from \"node:http\";\n\nexport interface OAuthCallback {\n\tcode: string;\n\tstate: string;\n\tiss?: string;\n}\n\n/** Outcome shown on the browser page after the redirect. */\nexport type OAuthCallbackPage = { ok: true } | { ok: false; message: string; details?: string };\n\nexport interface OAuthCallbackServerOptions {\n\t/** Address to listen on. Default: `127.0.0.1`. */\n\thost?: string;\n\t/**\n\t * Host name in `redirectUrl`, for example `localhost` for a client registered with it while\n\t * listening on `127.0.0.1`. Default: `host`.\n\t */\n\tredirectHost?: string;\n\tport?: number;\n\tpath?: string;\n\t/** More paths that receive the callback, for example a server-specific path of a redirect URI. */\n\textraPaths?: string[];\n\ttimeoutMs?: number;\n\t/** Render the browser page as HTML. Default: a plain-text message. */\n\trenderPage?: (page: OAuthCallbackPage) => string;\n}\n\nfunction plainText(page: OAuthCallbackPage): string {\n\tif (page.ok) return \"Authorization complete. You may close this window.\";\n\treturn page.details ? `${page.message}\\n\\n${page.details}` : page.message;\n}\n\nexport class OAuthCallbackServer {\n\treadonly redirectUrl: string;\n\tprivate server: Server;\n\tprivate paths: string[];\n\tprivate timeoutMs: number;\n\tprivate renderPage: ((page: OAuthCallbackPage) => string) | undefined;\n\tprivate pending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tresolve: (callback: OAuthCallback) => void;\n\t\t\treject: (error: Error) => void;\n\t\t\ttimer: ReturnType<typeof setTimeout>;\n\t\t\tpath: string | undefined;\n\t\t}\n\t>();\n\n\tprivate constructor(\n\t\tserver: Server,\n\t\tredirectUrl: string,\n\t\tpaths: string[],\n\t\ttimeoutMs: number,\n\t\trenderPage: ((page: OAuthCallbackPage) => string) | undefined,\n\t) {\n\t\tthis.server = server;\n\t\tthis.redirectUrl = redirectUrl;\n\t\tthis.paths = paths;\n\t\tthis.timeoutMs = timeoutMs;\n\t\tthis.renderPage = renderPage;\n\t}\n\n\tstatic async listen(options: OAuthCallbackServerOptions = {}): Promise<OAuthCallbackServer> {\n\t\tconst host = options.host ?? \"127.0.0.1\";\n\t\tconst redirectHost = options.redirectHost ?? host;\n\t\tconst path = options.path ?? \"/callback\";\n\t\tlet instance: OAuthCallbackServer | undefined;\n\t\tconst server = createServer((request, response) => instance?.handle(request.url ?? \"/\", response));\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tserver.once(\"error\", reject);\n\t\t\tserver.listen(options.port ?? 0, host, () => {\n\t\t\t\tserver.off(\"error\", reject);\n\t\t\t\tresolve();\n\t\t\t});\n\t\t});\n\t\tconst address = server.address();\n\t\tif (!address || typeof address === \"string\") throw new Error(\"OAuth callback server did not bind to TCP\");\n\t\tinstance = new OAuthCallbackServer(\n\t\t\tserver,\n\t\t\t`http://${redirectHost.includes(\":\") ? `[${redirectHost}]` : redirectHost}:${address.port}${path}`,\n\t\t\t[path, ...(options.extraPaths ?? [])],\n\t\t\toptions.timeoutMs ?? 5 * 60_000,\n\t\t\toptions.renderPage,\n\t\t);\n\t\treturn instance;\n\t}\n\n\t/**\n\t * Wait for the authorization response with `state`. With `path`, a response on another path fails, so\n\t * a server-specific redirect URI can tell authorization servers apart (RFC 9700 section 4.4.2.2).\n\t */\n\twaitForCallback(state: string, path?: string): Promise<OAuthCallback> {\n\t\tif (this.pending.has(state)) throw new Error(\"OAuth state is already pending\");\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.pending.delete(state);\n\t\t\t\treject(new Error(\"OAuth callback timed out\"));\n\t\t\t}, this.timeoutMs);\n\t\t\tthis.pending.set(state, { resolve, reject, timer, path });\n\t\t});\n\t}\n\n\tasync close(): Promise<void> {\n\t\tfor (const pending of this.pending.values()) {\n\t\t\tclearTimeout(pending.timer);\n\t\t\tpending.reject(new Error(\"OAuth callback server closed\"));\n\t\t}\n\t\tthis.pending.clear();\n\t\tconst closed = new Promise<void>((resolve, reject) => {\n\t\t\tthis.server.close((error) => (error ? reject(error) : resolve()));\n\t\t});\n\t\t// `close()` waits for every connection, and a browser may keep an unused one open (a preconnect or a\n\t\t// spare keep-alive socket) for minutes. Responses have been sent by now, so drop them.\n\t\tthis.server.closeAllConnections();\n\t\tawait closed;\n\t}\n\n\tprivate reply(response: ServerResponse, status: number, page: OAuthCallbackPage): void {\n\t\tif (this.renderPage) {\n\t\t\tresponse.writeHead(status, { \"content-type\": \"text/html; charset=utf-8\", \"cache-control\": \"no-store\" });\n\t\t\tresponse.end(this.renderPage(page));\n\t\t} else {\n\t\t\tresponse.writeHead(status, { \"content-type\": \"text/plain; charset=utf-8\" }).end(plainText(page));\n\t\t}\n\t}\n\n\tprivate handle(rawUrl: string, response: ServerResponse): void {\n\t\tconst url = new URL(rawUrl, this.redirectUrl);\n\t\tif (!this.paths.includes(url.pathname)) {\n\t\t\tthis.reply(response, 404, { ok: false, message: \"Not found\" });\n\t\t\treturn;\n\t\t}\n\t\tconst state = url.searchParams.get(\"state\");\n\t\tconst pending = state ? this.pending.get(state) : undefined;\n\t\tif (!state || !pending) {\n\t\t\tthis.reply(response, 400, { ok: false, message: \"Invalid or expired OAuth state\" });\n\t\t\treturn;\n\t\t}\n\t\tclearTimeout(pending.timer);\n\t\tthis.pending.delete(state);\n\t\tif (pending.path !== undefined && url.pathname !== pending.path) {\n\t\t\tpending.reject(new Error(\"The authorization response arrived on another redirect URI\"));\n\t\t\tthis.reply(response, 400, { ok: false, message: \"Unexpected redirect URI\" });\n\t\t\treturn;\n\t\t}\n\t\tconst error = url.searchParams.get(\"error\");\n\t\tif (error) {\n\t\t\tconst description = url.searchParams.get(\"error_description\") ?? error;\n\t\t\tpending.reject(new Error(description));\n\t\t\tthis.reply(response, 200, {\n\t\t\t\tok: false,\n\t\t\t\tmessage: \"Authorization failed. You may close this window.\",\n\t\t\t\tdetails: description,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst code = url.searchParams.get(\"code\");\n\t\tif (!code) {\n\t\t\tpending.reject(new Error(\"OAuth callback did not include an authorization code\"));\n\t\t\tthis.reply(response, 400, { ok: false, message: \"Missing authorization code\" });\n\t\t\treturn;\n\t\t}\n\t\tconst iss = url.searchParams.get(\"iss\");\n\t\tpending.resolve({ code, state, ...(iss ? { iss } : {}) });\n\t\tthis.reply(response, 200, { ok: true });\n\t}\n}\n"]}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The OAuth client that sign-in and refreshes use for one MCP server: its settings, how it identifies
3
+ * itself (a pre-registered client, a Client ID Metadata Document, or dynamic client registration), and
4
+ * the `McpOAuthProvider` built from them.
5
+ */
6
+ import type { McpFetch } from "../auth-provider.ts";
7
+ import { McpOAuthProvider, type McpOAuthStateStore } from "./provider.ts";
8
+ import type { OAuthClientInformationMixed } from "./types.ts";
9
+ /** Client settings of one MCP server, usually from the application's configuration. */
10
+ export interface McpOAuthSettings {
11
+ /** Pre-registered client id. Without it, sign-in uses a Client ID Metadata Document or registers a client. */
12
+ clientId?: string;
13
+ /** Already resolved. */
14
+ clientSecret?: string;
15
+ /**
16
+ * Port of the loopback callback server, for clients registered with a fixed redirect URI. Without
17
+ * `callbackUrl`, the redirect URI is `http://127.0.0.1:<port>/callback`. Only `listenForLoopbackSignIn`
18
+ * and the redirect URI of refreshes use it; `startMcpSignIn` and `completeMcpSignIn` take theirs from
19
+ * the caller.
20
+ */
21
+ callbackPort?: number;
22
+ /**
23
+ * Loopback redirect URI registered for `clientId`, for example `http://localhost:8080/oauth/callback`.
24
+ * Without a port, the callback server listens on `callbackPort` or a free port, which is added to the
25
+ * URI (RFC 8252). Default: `http://127.0.0.1/callback`. Like `callbackPort`, only `listenForLoopbackSignIn`
26
+ * and refreshes use it.
27
+ */
28
+ callbackUrl?: string;
29
+ /** Scopes to request, separated by spaces. Default: the scopes the server advertises. */
30
+ scope?: string;
31
+ /** `client_name` for dynamic client registration. Default: the application's `clientName`. */
32
+ clientName?: string;
33
+ /**
34
+ * How to identify without `clientId`. `auto` (default): a Client ID Metadata Document when the
35
+ * authorization server supports them for public clients and the documents list the redirect URI,
36
+ * otherwise dynamic client registration; with `clientName`, always dynamic client registration.
37
+ * `cimd`: always the document, and sign-in fails without support. `dcr`: always dynamic client
38
+ * registration.
39
+ */
40
+ clientRegistration?: "auto" | "dcr" | "cimd";
41
+ /**
42
+ * Authorization server metadata document to use instead of discovery through the server, for servers
43
+ * that advertise a wrong authorization server or none. It is trusted as configured.
44
+ */
45
+ authServerMetadataUrl?: URL;
46
+ }
47
+ /**
48
+ * The Client ID Metadata Documents an application publishes: `<baseUrl>/client.json`, and for each MCP
49
+ * server `<baseUrl>/<callback ID>/client.json`, whose redirect URIs carry the path `<path>/<callback ID>`.
50
+ */
51
+ export interface McpClientMetadataDocuments {
52
+ /** For example `https://pi.dev/oauth`. */
53
+ baseUrl: string;
54
+ /** Whether the documents list `redirectUrl`. Loopback redirect URIs match on any port (RFC 8252). */
55
+ listsRedirectUrl(redirectUrl: string): boolean;
56
+ }
57
+ /** State store of one MCP server, with a lock that keeps other processes from refreshing at the same time. */
58
+ export interface McpOAuthServerStore extends McpOAuthStateStore {
59
+ /**
60
+ * Run `fn` while no other process refreshes the server's tokens. Not reentrant: `startMcpSignIn` and
61
+ * `createMcpAuthProvider` take it themselves, so never call them from `fn`. Throw
62
+ * `McpRefreshLockBusyError` when another process holds the lock for the whole wait: `startMcpSignIn`
63
+ * then goes to the browser step. Any other error fails the sign-in or refresh.
64
+ */
65
+ withRefreshLock<T>(fn: () => Promise<T>): Promise<T>;
66
+ }
67
+ /** The OAuth client of one MCP server: where its state lives, its settings, and the application's identity. */
68
+ export interface McpOAuthClientOptions {
69
+ serverUrl: string;
70
+ store: McpOAuthServerStore;
71
+ settings: McpOAuthSettings;
72
+ /** `client_name` for dynamic client registration unless `settings.clientName` names another: the application. */
73
+ clientName: string;
74
+ /** Omitted: dynamic client registration only. */
75
+ clientMetadataDocuments?: McpClientMetadataDocuments;
76
+ }
77
+ /** An `McpOAuthProvider` for the client, redirecting to `redirectUrl` or the document's variant of it. */
78
+ export declare function createOAuthClientProvider(options: McpOAuthClientOptions, redirectUrl: string, onRedirect?: (url: URL) => void): McpOAuthProvider;
79
+ /**
80
+ * Whether the client may identify with a Client ID Metadata Document instead of registering. `auto`
81
+ * registers when the user asked for a client name or the documents do not list the redirect URI.
82
+ */
83
+ export declare function offersClientMetadataDocument(settings: McpOAuthSettings, documents: McpClientMetadataDocuments | undefined, redirectUrl: string): boolean;
84
+ /** The path of the redirect URI of a server-specific Client ID Metadata Document: `<path>/<callback ID>`. */
85
+ export declare function serverSpecificPath(path: string, serverUrl: string): string;
86
+ /** Redirect URIs a stored client was registered with. */
87
+ export declare function registeredRedirectUrls(client: OAuthClientInformationMixed | undefined): string[];
88
+ /** `fetch` with `OAUTH_REQUEST_TIMEOUT_MS` per request, on top of the request's own signal. */
89
+ export declare function timedFetch(fetch?: McpFetch): McpFetch;
90
+ /** Redirect URI when the settings name none and no client is stored. */
91
+ export declare const DEFAULT_REDIRECT_URL = "http://127.0.0.1/callback";
92
+ /** Where the loopback callback server listens and the redirect URI it serves, from `callbackUrl` and `callbackPort`. */
93
+ export interface LoopbackSettings {
94
+ /** The redirect URI, without the port unless one is configured. */
95
+ url: string;
96
+ /** Address to listen on. */
97
+ host: string;
98
+ /** Host name in the redirect URI. */
99
+ redirectHost: string;
100
+ port: number | undefined;
101
+ path: string;
102
+ /** The exact redirect URI, when the port is fixed. */
103
+ fixedRedirectUrl: string | undefined;
104
+ }
105
+ /** The loopback callback settings of `settings`. */
106
+ export declare function loopbackSettings(settings: McpOAuthSettings): LoopbackSettings;
107
+ //# sourceMappingURL=client-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-settings.d.ts","sourceRoot":"","sources":["../../src/oauth/client-settings.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAA+B,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAE3F,uFAAuF;AACvF,MAAM,WAAW,gBAAgB;IAChC,8GAA8G;IAC9G,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;IAC7C;;;OAGG;IACH,qBAAqB,CAAC,EAAE,GAAG,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IAC1C,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,qGAAqG;IACrG,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;CAC/C;AAED,8GAA8G;AAC9G,MAAM,WAAW,mBAAoB,SAAQ,kBAAkB;IAC9D;;;;;OAKG;IACH,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACrD;AAED,+GAA+G;AAC/G,MAAM,WAAW,qBAAqB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,mBAAmB,CAAC;IAC3B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,iHAAiH;IACjH,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,uBAAuB,CAAC,EAAE,0BAA0B,CAAC;CACrD;AAED,0GAA0G;AAC1G,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,qBAAqB,EAC9B,WAAW,EAAE,MAAM,EACnB,UAAU,GAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAe,GACvC,gBAAgB,CA2BlB;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,0BAA0B,GAAG,SAAS,EACjD,WAAW,EAAE,MAAM,GACjB,OAAO,CAIT;AA0BD,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1E;AASD,yDAAyD;AACzD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,2BAA2B,GAAG,SAAS,GAAG,MAAM,EAAE,CAEhG;AAQD,+FAA+F;AAC/F,wBAAgB,UAAU,CAAC,KAAK,GAAE,QAA2B,GAAG,QAAQ,CAKvE;AAKD,wEAAwE;AACxE,eAAO,MAAM,oBAAoB,8BAA4C,CAAC;AAE9E,wHAAwH;AACxH,MAAM,WAAW,gBAAgB;IAChC,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;IACZ,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,oDAAoD;AACpD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,CAqB7E"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The OAuth client that sign-in and refreshes use for one MCP server: its settings, how it identifies
3
+ * itself (a pre-registered client, a Client ID Metadata Document, or dynamic client registration), and
4
+ * the `McpOAuthProvider` built from them.
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { McpOAuthProvider } from "./provider.js";
8
+ /** An `McpOAuthProvider` for the client, redirecting to `redirectUrl` or the document's variant of it. */
9
+ export function createOAuthClientProvider(options, redirectUrl, onRedirect = () => { }) {
10
+ const { serverUrl, settings, clientMetadataDocuments: documents } = options;
11
+ return new McpOAuthProvider({
12
+ serverUrl,
13
+ redirectUrl,
14
+ clientMetadata: { client_name: settings.clientName ?? options.clientName },
15
+ clientMetadataDocument: offersClientMetadataDocument(settings, documents, redirectUrl)
16
+ ? (metadata) => {
17
+ if (!documents?.listsRedirectUrl(redirectUrl)) {
18
+ throw new Error(`oauth.clientRegistration "cimd" needs Client ID Metadata Documents that list the redirect URI ${redirectUrl}`);
19
+ }
20
+ const document = clientMetadataDocument(serverUrl, redirectUrl, metadata, documents.baseUrl);
21
+ if (!document && settings.clientRegistration === "cimd") {
22
+ throw new Error('The authorization server does not support Client ID Metadata Documents for public clients; remove oauth.clientRegistration "cimd"');
23
+ }
24
+ return document;
25
+ }
26
+ : undefined,
27
+ clientId: settings.clientId,
28
+ clientSecret: settings.clientSecret,
29
+ store: options.store,
30
+ onRedirect,
31
+ });
32
+ }
33
+ /**
34
+ * Whether the client may identify with a Client ID Metadata Document instead of registering. `auto`
35
+ * registers when the user asked for a client name or the documents do not list the redirect URI.
36
+ */
37
+ export function offersClientMetadataDocument(settings, documents, redirectUrl) {
38
+ const registration = settings.clientRegistration ?? "auto";
39
+ if (registration !== "auto")
40
+ return registration === "cimd";
41
+ return !settings.clientId && !settings.clientName && documents?.listsRedirectUrl(redirectUrl) === true;
42
+ }
43
+ /**
44
+ * The Client ID Metadata Document to use, chosen like Codex chooses its own, or `undefined` when the
45
+ * authorization server does not support documents for public clients. Without the `iss` parameter in
46
+ * authorization responses (RFC 9207), the redirect URI and the document are specific to the MCP server,
47
+ * so a response cannot be mixed up with one from another authorization server (RFC 9700 section 4.4.2.2).
48
+ */
49
+ function clientMetadataDocument(serverUrl, redirectUrl, metadata, baseUrl) {
50
+ if (!metadata?.client_id_metadata_document_supported ||
51
+ !metadata.token_endpoint_auth_methods_supported?.includes("none")) {
52
+ return undefined;
53
+ }
54
+ if (metadata.authorization_response_iss_parameter_supported)
55
+ return { url: `${baseUrl}/client.json`, redirectUrl };
56
+ const redirect = new URL(redirectUrl);
57
+ redirect.pathname = serverSpecificPath(redirect.pathname, serverUrl);
58
+ return { url: `${baseUrl}/${callbackId(serverUrl)}/client.json`, redirectUrl: redirect.href };
59
+ }
60
+ /** The path of the redirect URI of a server-specific Client ID Metadata Document: `<path>/<callback ID>`. */
61
+ export function serverSpecificPath(path, serverUrl) {
62
+ return `${path.replace(/\/$/, "")}/${callbackId(serverUrl)}`;
63
+ }
64
+ /** 12 characters identifying an MCP server URL in callback paths, computed like Codex does. */
65
+ function callbackId(serverUrl) {
66
+ const url = new URL(serverUrl);
67
+ url.hash = "";
68
+ return createHash("sha256").update(url.href).digest().subarray(0, 9).toString("base64url");
69
+ }
70
+ /** Redirect URIs a stored client was registered with. */
71
+ export function registeredRedirectUrls(client) {
72
+ return client && "redirect_uris" in client ? client.redirect_uris : [];
73
+ }
74
+ /**
75
+ * Bounds each request to the authorization server, so an unresponsive one cannot hold the refresh lock,
76
+ * delay shutdown, or keep a sign-in waiting for long.
77
+ */
78
+ const OAUTH_REQUEST_TIMEOUT_MS = 15_000;
79
+ /** `fetch` with `OAUTH_REQUEST_TIMEOUT_MS` per request, on top of the request's own signal. */
80
+ export function timedFetch(fetch = globalThis.fetch) {
81
+ return (input, init) => {
82
+ const timeout = AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);
83
+ return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });
84
+ };
85
+ }
86
+ const CALLBACK_HOST = "127.0.0.1";
87
+ const CALLBACK_PATH = "/callback";
88
+ /** Redirect URI when the settings name none and no client is stored. */
89
+ export const DEFAULT_REDIRECT_URL = `http://${CALLBACK_HOST}${CALLBACK_PATH}`;
90
+ /** The loopback callback settings of `settings`. */
91
+ export function loopbackSettings(settings) {
92
+ const configured = settings.callbackUrl ?? DEFAULT_REDIRECT_URL;
93
+ const url = new URL(configured);
94
+ const address = url.hostname.replace(/^\[|\]$/g, "");
95
+ const port = url.port ? Number(url.port) : settings.callbackPort;
96
+ let fixedRedirectUrl;
97
+ // A configured URI with a port is sent exactly as written, since servers compare it as a string.
98
+ if (url.port)
99
+ fixedRedirectUrl = settings.callbackUrl;
100
+ else if (port !== undefined) {
101
+ url.port = String(port);
102
+ fixedRedirectUrl = url.href;
103
+ }
104
+ return {
105
+ url: configured,
106
+ // `localhost` is served on 127.0.0.1; browsers fall back to it when ::1 refuses.
107
+ host: address === "localhost" ? CALLBACK_HOST : address,
108
+ redirectHost: address,
109
+ port,
110
+ path: url.pathname,
111
+ fixedRedirectUrl,
112
+ };
113
+ }
114
+ //# sourceMappingURL=client-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-settings.js","sourceRoot":"","sources":["../../src/oauth/client-settings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The OAuth client that sign-in and refreshes use for one MCP server: its settings, how it identifies\n * itself (a pre-registered client, a Client ID Metadata Document, or dynamic client registration), and\n * the `McpOAuthProvider` built from them.\n */\n\nimport { createHash } from \"node:crypto\";\nimport type { McpFetch } from \"../auth-provider.ts\";\nimport type { OAuthClientMetadataDocument } from \"./flow.ts\";\nimport { McpOAuthProvider, type McpOAuthStateStore } from \"./provider.ts\";\nimport type { AuthorizationServerMetadata, OAuthClientInformationMixed } from \"./types.ts\";\n\n/** Client settings of one MCP server, usually from the application's configuration. */\nexport interface McpOAuthSettings {\n\t/** Pre-registered client id. Without it, sign-in uses a Client ID Metadata Document or registers a client. */\n\tclientId?: string;\n\t/** Already resolved. */\n\tclientSecret?: string;\n\t/**\n\t * Port of the loopback callback server, for clients registered with a fixed redirect URI. Without\n\t * `callbackUrl`, the redirect URI is `http://127.0.0.1:<port>/callback`. Only `listenForLoopbackSignIn`\n\t * and the redirect URI of refreshes use it; `startMcpSignIn` and `completeMcpSignIn` take theirs from\n\t * the caller.\n\t */\n\tcallbackPort?: number;\n\t/**\n\t * Loopback redirect URI registered for `clientId`, for example `http://localhost:8080/oauth/callback`.\n\t * Without a port, the callback server listens on `callbackPort` or a free port, which is added to the\n\t * URI (RFC 8252). Default: `http://127.0.0.1/callback`. Like `callbackPort`, only `listenForLoopbackSignIn`\n\t * and refreshes use it.\n\t */\n\tcallbackUrl?: string;\n\t/** Scopes to request, separated by spaces. Default: the scopes the server advertises. */\n\tscope?: string;\n\t/** `client_name` for dynamic client registration. Default: the application's `clientName`. */\n\tclientName?: string;\n\t/**\n\t * How to identify without `clientId`. `auto` (default): a Client ID Metadata Document when the\n\t * authorization server supports them for public clients and the documents list the redirect URI,\n\t * otherwise dynamic client registration; with `clientName`, always dynamic client registration.\n\t * `cimd`: always the document, and sign-in fails without support. `dcr`: always dynamic client\n\t * registration.\n\t */\n\tclientRegistration?: \"auto\" | \"dcr\" | \"cimd\";\n\t/**\n\t * Authorization server metadata document to use instead of discovery through the server, for servers\n\t * that advertise a wrong authorization server or none. It is trusted as configured.\n\t */\n\tauthServerMetadataUrl?: URL;\n}\n\n/**\n * The Client ID Metadata Documents an application publishes: `<baseUrl>/client.json`, and for each MCP\n * server `<baseUrl>/<callback ID>/client.json`, whose redirect URIs carry the path `<path>/<callback ID>`.\n */\nexport interface McpClientMetadataDocuments {\n\t/** For example `https://pi.dev/oauth`. */\n\tbaseUrl: string;\n\t/** Whether the documents list `redirectUrl`. Loopback redirect URIs match on any port (RFC 8252). */\n\tlistsRedirectUrl(redirectUrl: string): boolean;\n}\n\n/** State store of one MCP server, with a lock that keeps other processes from refreshing at the same time. */\nexport interface McpOAuthServerStore extends McpOAuthStateStore {\n\t/**\n\t * Run `fn` while no other process refreshes the server's tokens. Not reentrant: `startMcpSignIn` and\n\t * `createMcpAuthProvider` take it themselves, so never call them from `fn`. Throw\n\t * `McpRefreshLockBusyError` when another process holds the lock for the whole wait: `startMcpSignIn`\n\t * then goes to the browser step. Any other error fails the sign-in or refresh.\n\t */\n\twithRefreshLock<T>(fn: () => Promise<T>): Promise<T>;\n}\n\n/** The OAuth client of one MCP server: where its state lives, its settings, and the application's identity. */\nexport interface McpOAuthClientOptions {\n\tserverUrl: string;\n\tstore: McpOAuthServerStore;\n\tsettings: McpOAuthSettings;\n\t/** `client_name` for dynamic client registration unless `settings.clientName` names another: the application. */\n\tclientName: string;\n\t/** Omitted: dynamic client registration only. */\n\tclientMetadataDocuments?: McpClientMetadataDocuments;\n}\n\n/** An `McpOAuthProvider` for the client, redirecting to `redirectUrl` or the document's variant of it. */\nexport function createOAuthClientProvider(\n\toptions: McpOAuthClientOptions,\n\tredirectUrl: string,\n\tonRedirect: (url: URL) => void = () => {},\n): McpOAuthProvider {\n\tconst { serverUrl, settings, clientMetadataDocuments: documents } = options;\n\treturn new McpOAuthProvider({\n\t\tserverUrl,\n\t\tredirectUrl,\n\t\tclientMetadata: { client_name: settings.clientName ?? options.clientName },\n\t\tclientMetadataDocument: offersClientMetadataDocument(settings, documents, redirectUrl)\n\t\t\t? (metadata) => {\n\t\t\t\t\tif (!documents?.listsRedirectUrl(redirectUrl)) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`oauth.clientRegistration \"cimd\" needs Client ID Metadata Documents that list the redirect URI ${redirectUrl}`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tconst document = clientMetadataDocument(serverUrl, redirectUrl, metadata, documents.baseUrl);\n\t\t\t\t\tif (!document && settings.clientRegistration === \"cimd\") {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'The authorization server does not support Client ID Metadata Documents for public clients; remove oauth.clientRegistration \"cimd\"',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\treturn document;\n\t\t\t\t}\n\t\t\t: undefined,\n\t\tclientId: settings.clientId,\n\t\tclientSecret: settings.clientSecret,\n\t\tstore: options.store,\n\t\tonRedirect,\n\t});\n}\n\n/**\n * Whether the client may identify with a Client ID Metadata Document instead of registering. `auto`\n * registers when the user asked for a client name or the documents do not list the redirect URI.\n */\nexport function offersClientMetadataDocument(\n\tsettings: McpOAuthSettings,\n\tdocuments: McpClientMetadataDocuments | undefined,\n\tredirectUrl: string,\n): boolean {\n\tconst registration = settings.clientRegistration ?? \"auto\";\n\tif (registration !== \"auto\") return registration === \"cimd\";\n\treturn !settings.clientId && !settings.clientName && documents?.listsRedirectUrl(redirectUrl) === true;\n}\n\n/**\n * The Client ID Metadata Document to use, chosen like Codex chooses its own, or `undefined` when the\n * authorization server does not support documents for public clients. Without the `iss` parameter in\n * authorization responses (RFC 9207), the redirect URI and the document are specific to the MCP server,\n * so a response cannot be mixed up with one from another authorization server (RFC 9700 section 4.4.2.2).\n */\nfunction clientMetadataDocument(\n\tserverUrl: string,\n\tredirectUrl: string,\n\tmetadata: AuthorizationServerMetadata | undefined,\n\tbaseUrl: string,\n): OAuthClientMetadataDocument | undefined {\n\tif (\n\t\t!metadata?.client_id_metadata_document_supported ||\n\t\t!metadata.token_endpoint_auth_methods_supported?.includes(\"none\")\n\t) {\n\t\treturn undefined;\n\t}\n\tif (metadata.authorization_response_iss_parameter_supported) return { url: `${baseUrl}/client.json`, redirectUrl };\n\tconst redirect = new URL(redirectUrl);\n\tredirect.pathname = serverSpecificPath(redirect.pathname, serverUrl);\n\treturn { url: `${baseUrl}/${callbackId(serverUrl)}/client.json`, redirectUrl: redirect.href };\n}\n\n/** The path of the redirect URI of a server-specific Client ID Metadata Document: `<path>/<callback ID>`. */\nexport function serverSpecificPath(path: string, serverUrl: string): string {\n\treturn `${path.replace(/\\/$/, \"\")}/${callbackId(serverUrl)}`;\n}\n\n/** 12 characters identifying an MCP server URL in callback paths, computed like Codex does. */\nfunction callbackId(serverUrl: string): string {\n\tconst url = new URL(serverUrl);\n\turl.hash = \"\";\n\treturn createHash(\"sha256\").update(url.href).digest().subarray(0, 9).toString(\"base64url\");\n}\n\n/** Redirect URIs a stored client was registered with. */\nexport function registeredRedirectUrls(client: OAuthClientInformationMixed | undefined): string[] {\n\treturn client && \"redirect_uris\" in client ? client.redirect_uris : [];\n}\n\n/**\n * Bounds each request to the authorization server, so an unresponsive one cannot hold the refresh lock,\n * delay shutdown, or keep a sign-in waiting for long.\n */\nconst OAUTH_REQUEST_TIMEOUT_MS = 15_000;\n\n/** `fetch` with `OAUTH_REQUEST_TIMEOUT_MS` per request, on top of the request's own signal. */\nexport function timedFetch(fetch: McpFetch = globalThis.fetch): McpFetch {\n\treturn (input, init) => {\n\t\tconst timeout = AbortSignal.timeout(OAUTH_REQUEST_TIMEOUT_MS);\n\t\treturn fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout });\n\t};\n}\n\nconst CALLBACK_HOST = \"127.0.0.1\";\nconst CALLBACK_PATH = \"/callback\";\n\n/** Redirect URI when the settings name none and no client is stored. */\nexport const DEFAULT_REDIRECT_URL = `http://${CALLBACK_HOST}${CALLBACK_PATH}`;\n\n/** Where the loopback callback server listens and the redirect URI it serves, from `callbackUrl` and `callbackPort`. */\nexport interface LoopbackSettings {\n\t/** The redirect URI, without the port unless one is configured. */\n\turl: string;\n\t/** Address to listen on. */\n\thost: string;\n\t/** Host name in the redirect URI. */\n\tredirectHost: string;\n\tport: number | undefined;\n\tpath: string;\n\t/** The exact redirect URI, when the port is fixed. */\n\tfixedRedirectUrl: string | undefined;\n}\n\n/** The loopback callback settings of `settings`. */\nexport function loopbackSettings(settings: McpOAuthSettings): LoopbackSettings {\n\tconst configured = settings.callbackUrl ?? DEFAULT_REDIRECT_URL;\n\tconst url = new URL(configured);\n\tconst address = url.hostname.replace(/^\\[|\\]$/g, \"\");\n\tconst port = url.port ? Number(url.port) : settings.callbackPort;\n\tlet fixedRedirectUrl: string | undefined;\n\t// A configured URI with a port is sent exactly as written, since servers compare it as a string.\n\tif (url.port) fixedRedirectUrl = settings.callbackUrl;\n\telse if (port !== undefined) {\n\t\turl.port = String(port);\n\t\tfixedRedirectUrl = url.href;\n\t}\n\treturn {\n\t\turl: configured,\n\t\t// `localhost` is served on 127.0.0.1; browsers fall back to it when ::1 refuses.\n\t\thost: address === \"localhost\" ? CALLBACK_HOST : address,\n\t\tredirectHost: address,\n\t\tport,\n\t\tpath: url.pathname,\n\t\tfixedRedirectUrl,\n\t};\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Adapted from modelcontextprotocol/typescript-sdk v1.29.0 src/client/auth.ts.
3
- * Copyright (c) 2024 Anthropic, PBC. Licensed under MIT; see LICENSES/.
3
+ * Copyright (c) 2024 Anthropic, PBC. Licensed under MIT; see THIRD_PARTY_NOTICES/.
4
4
  * Modified to remove Zod/CORS shims and enforce authorization-server issuer validation.
5
5
  */
6
6
  import { LATEST_PROTOCOL_VERSION } from "../protocol/types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/oauth/discovery.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Adapted from modelcontextprotocol/typescript-sdk v1.29.0 src/client/auth.ts.\n * Copyright (c) 2024 Anthropic, PBC. Licensed under MIT; see LICENSES/.\n * Modified to remove Zod/CORS shims and enforce authorization-server issuer validation.\n */\n\nimport type { McpFetch } from \"../auth-provider.ts\";\nimport { LATEST_PROTOCOL_VERSION } from \"../protocol/types.ts\";\nimport { OAuthIssuerMismatchError } from \"./errors.ts\";\nimport {\n\ttype AuthorizationServerMetadata,\n\ttype OAuthChallenge,\n\ttype OAuthProtectedResourceMetadata,\n\ttype OAuthServerInfo,\n\tparseAuthorizationServerMetadata,\n\tparseProtectedResourceMetadata,\n} from \"./types.ts\";\n\nfunction discard(response: Response | undefined): void {\n\tvoid response?.body?.cancel().catch(() => {});\n}\n\n/** 4xx and 502 mean \"not here\", so discovery tries the next candidate URL. */\nfunction isDiscoveryMiss(status: number): boolean {\n\treturn (status >= 400 && status < 500) || status === 502;\n}\n\n/** Path suffix for `/.well-known/<kind><path>`; empty for the root path. */\nfunction pathSuffix(pathname: string): string {\n\treturn pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n}\n\nfunction field(header: string, name: string): string | undefined {\n\tconst match = header.match(new RegExp(String.raw`(?:^|[,\\s])${name}=(?:\"([^\"]*)\"|([^\\s,]+))`, \"i\"));\n\t// An empty value (`scope=\"\"`) carries no information, so it counts as absent.\n\treturn match?.[1] || match?.[2] || undefined;\n}\n\nexport function parseWwwAuthenticate(header: string | null): OAuthChallenge {\n\tif (!header) return {};\n\tconst scheme = header.trimStart().split(/\\s+/, 1)[0]?.toLowerCase();\n\tif (scheme !== \"bearer\" && scheme !== \"dpop\") return {};\n\tconst resourceMetadata = field(header, \"resource_metadata\");\n\tlet resourceMetadataUrl: URL | undefined;\n\tif (resourceMetadata) {\n\t\ttry {\n\t\t\tresourceMetadataUrl = new URL(resourceMetadata);\n\t\t} catch {}\n\t}\n\treturn {\n\t\tresourceMetadataUrl,\n\t\tscope: field(header, \"scope\"),\n\t\terror: field(header, \"error\"),\n\t\terrorDescription: field(header, \"error_description\"),\n\t};\n}\n\nasync function fetchMetadata(\n\turl: URL,\n\tfetch: McpFetch,\n\tprotocolVersion: string,\n\tsignal: AbortSignal | undefined,\n): Promise<Response> {\n\treturn fetch(url, {\n\t\theaders: { Accept: \"application/json\", \"MCP-Protocol-Version\": protocolVersion },\n\t\tsignal,\n\t});\n}\n\nexport async function discoverProtectedResourceMetadata(\n\tserverUrl: string | URL,\n\toptions: {\n\t\tresourceMetadataUrl?: string | URL;\n\t\tprotocolVersion?: string;\n\t\tfetch?: McpFetch;\n\t\tsignal?: AbortSignal;\n\t} = {},\n): Promise<OAuthProtectedResourceMetadata> {\n\tconst server = new URL(serverUrl);\n\tconst fetch = options.fetch ?? globalThis.fetch;\n\tconst version = options.protocolVersion ?? LATEST_PROTOCOL_VERSION;\n\tlet response = await fetchMetadata(\n\t\toptions.resourceMetadataUrl\n\t\t\t? new URL(options.resourceMetadataUrl)\n\t\t\t: new URL(`/.well-known/oauth-protected-resource${pathSuffix(server.pathname)}`, server.origin),\n\t\tfetch,\n\t\tversion,\n\t\toptions.signal,\n\t);\n\tif (!options.resourceMetadataUrl && server.pathname !== \"/\" && isDiscoveryMiss(response.status)) {\n\t\tdiscard(response);\n\t\tresponse = await fetchMetadata(\n\t\t\tnew URL(\"/.well-known/oauth-protected-resource\", server.origin),\n\t\t\tfetch,\n\t\t\tversion,\n\t\t\toptions.signal,\n\t\t);\n\t}\n\tif (!response.ok) {\n\t\tdiscard(response);\n\t\tthrow new Error(`HTTP ${response.status} loading OAuth protected resource metadata`);\n\t}\n\treturn parseProtectedResourceMetadata(await response.json());\n}\n\nexport function buildAuthorizationServerDiscoveryUrls(\n\tauthorizationServerUrl: string | URL,\n): { url: URL; type: \"oauth\" | \"oidc\" }[] {\n\tconst issuer = new URL(authorizationServerUrl);\n\tconst path = pathSuffix(issuer.pathname);\n\tconst urls: { url: URL; type: \"oauth\" | \"oidc\" }[] = [\n\t\t{ url: new URL(`/.well-known/oauth-authorization-server${path}`, issuer.origin), type: \"oauth\" },\n\t\t{ url: new URL(`/.well-known/openid-configuration${path}`, issuer.origin), type: \"oidc\" },\n\t];\n\tif (path) urls.push({ url: new URL(`${path}/.well-known/openid-configuration`, issuer.origin), type: \"oidc\" });\n\treturn urls;\n}\n\nexport async function discoverAuthorizationServerMetadata(\n\tauthorizationServerUrl: string | URL,\n\toptions: { fetch?: McpFetch; protocolVersion?: string; skipIssuerValidation?: boolean; signal?: AbortSignal } = {},\n): Promise<AuthorizationServerMetadata | undefined> {\n\tconst fetch = options.fetch ?? globalThis.fetch;\n\tfor (const { url } of buildAuthorizationServerDiscoveryUrls(authorizationServerUrl)) {\n\t\tconst response = await fetchMetadata(\n\t\t\turl,\n\t\t\tfetch,\n\t\t\toptions.protocolVersion ?? LATEST_PROTOCOL_VERSION,\n\t\t\toptions.signal,\n\t\t);\n\t\tif (!response.ok) {\n\t\t\tdiscard(response);\n\t\t\tif (isDiscoveryMiss(response.status)) continue;\n\t\t\tthrow new Error(`HTTP ${response.status} loading authorization server metadata from ${url}`);\n\t\t}\n\t\tconst metadata = parseAuthorizationServerMetadata(await response.json());\n\t\tif (!options.skipIssuerValidation) {\n\t\t\tconst expected = String(authorizationServerUrl);\n\t\t\t// URL parsing adds a trailing slash to bare origins, so compare without one on either side.\n\t\t\tconst trim = (value: string) => (value.endsWith(\"/\") ? value.slice(0, -1) : value);\n\t\t\tif (trim(metadata.issuer) !== trim(expected)) throw new OAuthIssuerMismatchError(expected, metadata.issuer);\n\t\t}\n\t\treturn metadata;\n\t}\n\treturn undefined;\n}\n\nexport async function discoverOAuthServerInfo(\n\tserverUrl: string | URL,\n\toptions: {\n\t\tresourceMetadataUrl?: URL;\n\t\t/** Metadata document to use instead of discovery. It is trusted as configured, so its issuer is not checked. */\n\t\tauthorizationServerMetadataUrl?: URL;\n\t\tfetch?: McpFetch;\n\t\tskipIssuerValidation?: boolean;\n\t\tsignal?: AbortSignal;\n\t} = {},\n): Promise<OAuthServerInfo> {\n\tlet resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n\ttry {\n\t\tresourceMetadata = await discoverProtectedResourceMetadata(serverUrl, {\n\t\t\tresourceMetadataUrl: options.resourceMetadataUrl,\n\t\t\tfetch: options.fetch,\n\t\t\tsignal: options.signal,\n\t\t});\n\t} catch (error) {\n\t\tif (error instanceof TypeError) throw error;\n\t}\n\tif (options.authorizationServerMetadataUrl) {\n\t\tconst url = options.authorizationServerMetadataUrl;\n\t\tconst response = await fetchMetadata(\n\t\t\turl,\n\t\t\toptions.fetch ?? globalThis.fetch,\n\t\t\tLATEST_PROTOCOL_VERSION,\n\t\t\toptions.signal,\n\t\t);\n\t\tif (!response.ok) {\n\t\t\tdiscard(response);\n\t\t\tthrow new Error(`HTTP ${response.status} loading authorization server metadata from ${url}`);\n\t\t}\n\t\tconst metadata = parseAuthorizationServerMetadata(await response.json());\n\t\treturn { authorizationServerUrl: metadata.issuer, authorizationServerMetadata: metadata, resourceMetadata };\n\t}\n\tconst authorizationServerUrl = resourceMetadata?.authorization_servers?.[0] ?? String(new URL(\"/\", serverUrl));\n\treturn {\n\t\tauthorizationServerUrl,\n\t\tauthorizationServerMetadata: await discoverAuthorizationServerMetadata(authorizationServerUrl, {\n\t\t\tfetch: options.fetch,\n\t\t\tskipIssuerValidation: options.skipIssuerValidation,\n\t\t\tsignal: options.signal,\n\t\t}),\n\t\tresourceMetadata,\n\t};\n}\n\nexport function resourceUrlFromServerUrl(value: string | URL): URL {\n\tconst url = new URL(value);\n\turl.hash = \"\";\n\treturn url;\n}\n\nexport function selectResource(serverUrl: string | URL, metadata?: OAuthProtectedResourceMetadata): string | undefined {\n\tif (!metadata) return undefined;\n\tconst requested = resourceUrlFromServerUrl(serverUrl);\n\tconst configured = new URL(metadata.resource);\n\tif (requested.origin !== configured.origin) {\n\t\tthrow new Error(`Protected resource ${metadata.resource} does not match MCP server ${requested}`);\n\t}\n\tconst requestedPath = requested.pathname.endsWith(\"/\") ? requested.pathname : `${requested.pathname}/`;\n\tconst configuredPath = configured.pathname.endsWith(\"/\") ? configured.pathname : `${configured.pathname}/`;\n\tif (!requestedPath.startsWith(configuredPath)) {\n\t\tthrow new Error(`Protected resource ${metadata.resource} does not match MCP server ${requested}`);\n\t}\n\treturn metadata.resource;\n}\n"]}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/oauth/discovery.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Adapted from modelcontextprotocol/typescript-sdk v1.29.0 src/client/auth.ts.\n * Copyright (c) 2024 Anthropic, PBC. Licensed under MIT; see THIRD_PARTY_NOTICES/.\n * Modified to remove Zod/CORS shims and enforce authorization-server issuer validation.\n */\n\nimport type { McpFetch } from \"../auth-provider.ts\";\nimport { LATEST_PROTOCOL_VERSION } from \"../protocol/types.ts\";\nimport { OAuthIssuerMismatchError } from \"./errors.ts\";\nimport {\n\ttype AuthorizationServerMetadata,\n\ttype OAuthChallenge,\n\ttype OAuthProtectedResourceMetadata,\n\ttype OAuthServerInfo,\n\tparseAuthorizationServerMetadata,\n\tparseProtectedResourceMetadata,\n} from \"./types.ts\";\n\nfunction discard(response: Response | undefined): void {\n\tvoid response?.body?.cancel().catch(() => {});\n}\n\n/** 4xx and 502 mean \"not here\", so discovery tries the next candidate URL. */\nfunction isDiscoveryMiss(status: number): boolean {\n\treturn (status >= 400 && status < 500) || status === 502;\n}\n\n/** Path suffix for `/.well-known/<kind><path>`; empty for the root path. */\nfunction pathSuffix(pathname: string): string {\n\treturn pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n}\n\nfunction field(header: string, name: string): string | undefined {\n\tconst match = header.match(new RegExp(String.raw`(?:^|[,\\s])${name}=(?:\"([^\"]*)\"|([^\\s,]+))`, \"i\"));\n\t// An empty value (`scope=\"\"`) carries no information, so it counts as absent.\n\treturn match?.[1] || match?.[2] || undefined;\n}\n\nexport function parseWwwAuthenticate(header: string | null): OAuthChallenge {\n\tif (!header) return {};\n\tconst scheme = header.trimStart().split(/\\s+/, 1)[0]?.toLowerCase();\n\tif (scheme !== \"bearer\" && scheme !== \"dpop\") return {};\n\tconst resourceMetadata = field(header, \"resource_metadata\");\n\tlet resourceMetadataUrl: URL | undefined;\n\tif (resourceMetadata) {\n\t\ttry {\n\t\t\tresourceMetadataUrl = new URL(resourceMetadata);\n\t\t} catch {}\n\t}\n\treturn {\n\t\tresourceMetadataUrl,\n\t\tscope: field(header, \"scope\"),\n\t\terror: field(header, \"error\"),\n\t\terrorDescription: field(header, \"error_description\"),\n\t};\n}\n\nasync function fetchMetadata(\n\turl: URL,\n\tfetch: McpFetch,\n\tprotocolVersion: string,\n\tsignal: AbortSignal | undefined,\n): Promise<Response> {\n\treturn fetch(url, {\n\t\theaders: { Accept: \"application/json\", \"MCP-Protocol-Version\": protocolVersion },\n\t\tsignal,\n\t});\n}\n\nexport async function discoverProtectedResourceMetadata(\n\tserverUrl: string | URL,\n\toptions: {\n\t\tresourceMetadataUrl?: string | URL;\n\t\tprotocolVersion?: string;\n\t\tfetch?: McpFetch;\n\t\tsignal?: AbortSignal;\n\t} = {},\n): Promise<OAuthProtectedResourceMetadata> {\n\tconst server = new URL(serverUrl);\n\tconst fetch = options.fetch ?? globalThis.fetch;\n\tconst version = options.protocolVersion ?? LATEST_PROTOCOL_VERSION;\n\tlet response = await fetchMetadata(\n\t\toptions.resourceMetadataUrl\n\t\t\t? new URL(options.resourceMetadataUrl)\n\t\t\t: new URL(`/.well-known/oauth-protected-resource${pathSuffix(server.pathname)}`, server.origin),\n\t\tfetch,\n\t\tversion,\n\t\toptions.signal,\n\t);\n\tif (!options.resourceMetadataUrl && server.pathname !== \"/\" && isDiscoveryMiss(response.status)) {\n\t\tdiscard(response);\n\t\tresponse = await fetchMetadata(\n\t\t\tnew URL(\"/.well-known/oauth-protected-resource\", server.origin),\n\t\t\tfetch,\n\t\t\tversion,\n\t\t\toptions.signal,\n\t\t);\n\t}\n\tif (!response.ok) {\n\t\tdiscard(response);\n\t\tthrow new Error(`HTTP ${response.status} loading OAuth protected resource metadata`);\n\t}\n\treturn parseProtectedResourceMetadata(await response.json());\n}\n\nexport function buildAuthorizationServerDiscoveryUrls(\n\tauthorizationServerUrl: string | URL,\n): { url: URL; type: \"oauth\" | \"oidc\" }[] {\n\tconst issuer = new URL(authorizationServerUrl);\n\tconst path = pathSuffix(issuer.pathname);\n\tconst urls: { url: URL; type: \"oauth\" | \"oidc\" }[] = [\n\t\t{ url: new URL(`/.well-known/oauth-authorization-server${path}`, issuer.origin), type: \"oauth\" },\n\t\t{ url: new URL(`/.well-known/openid-configuration${path}`, issuer.origin), type: \"oidc\" },\n\t];\n\tif (path) urls.push({ url: new URL(`${path}/.well-known/openid-configuration`, issuer.origin), type: \"oidc\" });\n\treturn urls;\n}\n\nexport async function discoverAuthorizationServerMetadata(\n\tauthorizationServerUrl: string | URL,\n\toptions: { fetch?: McpFetch; protocolVersion?: string; skipIssuerValidation?: boolean; signal?: AbortSignal } = {},\n): Promise<AuthorizationServerMetadata | undefined> {\n\tconst fetch = options.fetch ?? globalThis.fetch;\n\tfor (const { url } of buildAuthorizationServerDiscoveryUrls(authorizationServerUrl)) {\n\t\tconst response = await fetchMetadata(\n\t\t\turl,\n\t\t\tfetch,\n\t\t\toptions.protocolVersion ?? LATEST_PROTOCOL_VERSION,\n\t\t\toptions.signal,\n\t\t);\n\t\tif (!response.ok) {\n\t\t\tdiscard(response);\n\t\t\tif (isDiscoveryMiss(response.status)) continue;\n\t\t\tthrow new Error(`HTTP ${response.status} loading authorization server metadata from ${url}`);\n\t\t}\n\t\tconst metadata = parseAuthorizationServerMetadata(await response.json());\n\t\tif (!options.skipIssuerValidation) {\n\t\t\tconst expected = String(authorizationServerUrl);\n\t\t\t// URL parsing adds a trailing slash to bare origins, so compare without one on either side.\n\t\t\tconst trim = (value: string) => (value.endsWith(\"/\") ? value.slice(0, -1) : value);\n\t\t\tif (trim(metadata.issuer) !== trim(expected)) throw new OAuthIssuerMismatchError(expected, metadata.issuer);\n\t\t}\n\t\treturn metadata;\n\t}\n\treturn undefined;\n}\n\nexport async function discoverOAuthServerInfo(\n\tserverUrl: string | URL,\n\toptions: {\n\t\tresourceMetadataUrl?: URL;\n\t\t/** Metadata document to use instead of discovery. It is trusted as configured, so its issuer is not checked. */\n\t\tauthorizationServerMetadataUrl?: URL;\n\t\tfetch?: McpFetch;\n\t\tskipIssuerValidation?: boolean;\n\t\tsignal?: AbortSignal;\n\t} = {},\n): Promise<OAuthServerInfo> {\n\tlet resourceMetadata: OAuthProtectedResourceMetadata | undefined;\n\ttry {\n\t\tresourceMetadata = await discoverProtectedResourceMetadata(serverUrl, {\n\t\t\tresourceMetadataUrl: options.resourceMetadataUrl,\n\t\t\tfetch: options.fetch,\n\t\t\tsignal: options.signal,\n\t\t});\n\t} catch (error) {\n\t\tif (error instanceof TypeError) throw error;\n\t}\n\tif (options.authorizationServerMetadataUrl) {\n\t\tconst url = options.authorizationServerMetadataUrl;\n\t\tconst response = await fetchMetadata(\n\t\t\turl,\n\t\t\toptions.fetch ?? globalThis.fetch,\n\t\t\tLATEST_PROTOCOL_VERSION,\n\t\t\toptions.signal,\n\t\t);\n\t\tif (!response.ok) {\n\t\t\tdiscard(response);\n\t\t\tthrow new Error(`HTTP ${response.status} loading authorization server metadata from ${url}`);\n\t\t}\n\t\tconst metadata = parseAuthorizationServerMetadata(await response.json());\n\t\treturn { authorizationServerUrl: metadata.issuer, authorizationServerMetadata: metadata, resourceMetadata };\n\t}\n\tconst authorizationServerUrl = resourceMetadata?.authorization_servers?.[0] ?? String(new URL(\"/\", serverUrl));\n\treturn {\n\t\tauthorizationServerUrl,\n\t\tauthorizationServerMetadata: await discoverAuthorizationServerMetadata(authorizationServerUrl, {\n\t\t\tfetch: options.fetch,\n\t\t\tskipIssuerValidation: options.skipIssuerValidation,\n\t\t\tsignal: options.signal,\n\t\t}),\n\t\tresourceMetadata,\n\t};\n}\n\nexport function resourceUrlFromServerUrl(value: string | URL): URL {\n\tconst url = new URL(value);\n\turl.hash = \"\";\n\treturn url;\n}\n\nexport function selectResource(serverUrl: string | URL, metadata?: OAuthProtectedResourceMetadata): string | undefined {\n\tif (!metadata) return undefined;\n\tconst requested = resourceUrlFromServerUrl(serverUrl);\n\tconst configured = new URL(metadata.resource);\n\tif (requested.origin !== configured.origin) {\n\t\tthrow new Error(`Protected resource ${metadata.resource} does not match MCP server ${requested}`);\n\t}\n\tconst requestedPath = requested.pathname.endsWith(\"/\") ? requested.pathname : `${requested.pathname}/`;\n\tconst configuredPath = configured.pathname.endsWith(\"/\") ? configured.pathname : `${configured.pathname}/`;\n\tif (!requestedPath.startsWith(configuredPath)) {\n\t\tthrow new Error(`Protected resource ${metadata.resource} does not match MCP server ${requested}`);\n\t}\n\treturn metadata.resource;\n}\n"]}