@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +140 -7
  2. package/OAUTH.md +13 -16
  3. package/README.md +8 -8
  4. package/cli.js +67 -28
  5. package/dist/abort.js +18 -9
  6. package/dist/client-operations.js +4 -2
  7. package/dist/commands.js +77 -43
  8. package/dist/config.js +194 -105
  9. package/dist/connection-runtime.d.ts +2 -2
  10. package/dist/connection-runtime.js +29 -18
  11. package/dist/consent-manager.d.ts +2 -2
  12. package/dist/consent-manager.js +5 -2
  13. package/dist/direct-tools.d.ts +1 -1
  14. package/dist/direct-tools.js +13 -7
  15. package/dist/elicitation-handler.d.ts +2 -3
  16. package/dist/elicitation-handler.js +113 -58
  17. package/dist/gateway-arguments.d.ts +1 -1
  18. package/dist/gateway-arguments.js +90 -23
  19. package/dist/glimpse-ui.js +10 -5
  20. package/dist/host-html-template.d.ts +3 -1
  21. package/dist/host-html-template.js +9 -12
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +264 -119
  24. package/dist/init.d.ts +1 -3
  25. package/dist/init.js +91 -51
  26. package/dist/legacy-migration.d.ts +1 -1
  27. package/dist/legacy-migration.js +17 -12
  28. package/dist/lifecycle.d.ts +3 -3
  29. package/dist/lifecycle.js +33 -17
  30. package/dist/logger.d.ts +2 -2
  31. package/dist/logger.js +4 -2
  32. package/dist/mcp-auth-flow.js +150 -58
  33. package/dist/mcp-auth.d.ts +7 -7
  34. package/dist/mcp-auth.js +189 -138
  35. package/dist/mcp-callback-server.js +19 -8
  36. package/dist/mcp-code.js +168 -54
  37. package/dist/mcp-keyring-helper.cjs +57 -23
  38. package/dist/mcp-oauth-provider.d.ts +6 -7
  39. package/dist/mcp-oauth-provider.js +146 -72
  40. package/dist/mcp-oauth-runtime.js +29 -16
  41. package/dist/mcp-output-guard.js +193 -65
  42. package/dist/mcp-output-preview.js +41 -20
  43. package/dist/mcp-panel.d.ts +9 -9
  44. package/dist/mcp-panel.js +141 -70
  45. package/dist/mcp-script-worker.mjs +135 -43
  46. package/dist/mcp-setup-panel.d.ts +8 -8
  47. package/dist/mcp-setup-panel.js +130 -44
  48. package/dist/mcp-status.js +14 -7
  49. package/dist/mcp-trace.d.ts +10 -5
  50. package/dist/mcp-trace.js +127 -61
  51. package/dist/metadata-cache.d.ts +3 -4
  52. package/dist/metadata-cache.js +83 -43
  53. package/dist/npx-resolver.js +101 -60
  54. package/dist/onboarding-state.js +4 -2
  55. package/dist/plain-result-reader.js +78 -31
  56. package/dist/prompts.js +36 -20
  57. package/dist/proxy-modes.js +670 -193
  58. package/dist/resource-tools.js +30 -9
  59. package/dist/runtime-owner.js +36 -18
  60. package/dist/sampling-handler.d.ts +1 -2
  61. package/dist/sampling-handler.js +41 -21
  62. package/dist/search-ranking.d.ts +1 -1
  63. package/dist/search-ranking.js +67 -34
  64. package/dist/server-manager.d.ts +11 -11
  65. package/dist/server-manager.js +283 -166
  66. package/dist/session-recovery.js +113 -61
  67. package/dist/tool-approval.js +9 -3
  68. package/dist/tool-loader.js +154 -55
  69. package/dist/tool-metadata.d.ts +1 -1
  70. package/dist/tool-metadata.js +51 -23
  71. package/dist/tool-registrar.js +32 -10
  72. package/dist/tool-result-renderer.d.ts +3 -3
  73. package/dist/tool-result-renderer.js +65 -44
  74. package/dist/types.d.ts +2 -2
  75. package/dist/types.js +39 -18
  76. package/dist/ui-message-history.js +33 -13
  77. package/dist/ui-resource-handler.d.ts +3 -3
  78. package/dist/ui-resource-handler.js +37 -21
  79. package/dist/ui-server.js +101 -43
  80. package/dist/ui-session.js +66 -29
  81. package/dist/ui-stream-types.js +14 -4
  82. package/dist/ui-tool-visibility.js +12 -6
  83. package/dist/unix-socket-transport.d.ts +1 -2
  84. package/dist/unix-socket-transport.js +19 -12
  85. package/dist/utils.d.ts +2 -0
  86. package/dist/utils.js +60 -35
  87. package/package.json +29 -5
  88. package/skills/mcp-scripting/SKILL.md +6 -6
@@ -8,23 +8,29 @@ import { AsyncLocalStorage } from "node:async_hooks";
8
8
  import { UnauthorizedError, OAuthError, OAuthErrorCode, createPrivateKeyJwtAuth, discoverAndRequestJwtAuthGrant, validateClientMetadataUrl, } from "@modelcontextprotocol/client";
9
9
  import { getAuthForUrl, updateTokens, updateClientInfo, clearAllCredentials, clearClientInfo, clearCodeVerifier, clearTokens, } from "./mcp-auth.js";
10
10
  import { resolveCommandSecret } from "./utils.js";
11
- import { isNonInteractiveOAuth } from "./types.js";
11
+ import { isNonInteractiveOAuth, } from "./types.js";
12
12
  import { combineAbortSignals } from "./runtime-owner.js";
13
13
  import { getOAuthCallbackPath, getOAuthCallbackPort } from "./mcp-oauth-callback-config.js";
14
14
  import sharedClientMetadata from "./docs/client-metadata.json" with { type: "json" };
15
15
  /** Validate explicit document URLs without echoing possible credentials in errors. */
16
16
  export function validateOAuthClientMetadataUrl(value) {
17
- if (value === undefined || value === false)
17
+ if (value === undefined || value === false) {
18
18
  return;
19
+ }
19
20
  try {
20
- if (typeof value !== "string" || !value)
21
+ if (typeof value !== "string" || !value) {
21
22
  throw new Error();
23
+ }
22
24
  validateClientMetadataUrl(value);
23
25
  const url = new URL(value);
24
26
  const raw = value.match(/^[^:]+:[/\\]*([^/\\?#]*)([^?#]*)/);
25
- if (url.username || url.password || raw?.[1]?.includes("@") || value.includes("#")
26
- || /[/\\](?:\.|%2e){1,2}(?=[/\\]|$)/i.test(raw?.[2] ?? ""))
27
+ if (url.username ||
28
+ url.password ||
29
+ raw?.[1]?.includes("@") ||
30
+ value.includes("#") ||
31
+ /[/\\](?:\.|%2e){1,2}(?=[/\\]|$)/i.test(raw?.[2] ?? "")) {
27
32
  throw new Error();
33
+ }
28
34
  }
29
35
  catch {
30
36
  throw new Error("OAuth clientMetadataUrl must be an HTTPS URL with a non-root path and no userinfo, fragment or dot segments");
@@ -32,24 +38,26 @@ export function validateOAuthClientMetadataUrl(value) {
32
38
  }
33
39
  /** Only the port may differ; keep raw host, path and query spelling exact. */
34
40
  export function loopbackRedirectsMatch(first, second) {
35
- if (typeof second !== "string")
41
+ if (typeof second !== "string") {
36
42
  return false;
43
+ }
37
44
  const port = /^([^:/?#]+:\/\/[^/\\?#@]+):\d+(?=[/\\?#]|$)/;
38
- if (!port.test(first) || !port.test(second) || first.includes("#") || second.includes("#"))
45
+ if (!port.test(first) || !port.test(second) || first.includes("#") || second.includes("#")) {
39
46
  return false;
47
+ }
40
48
  try {
41
- return [new URL(first), new URL(second)].every(url => url.protocol === "http:"
42
- && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) && Number(url.port) > 0)
43
- && first.replace(port, "$1") === second.replace(port, "$1");
49
+ return ([new URL(first), new URL(second)].every((url) => url.protocol === "http:" &&
50
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) &&
51
+ Number(url.port) > 0) && first.replace(port, "$1") === second.replace(port, "$1"));
44
52
  }
45
53
  catch {
46
54
  return false;
47
55
  }
48
56
  }
49
57
  export function issuersMatch(first, second) {
50
- return first === second
51
- || (first.endsWith("/") && first.slice(0, -1) === second)
52
- || (second.endsWith("/") && second.slice(0, -1) === first);
58
+ return (first === second ||
59
+ (first.endsWith("/") && first.slice(0, -1) === second) ||
60
+ (second.endsWith("/") && second.slice(0, -1) === first));
53
61
  }
54
62
  function isRecord(value) {
55
63
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -57,47 +65,61 @@ function isRecord(value) {
57
65
  /** Validate shapes and method conflicts without resolving or disclosing key sources. */
58
66
  export function validateOAuthPrivateKeyJwt(config) {
59
67
  const jwt = config.privateKeyJwt;
60
- if (jwt === undefined)
68
+ if (jwt === undefined) {
61
69
  return;
62
- if (!isRecord(jwt))
70
+ }
71
+ if (!isRecord(jwt)) {
63
72
  throw new Error("OAuth privateKeyJwt must be an object");
73
+ }
64
74
  if (!(typeof jwt.privateKey === "string" ? jwt.privateKey.trim() : isRecord(jwt.privateKey))) {
65
75
  throw new Error("OAuth privateKeyJwt.privateKey must be a nonempty string or JWK object");
66
76
  }
67
- if (typeof jwt.algorithm !== "string" || !jwt.algorithm.trim() || /^(?:HS|none$)/i.test(jwt.algorithm)) {
77
+ if (typeof jwt.algorithm !== "string" ||
78
+ !jwt.algorithm.trim() ||
79
+ /^(?:HS|none$)/i.test(jwt.algorithm)) {
68
80
  throw new Error("OAuth privateKeyJwt.algorithm must be an asymmetric JOSE algorithm");
69
81
  }
70
82
  if (jwt.audience !== undefined && (typeof jwt.audience !== "string" || !jwt.audience.trim())) {
71
83
  throw new Error("OAuth privateKeyJwt.audience must be a nonempty string");
72
84
  }
73
- if (jwt.lifetimeSeconds !== undefined && (!Number.isSafeInteger(jwt.lifetimeSeconds) || jwt.lifetimeSeconds <= 0)) {
85
+ if (jwt.lifetimeSeconds !== undefined &&
86
+ (!Number.isSafeInteger(jwt.lifetimeSeconds) || jwt.lifetimeSeconds <= 0)) {
74
87
  throw new Error("OAuth privateKeyJwt.lifetimeSeconds must be a positive integer");
75
88
  }
76
- if (jwt.claims !== undefined && !isRecord(jwt.claims))
89
+ if (jwt.claims !== undefined && !isRecord(jwt.claims)) {
77
90
  throw new Error("OAuth privateKeyJwt.claims must be an object");
78
- if (config.clientSecret !== undefined)
91
+ }
92
+ if (config.clientSecret !== undefined) {
79
93
  throw new Error("OAuth privateKeyJwt cannot be combined with clientSecret");
80
- if (!config.clientId && (typeof config.clientMetadataUrl !== "string" || config.clientMetadataUrl === sharedClientMetadata.client_id)) {
94
+ }
95
+ if (!config.clientId &&
96
+ (typeof config.clientMetadataUrl !== "string" ||
97
+ config.clientMetadataUrl === sharedClientMetadata.client_id)) {
81
98
  throw new Error("OAuth privateKeyJwt requires a configured clientId or custom clientMetadataUrl, not the shared browser identity");
82
99
  }
83
100
  }
84
101
  export function validateOAuthCrossAppAccess(config) {
85
102
  const crossApp = config.crossAppAccess;
86
- if (crossApp === undefined)
103
+ if (crossApp === undefined) {
87
104
  return;
88
- if (!isRecord(crossApp))
105
+ }
106
+ if (!isRecord(crossApp)) {
89
107
  throw new Error("OAuth crossAppAccess must be an object");
108
+ }
90
109
  for (const field of ["idpUrl", "clientId", "idToken", "clientSecret"]) {
91
- if (field === "clientSecret" && crossApp[field] === undefined)
110
+ if (field === "clientSecret" && crossApp[field] === undefined) {
92
111
  continue;
112
+ }
93
113
  if (typeof crossApp[field] !== "string" || !crossApp[field].trim()) {
94
114
  throw new Error(`OAuth crossAppAccess.${field} must be a nonempty string`);
95
115
  }
96
116
  }
97
- if (config.grantType !== undefined)
117
+ if (config.grantType !== undefined) {
98
118
  throw new Error("OAuth crossAppAccess selects the JWT-bearer grant; omit grantType");
99
- if ("audience" in crossApp || "resource" in crossApp)
119
+ }
120
+ if ("audience" in crossApp || "resource" in crossApp) {
100
121
  throw new Error("OAuth crossAppAccess uses the discovered MCP issuer and resource; overrides are not supported");
122
+ }
101
123
  }
102
124
  const reservedAuthorizationParams = new Set([
103
125
  "client_id",
@@ -110,8 +132,9 @@ const reservedAuthorizationParams = new Set([
110
132
  "state",
111
133
  ]);
112
134
  function addAuthorizationParams(authorizationUrl, params) {
113
- if (!params)
135
+ if (!params) {
114
136
  return authorizationUrl;
137
+ }
115
138
  const nextUrl = new URL(authorizationUrl.toString());
116
139
  for (const [key, value] of Object.entries(params)) {
117
140
  if (reservedAuthorizationParams.has(key) || nextUrl.searchParams.has(key)) {
@@ -156,15 +179,18 @@ export class McpOAuthProvider {
156
179
  this.flowState = initialState;
157
180
  this.redirectUrlSnapshot = isNonInteractiveOAuth(config)
158
181
  ? undefined
159
- : config.redirectUri ?? `http://localhost:${getOAuthCallbackPort()}${getOAuthCallbackPath()}`;
182
+ : (config.redirectUri ??
183
+ `http://localhost:${getOAuthCallbackPort()}${getOAuthCallbackPath()}`);
160
184
  if (config.clientMetadataUrl !== false && config.clientSecret === undefined) {
161
185
  if (config.clientMetadataUrl && config.clientMetadataUrl !== sharedClientMetadata.client_id) {
162
186
  this.clientMetadataUrl = config.clientMetadataUrl;
163
187
  }
164
- else if (!this.isNonInteractive && !config.privateKeyJwt
165
- && (config.clientName === undefined || config.clientName === sharedClientMetadata.client_name)
166
- && (config.clientUri === undefined || config.clientUri === sharedClientMetadata.client_uri)
167
- && sharedClientMetadata.redirect_uris.some(uri => loopbackRedirectsMatch(uri, this.redirectUrl))) {
188
+ else if (!this.isNonInteractive &&
189
+ !config.privateKeyJwt &&
190
+ (config.clientName === undefined ||
191
+ config.clientName === sharedClientMetadata.client_name) &&
192
+ (config.clientUri === undefined || config.clientUri === sharedClientMetadata.client_uri) &&
193
+ sharedClientMetadata.redirect_uris.some((uri) => loopbackRedirectsMatch(uri, this.redirectUrl))) {
168
194
  this.clientMetadataUrl = sharedClientMetadata.client_id;
169
195
  }
170
196
  }
@@ -173,8 +199,8 @@ export class McpOAuthProvider {
173
199
  return isNonInteractiveOAuth(this.config);
174
200
  }
175
201
  get discoveredIssuer() {
176
- return this.flowDiscoveryState?.authorizationServerMetadata?.issuer
177
- ?? this.flowDiscoveryState?.authorizationServerUrl;
202
+ return (this.flowDiscoveryState?.authorizationServerMetadata?.issuer ??
203
+ this.flowDiscoveryState?.authorizationServerUrl);
178
204
  }
179
205
  get signal() {
180
206
  return combineAbortSignals(this.lifetimeSignal, this.requestSignals.getStore());
@@ -191,11 +217,11 @@ export class McpOAuthProvider {
191
217
  if (this.flowIssuerMismatch) {
192
218
  throw new Error(`OAuth authorization server issuer changed for ${this.serverName}; clear credentials before authenticating again`);
193
219
  }
194
- if (!entry || !issuer)
220
+ if (!entry || !issuer) {
195
221
  return;
196
- const storedIssuers = [entry.clientInfo?.issuer, entry.tokens?.issuer]
197
- .filter((storedIssuer) => storedIssuer !== undefined);
198
- if (storedIssuers.some(storedIssuer => !issuersMatch(storedIssuer, issuer))) {
222
+ }
223
+ const storedIssuers = [entry.clientInfo?.issuer, entry.tokens?.issuer].filter((storedIssuer) => storedIssuer !== undefined);
224
+ if (storedIssuers.some((storedIssuer) => !issuersMatch(storedIssuer, issuer))) {
199
225
  this.flowIssuerMismatch = true;
200
226
  throw new Error(`OAuth authorization server issuer changed for ${this.serverName}; clear credentials before authenticating again`);
201
227
  }
@@ -220,8 +246,16 @@ export class McpOAuthProvider {
220
246
  client_name: this.config.clientName ?? "Pi Coding Agent",
221
247
  client_uri: this.config.clientUri ?? "https://github.com/fitchmultz/pi-mcp-adapter",
222
248
  redirect_uris: [],
223
- grant_types: [this.config.crossAppAccess ? "urn:ietf:params:oauth:grant-type:jwt-bearer" : "client_credentials"],
224
- token_endpoint_auth_method: this.config.privateKeyJwt ? "private_key_jwt" : this.config.clientSecret ? "client_secret_post" : "none",
249
+ grant_types: [
250
+ this.config.crossAppAccess
251
+ ? "urn:ietf:params:oauth:grant-type:jwt-bearer"
252
+ : "client_credentials",
253
+ ],
254
+ token_endpoint_auth_method: this.config.privateKeyJwt
255
+ ? "private_key_jwt"
256
+ : this.config.clientSecret
257
+ ? "client_secret_post"
258
+ : "none",
225
259
  };
226
260
  }
227
261
  const redirectUrl = this.redirectUrl;
@@ -234,7 +268,11 @@ export class McpOAuthProvider {
234
268
  client_uri: this.config.clientUri ?? "https://github.com/fitchmultz/pi-mcp-adapter",
235
269
  grant_types: ["authorization_code", "refresh_token"],
236
270
  response_types: ["code"],
237
- token_endpoint_auth_method: this.config.privateKeyJwt ? "private_key_jwt" : this.config.clientSecret ? "client_secret_post" : "none",
271
+ token_endpoint_auth_method: this.config.privateKeyJwt
272
+ ? "private_key_jwt"
273
+ : this.config.clientSecret
274
+ ? "client_secret_post"
275
+ : "none",
238
276
  ...(this.config.scope !== undefined ? { scope: this.config.scope } : {}),
239
277
  };
240
278
  }
@@ -247,15 +285,15 @@ export class McpOAuthProvider {
247
285
  const stored = await getAuthForUrl(this.serverName, this.serverUrl, this.storageOptions);
248
286
  this.assertStoredIssuerBindings(stored, issuer);
249
287
  // Rejected discovery must not become the runtime's issuer binding.
250
- if (this.flowDiscoveryState)
288
+ if (this.flowDiscoveryState) {
251
289
  this.callbacks.onDiscoveryState?.(this.flowDiscoveryState);
290
+ }
252
291
  // Check config first (pre-registered client). Store only its issuer binding.
253
292
  // The configured secret stays in config and never enters the credential store.
254
293
  if (this.config.clientId) {
255
- const storedClient = stored?.clientInfo?.clientId === this.config.clientId
256
- ? stored.clientInfo
257
- : undefined;
258
- if (issuer && (storedClient?.issuer !== issuer || storedClient.configPreRegistered !== true)) {
294
+ const storedClient = stored?.clientInfo?.clientId === this.config.clientId ? stored.clientInfo : undefined;
295
+ if (issuer &&
296
+ (storedClient?.issuer !== issuer || storedClient.configPreRegistered !== true)) {
259
297
  updateClientInfo(this.serverName, { clientId: this.config.clientId, issuer, configPreRegistered: true }, this.serverUrl, this.storageOptions);
260
298
  }
261
299
  const clientSecret = this.config.clientSecret?.startsWith("!")
@@ -279,19 +317,22 @@ export class McpOAuthProvider {
279
317
  // would let a token refresh go out with a client_id but no secret,
280
318
  // causing invalid_client and credential invalidation. Return undefined
281
319
  // so callers treat this as "no client info".
282
- const isConfigStub = clientInfo.configPreRegistered === true
283
- || (clientInfo.clientSecret === undefined
284
- && clientInfo.clientIdIssuedAt === undefined
285
- && clientInfo.clientSecretExpiresAt === undefined
286
- && clientInfo.redirectUris === undefined);
320
+ const isConfigStub = clientInfo.configPreRegistered === true ||
321
+ (clientInfo.clientSecret === undefined &&
322
+ clientInfo.clientIdIssuedAt === undefined &&
323
+ clientInfo.clientSecretExpiresAt === undefined &&
324
+ clientInfo.redirectUris === undefined);
287
325
  if (isConfigStub) {
288
326
  return undefined;
289
327
  }
290
- if ((this.config.privateKeyJwt || this.isNonInteractive) && clientInfo.registrationType === "cimd" && clientInfo.clientId === sharedClientMetadata.client_id) {
328
+ if ((this.config.privateKeyJwt || this.isNonInteractive) &&
329
+ clientInfo.registrationType === "cimd" &&
330
+ clientInfo.clientId === sharedClientMetadata.client_id) {
291
331
  throw new OAuthError(OAuthErrorCode.InvalidRequest, `OAuth ${this.config.privateKeyJwt ? "privateKeyJwt" : this.config.crossAppAccess ? "crossAppAccess" : "client_credentials"} cannot use the saved shared browser registration; configure its own clientId or replace that login`);
292
332
  }
293
333
  // Check if client secret has expired
294
- if (clientInfo.clientSecretExpiresAt && clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
334
+ if (clientInfo.clientSecretExpiresAt &&
335
+ clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
295
336
  return undefined;
296
337
  }
297
338
  if (issuer && clientInfo.issuer && !issuersMatch(clientInfo.issuer, issuer)) {
@@ -336,18 +377,25 @@ export class McpOAuthProvider {
336
377
  }, this.serverUrl, this.storageOptions);
337
378
  return;
338
379
  }
339
- const redirectUris = ("redirect_uris" in info ? info.redirect_uris : undefined)
340
- ?? (this.redirectUrl ? [this.redirectUrl] : []);
380
+ const redirectUris = ("redirect_uris" in info ? info.redirect_uris : undefined) ??
381
+ (this.redirectUrl ? [this.redirectUrl] : []);
341
382
  const clientInfo = {
342
383
  clientId: info.client_id,
343
384
  ...(info.client_secret !== undefined ? { clientSecret: info.client_secret } : {}),
344
- ...(info.client_id_issued_at !== undefined ? { clientIdIssuedAt: info.client_id_issued_at } : {}),
345
- ...(info.client_secret_expires_at !== undefined ? { clientSecretExpiresAt: info.client_secret_expires_at } : {}),
385
+ ...(info.client_id_issued_at !== undefined
386
+ ? { clientIdIssuedAt: info.client_id_issued_at }
387
+ : {}),
388
+ ...(info.client_secret_expires_at !== undefined
389
+ ? { clientSecretExpiresAt: info.client_secret_expires_at }
390
+ : {}),
346
391
  redirectUris,
347
392
  // Native CIMD saves only client_id + issuer; DCR's full schema requires redirect_uris.
348
- ...(info.client_id === this.clientMetadataUrl && !("redirect_uris" in info)
349
- && this.flowDiscoveryState?.authorizationServerMetadata?.client_id_metadata_document_supported === true
350
- ? { registrationType: "cimd" } : {}),
393
+ ...(info.client_id === this.clientMetadataUrl &&
394
+ !("redirect_uris" in info) &&
395
+ this.flowDiscoveryState?.authorizationServerMetadata
396
+ ?.client_id_metadata_document_supported === true
397
+ ? { registrationType: "cimd" }
398
+ : {}),
351
399
  ...(issuer !== undefined ? { issuer } : {}),
352
400
  };
353
401
  this.flowClientInfo = clientInfo;
@@ -360,8 +408,9 @@ export class McpOAuthProvider {
360
408
  async tokens(context) {
361
409
  // Use getAuthForUrl to validate tokens are for the current server URL.
362
410
  const entry = await getAuthForUrl(this.serverName, this.serverUrl, this.storageOptions);
363
- if (!entry?.tokens)
411
+ if (!entry?.tokens) {
364
412
  return undefined;
413
+ }
365
414
  const issuer = context?.issuer ?? this.discoveredIssuer;
366
415
  this.assertStoredIssuerBindings(entry, issuer);
367
416
  if (issuer && entry.tokens.issuer === undefined) {
@@ -390,7 +439,9 @@ export class McpOAuthProvider {
390
439
  // Preserve expiry even when expires_in is 0 (e.g. the SDK re-saving an
391
440
  // already-expired token) so expired tokens stay expired instead of
392
441
  // being persisted as never-expiring.
393
- ...(tokens.expires_in !== undefined ? { expiresAt: Date.now() / 1000 + tokens.expires_in } : {}),
442
+ ...(tokens.expires_in !== undefined
443
+ ? { expiresAt: Date.now() / 1000 + tokens.expires_in }
444
+ : {}),
394
445
  ...(tokens.scope !== undefined ? { scope: tokens.scope } : {}),
395
446
  ...(issuer !== undefined ? { issuer } : {}),
396
447
  };
@@ -465,7 +516,8 @@ export class McpOAuthProvider {
465
516
  this.flowResourceUrl = resource;
466
517
  }
467
518
  fetch = (input, init) => fetch(input, {
468
- ...init, signal: combineAbortSignals(init?.signal ?? undefined, this.signal),
519
+ ...init,
520
+ signal: combineAbortSignals(init?.signal ?? undefined, this.signal),
469
521
  });
470
522
  async discoveryState() {
471
523
  this.throwIfInactive();
@@ -531,7 +583,9 @@ export class McpOAuthProvider {
531
583
  */
532
584
  addClientAuthentication = async (headers, params, url, metadata) => {
533
585
  this.throwIfInactive();
534
- if (params.get("grant_type") === "authorization_code" && !params.has("scope") && this.config.scope) {
586
+ if (params.get("grant_type") === "authorization_code" &&
587
+ !params.has("scope") &&
588
+ this.config.scope) {
535
589
  params.set("scope", this.config.scope);
536
590
  }
537
591
  const clientInfo = await this.clientInformation(this.config.privateKeyJwt && metadata?.issuer ? { issuer: metadata.issuer } : undefined);
@@ -555,11 +609,16 @@ export class McpOAuthProvider {
555
609
  }
556
610
  }
557
611
  try {
558
- if (typeof privateKey === "string" && privateKey.trimStart().startsWith("{"))
612
+ if (typeof privateKey === "string" && privateKey.trimStart().startsWith("{")) {
559
613
  privateKey = JSON.parse(privateKey);
614
+ }
560
615
  const { algorithm, ...options } = jwt;
561
616
  await createPrivateKeyJwtAuth({
562
- ...options, privateKey, alg: algorithm, issuer: clientInfo.client_id, subject: clientInfo.client_id,
617
+ ...options,
618
+ privateKey,
619
+ alg: algorithm,
620
+ issuer: clientInfo.client_id,
621
+ subject: clientInfo.client_id,
563
622
  })(headers, params, url, metadata);
564
623
  }
565
624
  catch {
@@ -598,7 +657,9 @@ export class McpOAuthProvider {
598
657
  if (!params.has("client_id")) {
599
658
  params.set("client_id", clientInfo.client_id);
600
659
  }
601
- if (authMethod === "client_secret_post" && clientInfo.client_secret && !params.has("client_secret")) {
660
+ if (authMethod === "client_secret_post" &&
661
+ clientInfo.client_secret &&
662
+ !params.has("client_secret")) {
602
663
  params.set("client_secret", clientInfo.client_secret);
603
664
  }
604
665
  };
@@ -606,21 +667,33 @@ export class McpOAuthProvider {
606
667
  this.throwIfInactive();
607
668
  const audience = this.flowAuthorizationServerUrl;
608
669
  const resource = this.flowResourceUrl;
609
- if (!audience || !resource)
670
+ if (!audience || !resource) {
610
671
  throw new OAuthError(OAuthErrorCode.InvalidRequest, "OAuth crossAppAccess requires discovered MCP authorization server and protected resource metadata");
672
+ }
611
673
  const config = this.config.crossAppAccess;
612
674
  try {
613
675
  const result = await discoverAndRequestJwtAuthGrant({
614
676
  idpUrl: resolveCommandSecret(config.idpUrl, "OAuth crossAppAccess.idpUrl"),
615
677
  clientId: resolveCommandSecret(config.clientId, "OAuth crossAppAccess.clientId"),
616
678
  idToken: resolveCommandSecret(config.idToken, "OAuth crossAppAccess.idToken"),
617
- ...(config.clientSecret !== undefined ? { clientSecret: resolveCommandSecret(config.clientSecret, "OAuth crossAppAccess.clientSecret") } : {}),
618
- audience, resource, ...(scope ? { scope } : {}), fetchFn: this.fetch,
679
+ ...(config.clientSecret !== undefined
680
+ ? {
681
+ clientSecret: resolveCommandSecret(config.clientSecret, "OAuth crossAppAccess.clientSecret"),
682
+ }
683
+ : {}),
684
+ audience,
685
+ resource,
686
+ ...(scope ? { scope } : {}),
687
+ fetchFn: this.fetch,
619
688
  });
620
689
  this.throwIfInactive();
621
- const params = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: result.jwtAuthGrant });
622
- if (scope)
690
+ const params = new URLSearchParams({
691
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
692
+ assertion: result.jwtAuthGrant,
693
+ });
694
+ if (scope) {
623
695
  params.set("scope", scope);
696
+ }
624
697
  return params;
625
698
  }
626
699
  catch {
@@ -630,8 +703,9 @@ export class McpOAuthProvider {
630
703
  }
631
704
  }
632
705
  prepareTokenRequest(scope) {
633
- if (this.config.crossAppAccess)
706
+ if (this.config.crossAppAccess) {
634
707
  return this.prepareCrossAppRequest(scope ?? this.config.scope);
708
+ }
635
709
  if (!this.isNonInteractive) {
636
710
  return undefined;
637
711
  }
@@ -4,13 +4,15 @@
4
4
  * only by operations that need it; all callers share the state owned here.
5
5
  */
6
6
  import { cancelPendingCallback, stopCallbackServer } from "./mcp-callback-server.js";
7
- import { getAuthBaseDir, getOAuthState, clearOAuthState } from "./mcp-auth.js";
7
+ import { getAuthBaseDir, getOAuthState, clearOAuthState, } from "./mcp-auth.js";
8
8
  import { combineAbortSignals } from "./runtime-owner.js";
9
9
  const runtimeStates = new WeakMap();
10
10
  const activeRuntimes = new Set();
11
11
  export function createOAuthRuntime(signal) {
12
12
  const controller = new AbortController();
13
- const runtime = { signal: combineAbortSignals(signal, controller.signal) };
13
+ const runtime = {
14
+ signal: combineAbortSignals(signal, controller.signal),
15
+ };
14
16
  runtimeStates.set(runtime, {
15
17
  controller,
16
18
  generation: 0,
@@ -31,15 +33,17 @@ export function getRuntime(options) {
31
33
  activeRuntimes.add(options.runtime);
32
34
  return options.runtime;
33
35
  }
34
- if (legacyRuntime.signal.aborted)
36
+ if (legacyRuntime.signal.aborted) {
35
37
  legacyRuntime = createOAuthRuntime();
38
+ }
36
39
  activeRuntimes.add(legacyRuntime);
37
40
  return legacyRuntime;
38
41
  }
39
42
  export function getRuntimeState(runtime) {
40
43
  const state = runtimeStates.get(runtime);
41
- if (!state)
44
+ if (!state) {
42
45
  throw new Error("Unknown OAuth runtime");
46
+ }
43
47
  return state;
44
48
  }
45
49
  export function getPendingAuthKey(serverName, options) {
@@ -50,7 +54,7 @@ export function hasPendingAuth(serverName, options, runtime) {
50
54
  if (options) {
51
55
  return state.pendingAuths.has(getPendingAuthKey(serverName, options));
52
56
  }
53
- return Array.from(state.pendingAuths.values()).some(pendingAuth => pendingAuth.serverName === serverName);
57
+ return Array.from(state.pendingAuths.values()).some((pendingAuth) => pendingAuth.serverName === serverName);
54
58
  }
55
59
  export async function clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions = {}) {
56
60
  const state = getRuntimeState(runtime);
@@ -58,8 +62,9 @@ export async function clearPendingAuth(runtime, serverName, oauthState, fallback
58
62
  const pendingAuth = state.pendingAuths.get(key);
59
63
  const authStorageOptions = pendingAuth?.authStorageOptions ?? fallbackStorageOptions;
60
64
  const pendingState = state.pendingAuthStates.get(key);
61
- if (oauthState && pendingState && pendingState !== oauthState)
65
+ if (oauthState && pendingState && pendingState !== oauthState) {
62
66
  return;
67
+ }
63
68
  const timer = state.pendingAuthCleanupTimers.get(key);
64
69
  if (timer) {
65
70
  clearTimeout(timer);
@@ -86,18 +91,23 @@ export async function clearPendingAuth(runtime, serverName, oauthState, fallback
86
91
  */
87
92
  export function supportsOAuth(definition) {
88
93
  // OAuth requires a URL
89
- if (!definition.url)
94
+ if (!definition.url) {
90
95
  return false;
96
+ }
91
97
  // Explicitly disabled via auth: false or oauth: false
92
- if (definition.auth === false)
98
+ if (definition.auth === false) {
93
99
  return false;
94
- if (definition.oauth === false)
100
+ }
101
+ if (definition.oauth === false) {
95
102
  return false;
96
- if (definition.auth === "oauth")
103
+ }
104
+ if (definition.auth === "oauth") {
97
105
  return true;
106
+ }
98
107
  // Configured custom headers take precedence over implicit OAuth auto-detection.
99
- if (definition.headers && Object.keys(definition.headers).length > 0)
108
+ if (definition.headers && Object.keys(definition.headers).length > 0) {
100
109
  return false;
110
+ }
101
111
  // OAuth is enabled when auth is not specified (auto-detect)
102
112
  return definition.auth === undefined;
103
113
  }
@@ -121,14 +131,16 @@ export async function initializeOAuth(runtimeOrSignal) {
121
131
  */
122
132
  export async function shutdownOAuth(runtime = legacyRuntime) {
123
133
  const state = getRuntimeState(runtime);
124
- if (state.controller.signal.aborted)
134
+ if (state.controller.signal.aborted) {
125
135
  return;
136
+ }
126
137
  state.generation += 1;
127
138
  state.controller.abort(new Error("OAuth runtime stopped"));
128
- for (const callbackState of Array.from(state.pendingAuthStates.values()))
139
+ for (const callbackState of Array.from(state.pendingAuthStates.values())) {
129
140
  cancelPendingCallback(callbackState);
130
- const results = await Promise.allSettled(Array.from(state.pendingAuths.values(), pendingAuth => clearPendingAuth(runtime, pendingAuth.serverName, undefined, pendingAuth.authStorageOptions)));
131
- const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
141
+ }
142
+ const results = await Promise.allSettled(Array.from(state.pendingAuths.values(), (pendingAuth) => clearPendingAuth(runtime, pendingAuth.serverName, undefined, pendingAuth.authStorageOptions)));
143
+ const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
132
144
  state.pendingAuthentications.clear();
133
145
  state.requests.clear();
134
146
  activeRuntimes.delete(runtime);
@@ -140,6 +152,7 @@ export async function shutdownOAuth(runtime = legacyRuntime) {
140
152
  failures.push(error);
141
153
  }
142
154
  }
143
- if (failures.length)
155
+ if (failures.length) {
144
156
  throw new AggregateError(failures, "MCP OAuth cleanup failed");
157
+ }
145
158
  }