@fitchmultz/pi-mcp-adapter 6.0.0 → 7.1.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 (111) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/OAUTH.md +1 -1
  3. package/README.md +27 -18
  4. package/dist/app-bridge.bundle.js +14 -14
  5. package/dist/config.js +41 -16
  6. package/dist/direct-tools.js +1 -1
  7. package/dist/gateway-arguments.d.ts +1 -1
  8. package/dist/index.js +38 -59
  9. package/dist/lifecycle.d.ts +2 -2
  10. package/dist/lifecycle.js +2 -2
  11. package/dist/mcp-auth-flow.js +2 -2
  12. package/dist/mcp-auth.d.ts +1 -1
  13. package/dist/mcp-auth.js +3 -3
  14. package/dist/mcp-code.d.ts +6 -6
  15. package/dist/mcp-code.js +11 -10
  16. package/dist/mcp-oauth-provider.d.ts +5 -5
  17. package/dist/mcp-oauth-provider.js +5 -5
  18. package/dist/mcp-output-guard.d.ts +1 -1
  19. package/dist/mcp-output-guard.js +1 -1
  20. package/dist/mcp-panel.d.ts +2 -2
  21. package/dist/mcp-panel.js +2 -2
  22. package/dist/mcp-setup-panel.d.ts +5 -4
  23. package/dist/mcp-setup-panel.js +15 -7
  24. package/dist/mcp-trace.d.ts +1 -1
  25. package/dist/mcp-trace.js +1 -1
  26. package/dist/npx-resolver.js +8 -4
  27. package/dist/proxy-modes.d.ts +2 -2
  28. package/dist/proxy-modes.js +20 -19
  29. package/dist/sampling-handler.js +1 -0
  30. package/dist/search-ranking.js +6 -2
  31. package/dist/server-manager.d.ts +2 -2
  32. package/dist/server-manager.js +44 -27
  33. package/dist/session-recovery.d.ts +7 -4
  34. package/dist/session-recovery.js +76 -10
  35. package/dist/tool-loader.d.ts +7 -2
  36. package/dist/tool-loader.js +56 -5
  37. package/dist/tool-metadata.d.ts +43 -43
  38. package/dist/tool-result-renderer.d.ts +3 -3
  39. package/dist/tool-result-renderer.js +3 -3
  40. package/dist/ui-resource-handler.d.ts +2 -2
  41. package/dist/ui-resource-handler.js +2 -2
  42. package/dist/ui-server.js +5 -4
  43. package/dist/ui-stream-types.d.ts +8 -8
  44. package/dist/unix-socket-transport.d.ts +1 -1
  45. package/dist/unix-socket-transport.js +1 -1
  46. package/dist/utils.d.ts +1 -0
  47. package/dist/utils.js +14 -0
  48. package/package.json +34 -98
  49. package/skills/mcp-scripting/SKILL.md +2 -0
  50. package/abort.ts +0 -34
  51. package/agent-dir.ts +0 -24
  52. package/app-bridge.bundle.js +0 -109
  53. package/banner.png +0 -0
  54. package/checkpoint.ts +0 -55
  55. package/commands.ts +0 -622
  56. package/config.ts +0 -1204
  57. package/consent-manager.ts +0 -25
  58. package/direct-tools.ts +0 -75
  59. package/elicitation-handler.ts +0 -349
  60. package/error-signal.ts +0 -21
  61. package/gateway-arguments.ts +0 -73
  62. package/glimpse-ui.ts +0 -80
  63. package/host-html-template.ts +0 -456
  64. package/index.ts +0 -767
  65. package/init.ts +0 -613
  66. package/legacy-migration.ts +0 -47
  67. package/lifecycle.ts +0 -165
  68. package/logger.ts +0 -127
  69. package/mcp-auth-flow.ts +0 -975
  70. package/mcp-auth.ts +0 -793
  71. package/mcp-callback-server.ts +0 -450
  72. package/mcp-code.ts +0 -412
  73. package/mcp-keyring-helper.cjs +0 -89
  74. package/mcp-oauth-provider.ts +0 -774
  75. package/mcp-output-guard.ts +0 -530
  76. package/mcp-panel.ts +0 -907
  77. package/mcp-script-worker.mjs +0 -131
  78. package/mcp-setup-panel.ts +0 -666
  79. package/mcp-status.ts +0 -110
  80. package/mcp-trace.ts +0 -295
  81. package/metadata-cache.ts +0 -271
  82. package/npx-resolver.ts +0 -488
  83. package/onboarding-state.ts +0 -58
  84. package/panel-keys.ts +0 -37
  85. package/prompts.ts +0 -337
  86. package/proxy-modes.ts +0 -1109
  87. package/resource-tools.ts +0 -52
  88. package/runtime-owner.ts +0 -123
  89. package/sampling-handler.ts +0 -276
  90. package/scripts/build-app-bridge.mjs +0 -15
  91. package/scripts/build.mjs +0 -132
  92. package/scripts/prepare.mjs +0 -103
  93. package/search-ranking.ts +0 -132
  94. package/server-manager.ts +0 -1320
  95. package/session-recovery.ts +0 -316
  96. package/state.ts +0 -64
  97. package/tool-approval.ts +0 -78
  98. package/tool-loader.ts +0 -153
  99. package/tool-metadata.ts +0 -240
  100. package/tool-registrar.ts +0 -74
  101. package/tool-result-renderer.ts +0 -294
  102. package/tsconfig.build.json +0 -13
  103. package/tsconfig.json +0 -16
  104. package/types.ts +0 -785
  105. package/ui-resource-handler.ts +0 -249
  106. package/ui-server.ts +0 -808
  107. package/ui-session.ts +0 -568
  108. package/ui-stream-types.ts +0 -89
  109. package/ui-tool-visibility.ts +0 -25
  110. package/unix-socket-transport.ts +0 -85
  111. package/utils.ts +0 -307
package/mcp-auth-flow.ts DELETED
@@ -1,975 +0,0 @@
1
- /**
2
- * MCP Auth Flow
3
- *
4
- * High-level OAuth flow management using the MCP SDK's built-in auth functions.
5
- */
6
-
7
- import {
8
- auth as runSdkAuth,
9
- extractWWWAuthenticateParams,
10
- UnauthorizedError,
11
- Client,
12
- StreamableHTTPClientTransport,
13
- validateAuthorizationResponseIssuer,
14
- InsufficientScopeError,
15
- SdkHttpError,
16
- computeScopeUnion,
17
- isStrictScopeSuperset,
18
- } from "@modelcontextprotocol/client"
19
- import open from "open"
20
- import { McpOAuthProvider, issuersMatch, loopbackRedirectsMatch, validateOAuthClientMetadataUrl, validateOAuthPrivateKeyJwt, validateOAuthCrossAppAccess, type McpOAuthConfig } from "./mcp-oauth-provider.ts"
21
- import {
22
- ensureCallbackServer,
23
- waitForCallback,
24
- cancelPendingCallback,
25
- stopCallbackServer,
26
- releaseCallbackServer,
27
- takeCallbackResult,
28
- } from "./mcp-callback-server.ts"
29
- import {
30
- getAuthForUrl,
31
- clearAllCredentials,
32
- clearClientInfo,
33
- clearTokens,
34
- clearCodeVerifier,
35
- getOAuthState,
36
- clearOAuthState,
37
- getAuthBaseDir,
38
- type AuthStorageOptions,
39
- } from "./mcp-auth.ts"
40
- import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, type ServerEntry } from "./types.ts"
41
- import { formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs } from "./utils.ts"
42
- import { abortable, throwIfAborted } from "./abort.ts"
43
- import { combineAbortSignals, isAbortError } from "./runtime-owner.ts"
44
-
45
- /** Auth status for a server */
46
- export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
47
-
48
- import type { McpCheckpointEvent } from "./checkpoint.ts"
49
-
50
- export interface McpOAuthRuntime {
51
- readonly signal: AbortSignal
52
- }
53
-
54
- export interface AuthenticateOptions {
55
- onAuthorizationUrl?: (authorizationUrl: string) => void | Promise<void>
56
- authStorageOptions?: AuthStorageOptions
57
- signal?: AbortSignal
58
- runtime?: McpOAuthRuntime
59
- }
60
-
61
- type AuthDiscovery = {
62
- resourceMetadataUrl?: URL
63
- scope?: string
64
- skipIssuerMetadataValidation?: boolean
65
- }
66
-
67
- function applyConfiguredScope(discovery: AuthDiscovery, config: McpOAuthConfig): AuthDiscovery {
68
- return {
69
- ...discovery,
70
- ...(config.scope !== undefined ? { scope: config.scope } : {}),
71
- ...(config.skipIssuerMetadataValidation !== undefined ? { skipIssuerMetadataValidation: config.skipIssuerMetadataValidation } : {}),
72
- }
73
- }
74
-
75
- type CallbackIssuer = { expectedIssuer?: string | undefined; fallbackIssuer?: string; issParameterSupported?: boolean }
76
-
77
- function validateCallbackIssuer(serverName: string, context: CallbackIssuer, response: { iss?: string; error?: string }): void {
78
- if (response.error && context.expectedIssuer === undefined) {
79
- throw new Error("Cannot verify the OAuth error callback issuer")
80
- }
81
- try {
82
- validateAuthorizationResponseIssuer({ expectedIssuer: context.expectedIssuer ?? context.fallbackIssuer, issParameterSupported: context.issParameterSupported === true, iss: response.iss })
83
- } catch {
84
- if (response.iss === undefined && context.issParameterSupported) {
85
- throw new Error(
86
- `The authorization server for ${serverName} requires the RFC 9207 "iss" parameter. ` +
87
- "Paste the full redirect URL from the browser address bar (not just the authorization code).",
88
- )
89
- }
90
- throw new Error(`The OAuth authorization response issuer does not match the discovered issuer for ${serverName}.`)
91
- }
92
- }
93
-
94
- type PendingAuth = {
95
- serverName: string
96
- authProvider: McpOAuthProvider
97
- serverUrl: string
98
- authorizationUrl: string
99
- discovery: AuthDiscovery
100
- callbackIssuer: CallbackIssuer
101
- request?: OAuthRequest
102
- authStorageOptions: AuthStorageOptions
103
- }
104
-
105
- type RuntimeState = {
106
- controller: AbortController
107
- generation: number
108
- activeOperations: number
109
- checkpoint?: McpCheckpointEvent | undefined
110
- pendingAuths: Map<string, PendingAuth>
111
- pendingAuthStates: Map<string, string>
112
- pendingAuthCleanupTimers: Map<string, ReturnType<typeof setTimeout>>
113
- pendingAuthentications: Map<string, Promise<AuthStatus>>
114
- requests: Map<string, OAuthRequest>
115
- }
116
-
117
- const runtimeStates = new WeakMap<McpOAuthRuntime, RuntimeState>()
118
- const activeRuntimes = new Set<McpOAuthRuntime>()
119
-
120
- export function createOAuthRuntime(signal?: AbortSignal): McpOAuthRuntime {
121
- const controller = new AbortController()
122
- const runtime = { signal: combineAbortSignals(signal, controller.signal)! } satisfies McpOAuthRuntime
123
- runtimeStates.set(runtime, {
124
- controller,
125
- generation: 0,
126
- activeOperations: 0,
127
- pendingAuths: new Map(),
128
- pendingAuthStates: new Map(),
129
- pendingAuthCleanupTimers: new Map(),
130
- pendingAuthentications: new Map(),
131
- requests: new Map(),
132
- })
133
- activeRuntimes.add(runtime)
134
- return runtime
135
- }
136
-
137
- let legacyRuntime = createOAuthRuntime()
138
- activeRuntimes.delete(legacyRuntime)
139
-
140
- function getRuntime(options?: AuthenticateOptions): McpOAuthRuntime {
141
- if (options?.runtime) {
142
- options.runtime.signal.throwIfAborted()
143
- activeRuntimes.add(options.runtime)
144
- return options.runtime
145
- }
146
- if (legacyRuntime.signal.aborted) legacyRuntime = createOAuthRuntime()
147
- activeRuntimes.add(legacyRuntime)
148
- return legacyRuntime
149
- }
150
-
151
- function getRuntimeState(runtime: McpOAuthRuntime): RuntimeState {
152
- const state = runtimeStates.get(runtime)
153
- if (!state) throw new Error("Unknown OAuth runtime")
154
- return state
155
- }
156
-
157
- export function holdOAuthCheckpoint(runtime: McpOAuthRuntime, event: McpCheckpointEvent): () => void {
158
- const state = getRuntimeState(runtime)
159
- if (state.checkpoint) throw new Error("MCP OAuth checkpoint is already held")
160
- state.checkpoint = event
161
- return () => { if (state.checkpoint === event) state.checkpoint = undefined }
162
- }
163
-
164
- export function getOAuthCheckpointBlocker(runtime: McpOAuthRuntime): string | undefined {
165
- const state = getRuntimeState(runtime)
166
- if (state.activeOperations || state.pendingAuthentications.size) return "MCP OAuth operation is active"
167
- if (state.pendingAuths.size || state.pendingAuthStates.size || state.pendingAuthCleanupTimers.size) return "MCP browser OAuth callback/flow is pending"
168
- // requests caches server challenges/discovery, not a pending user callback.
169
- // Cold connect re-challenges; granted scopes and issuer bindings live in the
170
- // native credential store. Actual interactive/auth operations veto above.
171
- return undefined
172
- }
173
-
174
- async function withOAuthOperation<T>(runtime: McpOAuthRuntime, run: () => Promise<T>): Promise<T> {
175
- const state = getRuntimeState(runtime)
176
- state.checkpoint?.invalidate()
177
- state.activeOperations++
178
- try { return await run() } finally { state.activeOperations-- }
179
- }
180
-
181
- function getPendingAuthKey(serverName: string, options: AuthStorageOptions): string {
182
- return JSON.stringify([serverName, getAuthBaseDir(options)])
183
- }
184
-
185
- type OAuthRequest = {
186
- serverName: string
187
- serverUrl: string
188
- requestedScope?: string
189
- challenge?: { requiredScope?: string; resourceMetadataUrl?: URL }
190
- issuer?: string
191
- }
192
-
193
- export function isOAuthChallenge(error: unknown): error is InsufficientScopeError | UnauthorizedError | SdkHttpError {
194
- return error instanceof InsufficientScopeError || error instanceof UnauthorizedError
195
- || (error instanceof SdkHttpError && error.status === 401)
196
- }
197
-
198
- export function getOAuthRequest(serverName: string, serverUrl: string, storageBase: string, runtime: McpOAuthRuntime = legacyRuntime): OAuthRequest | undefined {
199
- if (runtime.signal.aborted) return undefined
200
- const request = getRuntimeState(runtime).requests.get(JSON.stringify([serverName, storageBase]))
201
- return request?.serverUrl === serverUrl ? request : undefined
202
- }
203
-
204
- /** Observe native challenges only; no credential reads or auth work here. */
205
- export function recordOAuthChallenge(serverName: string, serverUrl: string, storageBase: string, error: unknown, runtime: McpOAuthRuntime = legacyRuntime): void {
206
- if (runtime.signal.aborted || !isOAuthChallenge(error)) return
207
- getRuntimeState(runtime).checkpoint?.invalidate()
208
- const previous = getOAuthRequest(serverName, serverUrl, storageBase, runtime)
209
- const request: OAuthRequest = { ...previous, serverName, serverUrl }
210
- if (error instanceof InsufficientScopeError) {
211
- const requiredScope = computeScopeUnion(previous?.challenge?.requiredScope, error.requiredScope)
212
- const resourceMetadataUrl = error.resourceMetadataUrl ?? previous?.challenge?.resourceMetadataUrl
213
- request.challenge = { ...(requiredScope ? { requiredScope } : {}), ...(resourceMetadataUrl ? { resourceMetadataUrl } : {}) }
214
- }
215
- getRuntimeState(runtime).requests.set(JSON.stringify([serverName, storageBase]), request)
216
- }
217
-
218
- export function bindOAuthRequestIssuer(serverName: string, serverUrl: string, storageBase: string, issuer: string, runtime: McpOAuthRuntime = legacyRuntime): void {
219
- const request = getOAuthRequest(serverName, serverUrl, storageBase, runtime)
220
- if (!request) return
221
- if (request.issuer && !issuersMatch(request.issuer, issuer)) {
222
- throw new Error(`OAuth authorization server issuer changed for ${serverName}; clear credentials before authenticating again`)
223
- }
224
- request.issuer = issuer
225
- }
226
-
227
- export function hasPendingAuth(serverName: string, options?: AuthStorageOptions, runtime?: McpOAuthRuntime): boolean {
228
- const state = getRuntimeState(runtime ?? legacyRuntime)
229
- if (options) {
230
- return state.pendingAuths.has(getPendingAuthKey(serverName, options))
231
- }
232
- return Array.from(state.pendingAuths.values()).some(pendingAuth => pendingAuth.serverName === serverName)
233
- }
234
-
235
- /** Timeout for manual auth completion (5 minutes) */
236
- const MANUAL_AUTH_TIMEOUT_MS = 5 * 60 * 1000
237
-
238
- /**
239
- * Generate a cryptographically secure random state parameter.
240
- */
241
- function generateState(): string {
242
- return Array.from(crypto.getRandomValues(new Uint8Array(32)))
243
- .map((b) => b.toString(16).padStart(2, "0"))
244
- .join("")
245
- }
246
-
247
- /**
248
- * Extract OAuth configuration from a ServerEntry.
249
- */
250
- export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
251
- validateServerProtocolConfig(definition)
252
- if (definition.oauth === false) {
253
- return {}
254
- }
255
-
256
- const config: McpOAuthConfig = {}
257
- if (definition.oauth?.skipIssuerMetadataValidation !== undefined) config.skipIssuerMetadataValidation = definition.oauth.skipIssuerMetadataValidation
258
- if (definition.oauth?.grantType !== undefined) config.grantType = definition.oauth.grantType
259
- if (definition.oauth?.clientId !== undefined) {
260
- if (typeof definition.oauth.clientId !== "string") throw new Error("OAuth clientId must be a string")
261
- config.clientId = interpolateEnvVars(definition.oauth.clientId)
262
- }
263
- if (definition.oauth?.clientSecret !== undefined) {
264
- if (typeof definition.oauth.clientSecret !== "string") throw new Error("OAuth clientSecret must be a string")
265
- // Preserve command expressions for the provider; interpolation remains eager for ordinary values.
266
- config.clientSecret = definition.oauth.clientSecret.startsWith("!")
267
- ? definition.oauth.clientSecret
268
- : interpolateEnvVars(definition.oauth.clientSecret)
269
- }
270
- if (definition.oauth?.clientMetadataUrl !== undefined) {
271
- const value = definition.oauth.clientMetadataUrl
272
- if (value !== false && typeof value !== "string") throw new Error("OAuth clientMetadataUrl must be a string or false")
273
- config.clientMetadataUrl = value === false ? false : interpolateEnvVars(value)
274
- validateOAuthClientMetadataUrl(config.clientMetadataUrl)
275
- }
276
- if (definition.oauth?.privateKeyJwt !== undefined) {
277
- // Keep environment and command sources lazy; each authentication resolves its own key.
278
- config.privateKeyJwt = definition.oauth.privateKeyJwt
279
- }
280
- if (definition.oauth?.crossAppAccess !== undefined) config.crossAppAccess = definition.oauth.crossAppAccess
281
- validateOAuthPrivateKeyJwt(config)
282
- validateOAuthCrossAppAccess(config)
283
- if (definition.oauth?.scope !== undefined) {
284
- if (typeof definition.oauth.scope !== "string") throw new Error("OAuth scope must be a string")
285
- config.scope = interpolateEnvVars(definition.oauth.scope)
286
- }
287
- if (definition.oauth?.authorizationParams !== undefined) {
288
- const params = definition.oauth.authorizationParams
289
- if (!params || typeof params !== "object" || Array.isArray(params)) {
290
- throw new Error("OAuth authorizationParams must be an object")
291
- }
292
- config.authorizationParams = {}
293
- for (const [key, value] of Object.entries(params)) {
294
- if (!key) throw new Error("OAuth authorizationParams keys must not be empty")
295
- if (typeof value !== "string") throw new Error(`OAuth authorizationParams.${key} must be a string`)
296
- config.authorizationParams[key] = interpolateEnvVars(value)
297
- }
298
- }
299
- if (definition.oauth?.redirectUri !== undefined) {
300
- if (typeof definition.oauth.redirectUri !== "string") {
301
- throw new Error("OAuth redirectUri must be a string")
302
- }
303
- const redirectUri = interpolateEnvVars(definition.oauth.redirectUri).trim()
304
- if (!redirectUri) {
305
- throw new Error("OAuth redirectUri must not be empty")
306
- }
307
- config.redirectUri = redirectUri
308
- }
309
- if (definition.oauth?.clientName !== undefined) {
310
- if (typeof definition.oauth.clientName !== "string") {
311
- throw new Error("OAuth clientName must be a string")
312
- }
313
- const clientName = interpolateEnvVars(definition.oauth.clientName).trim()
314
- if (!clientName) {
315
- throw new Error("OAuth clientName must not be empty")
316
- }
317
- config.clientName = clientName
318
- }
319
- if (definition.oauth?.clientUri !== undefined) {
320
- if (typeof definition.oauth.clientUri !== "string") {
321
- throw new Error("OAuth clientUri must be a string")
322
- }
323
- const clientUri = interpolateEnvVars(definition.oauth.clientUri).trim()
324
- if (!clientUri) {
325
- throw new Error("OAuth clientUri must not be empty")
326
- }
327
- config.clientUri = clientUri
328
- }
329
- return config
330
- }
331
-
332
- async function probeAuthDiscovery(serverUrl: string, definition?: ServerEntry, signal?: AbortSignal): Promise<AuthDiscovery> {
333
- // Discovery must not execute config commands or send their source text.
334
- const discoveryHeaders = definition?.headers
335
- ? Object.fromEntries(Object.entries(definition.headers).filter(([, value]) => !value.startsWith("!") || value.startsWith("!!")))
336
- : undefined
337
- const headers = new Headers(interpolateEnvRecord(discoveryHeaders))
338
- const timeout = normalizeRequestTimeoutMs(definition?.requestTimeoutMs) ?? 5000
339
- const discoverySignal = combineAbortSignals(signal, AbortSignal.timeout(Math.ceil(timeout)))
340
- let discovery: AuthDiscovery = {}
341
- const client = new Client({ name: "pi-mcp-auth-discovery", version: "4.3.1" }, {
342
- versionNegotiation: { mode: definition?.protocolVersion ?? "auto" },
343
- ...(isNonInteractiveOAuth(definition?.oauth)
344
- ? { capabilities: { extensions: { [definition?.oauth && definition.oauth.crossAppAccess
345
- ? "io.modelcontextprotocol/enterprise-managed-authorization" : "io.modelcontextprotocol/oauth-client-credentials"]: {} } } } : {}),
346
- })
347
- const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
348
- requestInit: { headers },
349
- fetch: async (input, init) => {
350
- const response = await fetch(input, init)
351
- const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response)
352
- discovery = { ...(resourceMetadataUrl ? { resourceMetadataUrl } : {}), ...(scope ? { scope } : {}) }
353
- return response
354
- },
355
- })
356
- try {
357
- await abortable(client.connect(transport, { timeout }), discoverySignal)
358
- } catch (error) {
359
- if (signal?.aborted) throwIfAborted(signal)
360
- } finally {
361
- await (client.transport ? client.close() : transport.close())
362
- }
363
- return discovery
364
- }
365
-
366
- function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHost: string; callbackPath: string } {
367
- let url: URL
368
- try {
369
- url = new URL(redirectUri)
370
- } catch (error) {
371
- throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error })
372
- }
373
-
374
- const hostname = url.hostname.toLowerCase()
375
- const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
376
- if (url.protocol !== "http:" || !isLocalhost) {
377
- throw new Error("OAuth redirectUri must be an http:// localhost or loopback URI")
378
- }
379
-
380
- if (url.username || url.password) {
381
- throw new Error("OAuth redirectUri must not include username or password")
382
- }
383
-
384
- if (url.hash) {
385
- throw new Error("OAuth redirectUri must not include a fragment")
386
- }
387
-
388
- if (!url.port) {
389
- throw new Error("OAuth redirectUri must include an explicit numeric port")
390
- }
391
-
392
- const port = Number.parseInt(url.port, 10)
393
- if (!Number.isInteger(port) || port <= 0 || port > 65535) {
394
- throw new Error("OAuth redirectUri must include an explicit numeric port")
395
- }
396
-
397
- const callbackHost = hostname === "[::1]" ? "::1" : hostname
398
- return { port, callbackHost, callbackPath: url.pathname }
399
- }
400
-
401
- /**
402
- * Start OAuth authentication flow for a server.
403
- * Returns the authorization URL when browser authorization is required.
404
- */
405
- export const startAuth: typeof startAuthOperation = async (...args) =>
406
- withOAuthOperation(getRuntime(args[3]), () => startAuthOperation(...args))
407
-
408
- async function startAuthOperation(
409
- serverName: string,
410
- serverUrl: string,
411
- definition?: ServerEntry,
412
- options: AuthenticateOptions = {},
413
- ): Promise<{ authorizationUrl: string }> {
414
- if (isServerDisabled(definition)) throw new Error(`MCP server "${serverName}" is disabled`)
415
- const runtime = getRuntime(options)
416
- const runtimeState = getRuntimeState(runtime)
417
- const config = definition ? extractOAuthConfig(definition) : {}
418
- const authStorageOptions = options.authStorageOptions ?? {}
419
- const signal = combineAbortSignals(runtime.signal, options.signal)
420
- const generation = runtimeState.generation
421
- throwIfAborted(signal)
422
-
423
- if (isNonInteractiveOAuth(config)) {
424
- const storedAuth = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
425
- if (!config.crossAppAccess && storedAuth?.clientInfo && !storedAuth.tokens && !config.clientId) {
426
- clearClientInfo(serverName, authStorageOptions)
427
- clearCodeVerifier(serverName, authStorageOptions)
428
- await clearOAuthState(serverName, authStorageOptions)
429
- }
430
-
431
- const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
432
- onRedirect: async () => {
433
- throw new Error("Browser redirect is not used for noninteractive OAuth flows")
434
- },
435
- }, authStorageOptions, signal)
436
- try {
437
- const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
438
- throwIfAborted(signal)
439
- const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery, fetchFn: authProvider.fetch }), signal)
440
- throwIfAborted(signal)
441
- if (result !== "AUTHORIZED") {
442
- throw new UnauthorizedError("Failed to authorize")
443
- }
444
- return { authorizationUrl: "" }
445
- } finally {
446
- authProvider.deactivate()
447
- }
448
- }
449
-
450
- const storageBase = getAuthBaseDir(authStorageOptions)
451
- const request = getOAuthRequest(serverName, serverUrl, storageBase, runtime)
452
- const existingPendingAuth = runtimeState.pendingAuths.get(getPendingAuthKey(serverName, authStorageOptions))
453
- if (existingPendingAuth?.serverUrl === serverUrl) {
454
- return { authorizationUrl: existingPendingAuth.authorizationUrl }
455
- }
456
-
457
- const redirectCallback = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
458
- const oauthState = generateState()
459
- const callbackIssuer: CallbackIssuer = {}
460
-
461
- try {
462
- await ensureCallbackServer({
463
- strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
464
- oauthState,
465
- reserveState: true,
466
- validate: response => validateCallbackIssuer(serverName, callbackIssuer, response),
467
- ...(redirectCallback ? { port: redirectCallback.port, callbackHost: redirectCallback.callbackHost, callbackPath: redirectCallback.callbackPath } : {}),
468
- })
469
- throwIfAborted(signal)
470
- } catch (error) {
471
- releaseCallbackServer(oauthState)
472
- try {
473
- await clearOAuthState(serverName, authStorageOptions)
474
- } catch (cleanupError) {
475
- throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
476
- }
477
- throw error
478
- }
479
-
480
- let capturedUrl: URL | undefined
481
- let authProvider: McpOAuthProvider | undefined
482
- try {
483
- const storedAuth = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
484
- let discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
485
- if (request?.challenge) {
486
- const scope = computeScopeUnion(discovery.scope, request.requestedScope, storedAuth?.tokens?.scope, request.challenge.requiredScope)
487
- discovery = { ...discovery, ...(scope ? { scope } : {}), ...(request.challenge.resourceMetadataUrl ? { resourceMetadataUrl: request.challenge.resourceMetadataUrl } : {}) }
488
- }
489
- throwIfAborted(signal)
490
- const flowConfig = { ...config, ...(request?.challenge && discovery.scope ? { scope: discovery.scope } : {}) }
491
- authProvider = new McpOAuthProvider(serverName, serverUrl, flowConfig, {
492
- onRedirect: url => {
493
- capturedUrl = url
494
- const requestedScope = url.searchParams.get("scope") ?? undefined
495
- if (requestedScope) {
496
- discovery = { ...discovery, scope: requestedScope }
497
- if (request?.challenge) flowConfig.scope = requestedScope
498
- }
499
- const current = getOAuthRequest(serverName, serverUrl, storageBase, runtime)
500
- const scope = computeScopeUnion(current?.requestedScope, requestedScope)
501
- if (current && scope) current.requestedScope = scope
502
- },
503
- onDiscoveryState: state => {
504
- callbackIssuer.expectedIssuer = state.authorizationServerMetadata?.issuer
505
- callbackIssuer.fallbackIssuer = state.authorizationServerUrl
506
- callbackIssuer.issParameterSupported = state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
507
- bindOAuthRequestIssuer(serverName, serverUrl, storageBase, callbackIssuer.expectedIssuer ?? state.authorizationServerUrl, runtime)
508
- },
509
- }, authStorageOptions, runtime.signal, oauthState)
510
- if (storedAuth?.clientInfo && !config.clientId) {
511
- if (!storedAuth.tokens) {
512
- clearClientInfo(serverName, authStorageOptions)
513
- clearCodeVerifier(serverName, authStorageOptions)
514
- await clearOAuthState(serverName, authStorageOptions)
515
- } else {
516
- const redirectUris = storedAuth.clientInfo.redirectUris
517
- const redirectUrl = authProvider.redirectUrl ?? ""
518
- const matchesRedirect = Array.isArray(redirectUris) && (redirectUris.includes(redirectUrl)
519
- || (storedAuth.clientInfo.registrationType === "cimd"
520
- && redirectUris.some(uri => loopbackRedirectsMatch(redirectUrl, uri))))
521
- if (!matchesRedirect) {
522
- clearClientInfo(serverName, authStorageOptions)
523
- clearTokens(serverName, authStorageOptions)
524
- clearCodeVerifier(serverName, authStorageOptions)
525
- await clearOAuthState(serverName, authStorageOptions)
526
- }
527
- }
528
- }
529
-
530
- throwIfAborted(signal)
531
-
532
- const result = await abortable(runSdkAuth(authProvider, {
533
- serverUrl, ...discovery,
534
- ...(request?.challenge ? { forceReauthorization: isStrictScopeSuperset(discovery.scope, storedAuth?.tokens?.scope) } : {}),
535
- }), signal)
536
- throwIfAborted(signal)
537
- if (result === "AUTHORIZED") {
538
- authProvider.deactivate()
539
- releaseCallbackServer(oauthState)
540
- await clearOAuthState(serverName, authStorageOptions)
541
- return { authorizationUrl: "" }
542
- }
543
- if (!capturedUrl) {
544
- throw new UnauthorizedError("OAuth authorization URL was not provided")
545
- }
546
- await setPendingAuth(runtime, serverName, { serverName, authProvider, serverUrl, authorizationUrl: capturedUrl.toString(), discovery, callbackIssuer: { ...callbackIssuer }, authStorageOptions, ...(request ? { request } : {}) }, oauthState, signal, generation)
547
- return { authorizationUrl: capturedUrl.toString() }
548
- } catch (error) {
549
- authProvider?.deactivate()
550
- try {
551
- await clearPendingAuth(runtime, serverName, oauthState, authStorageOptions)
552
- } catch (cleanupError) {
553
- throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
554
- }
555
- throw error
556
- }
557
- }
558
-
559
- async function setPendingAuth(
560
- runtime: McpOAuthRuntime,
561
- serverName: string,
562
- pendingAuth: PendingAuth,
563
- oauthState: string,
564
- signal?: AbortSignal,
565
- generation = getRuntimeState(runtime).generation,
566
- ): Promise<void> {
567
- const state = getRuntimeState(runtime)
568
- const key = getPendingAuthKey(serverName, pendingAuth.authStorageOptions)
569
- await clearPendingAuth(runtime, serverName, undefined, pendingAuth.authStorageOptions)
570
- throwIfAborted(signal)
571
- if (generation !== state.generation) throw new Error("OAuth runtime stopped")
572
- state.pendingAuths.set(key, pendingAuth)
573
- state.pendingAuthStates.set(key, oauthState)
574
- const cleanupTimer = setTimeout(() => {
575
- void clearPendingAuth(runtime, serverName, oauthState, pendingAuth.authStorageOptions).catch(error => {
576
- console.error(`MCP Auth: Timed-out flow cleanup failed: ${formatTerminalError(error)}`)
577
- })
578
- }, MANUAL_AUTH_TIMEOUT_MS)
579
- cleanupTimer.unref?.()
580
- state.pendingAuthCleanupTimers.set(key, cleanupTimer)
581
- }
582
-
583
- function clearPendingAuth(...args: Parameters<typeof clearPendingAuthOperation>): Promise<void> {
584
- return withOAuthOperation(args[0], () => clearPendingAuthOperation(...args))
585
- }
586
-
587
- async function clearPendingAuthOperation(runtime: McpOAuthRuntime, serverName: string, oauthState?: string, fallbackStorageOptions: AuthStorageOptions = {}): Promise<void> {
588
- const state = getRuntimeState(runtime)
589
- const key = getPendingAuthKey(serverName, fallbackStorageOptions)
590
- const pendingAuth = state.pendingAuths.get(key)
591
- const authStorageOptions = pendingAuth?.authStorageOptions ?? fallbackStorageOptions
592
- const pendingState = state.pendingAuthStates.get(key)
593
- if (oauthState && pendingState && pendingState !== oauthState) return
594
-
595
- const timer = state.pendingAuthCleanupTimers.get(key)
596
- if (timer) {
597
- clearTimeout(timer)
598
- state.pendingAuthCleanupTimers.delete(key)
599
- }
600
-
601
- pendingAuth?.authProvider.deactivate()
602
- state.pendingAuths.delete(key)
603
- state.pendingAuthStates.delete(key)
604
- const stateToRelease = pendingState ?? oauthState
605
- if (stateToRelease) {
606
- cancelPendingCallback(stateToRelease)
607
- const storedState = await getOAuthState(serverName, authStorageOptions)
608
- if (storedState === stateToRelease) {
609
- await clearOAuthState(serverName, authStorageOptions)
610
- }
611
- }
612
- }
613
-
614
- function getSearchParamsFromInput(input: string): URLSearchParams | undefined {
615
- try {
616
- const url = new URL(input)
617
- const params = new URLSearchParams(url.search)
618
- if (url.hash) {
619
- const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash
620
- const hashParams = new URLSearchParams(hash)
621
- for (const [key, value] of hashParams) {
622
- if (!params.has(key)) params.set(key, value)
623
- }
624
- }
625
- return params
626
- } catch {
627
- const query = input.includes("?") ? input.slice(input.indexOf("?") + 1) : input
628
- const params = new URLSearchParams(query.startsWith("#") ? query.slice(1) : query)
629
- return params.has("code") || params.has("state") || params.has("error") ? params : undefined
630
- }
631
- }
632
-
633
- /** Authorization code plus the optional RFC 9207 `iss` callback parameter. */
634
- export interface AuthorizationCodeInput {
635
- code: string
636
- iss?: string
637
- }
638
-
639
- /**
640
- * Extract an OAuth authorization code (and the RFC 9207 `iss` parameter, when
641
- * present) from either a raw code, a query string, or the full localhost
642
- * redirect URL copied from the browser address bar.
643
- */
644
- type AuthorizationResponseInput = AuthorizationCodeInput | { error: string; errorDescription?: string; iss?: string; code?: never }
645
-
646
- export function parseAuthorizationRedirectInput(input: string, expectedState?: string): AuthorizationResponseInput {
647
- const trimmed = input.trim()
648
- if (!trimmed) {
649
- throw new Error("Authorization code or redirect URL is required")
650
- }
651
-
652
- const params = getSearchParamsFromInput(trimmed)
653
- if (params) {
654
- const state = params.get("state")
655
- if (expectedState && !state) {
656
- throw new Error("OAuth state missing from redirect URL")
657
- }
658
- if (expectedState && state !== expectedState) {
659
- throw new Error("OAuth state mismatch - potential CSRF attack")
660
- }
661
-
662
- const error = params.get("error")
663
- if (error) {
664
- const description = params.get("error_description")
665
- const iss = params.get("iss")
666
- return { error, ...(description !== null ? { errorDescription: description } : {}), ...(iss !== null ? { iss } : {}) }
667
- }
668
- const code = params.get("code")
669
- if (code) {
670
- const iss = params.get("iss")
671
- return { code, ...(iss !== null ? { iss } : {}) }
672
- }
673
- }
674
-
675
- if (/^[A-Za-z0-9._~+/=-]+$/.test(trimmed)) {
676
- return { code: trimmed }
677
- }
678
-
679
- throw new Error("Could not find an OAuth authorization code in the provided input")
680
- }
681
-
682
- /**
683
- * Complete OAuth authentication from pasted input or a captured browser callback.
684
- */
685
- export async function completeAuthFromInput(
686
- serverName: string,
687
- input?: string,
688
- options: AuthenticateOptions = {},
689
- ): Promise<AuthStatus> {
690
- const runtime = getRuntime(options)
691
- const runtimeState = getRuntimeState(runtime)
692
- const fallbackAuthStorageOptions = options.authStorageOptions ?? {}
693
- const signal = combineAbortSignals(runtime.signal, options.signal)
694
- throwIfAborted(signal)
695
- const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
696
- const oauthState = runtimeState.pendingAuthStates.get(key)
697
- throwIfAborted(signal)
698
- const parsed = input === undefined ? undefined : parseAuthorizationRedirectInput(input, oauthState)
699
- return completeAuth(serverName, parsed, options)
700
- }
701
-
702
- /**
703
- * Complete OAuth authentication with an explicit code or a captured callback.
704
- */
705
- export const completeAuth: typeof completeAuthOperation = async (...args) =>
706
- withOAuthOperation(getRuntime(args[2]), () => completeAuthOperation(...args))
707
-
708
- async function completeAuthOperation(
709
- serverName: string,
710
- authorizationCode?: string | AuthorizationResponseInput,
711
- options: AuthenticateOptions = {},
712
- ): Promise<AuthStatus> {
713
- const runtime = getRuntime(options)
714
- const runtimeState = getRuntimeState(runtime)
715
- const fallbackAuthStorageOptions = options.authStorageOptions ?? {}
716
- const signal = combineAbortSignals(runtime.signal, options.signal)
717
- throwIfAborted(signal)
718
- const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
719
- const pendingAuth = runtimeState.pendingAuths.get(key)
720
- const authStorageOptions = pendingAuth?.authStorageOptions ?? fallbackAuthStorageOptions
721
- if (!pendingAuth) {
722
- throw new Error(`No pending OAuth flow for server: ${serverName}. Run auth-start for this server first.`)
723
- }
724
-
725
- const oauthState = runtimeState.pendingAuthStates.get(key)
726
- throwIfAborted(signal)
727
- const response: AuthorizationResponseInput | undefined = typeof authorizationCode === "string" ? { code: authorizationCode }
728
- : authorizationCode ?? (oauthState ? takeCallbackResult(oauthState) : undefined)
729
- if (!response) {
730
- throw new Error(
731
- `No OAuth callback received yet for ${serverName}. Finish authorization in the browser, then retry auth-complete. ` +
732
- "If the browser cannot reach this Pi session's callback, pass args: { redirectUrl: \"FULL_REDIRECT_URL\" } instead.",
733
- )
734
- }
735
- const { iss } = response
736
-
737
- let keepPendingForRetry = false
738
- let caughtError: unknown
739
- try {
740
- keepPendingForRetry = iss === undefined && pendingAuth.callbackIssuer.issParameterSupported === true
741
- validateCallbackIssuer(serverName, pendingAuth.callbackIssuer, response)
742
- if ("error" in response) {
743
- keepPendingForRetry = true
744
- throw new Error(response.errorDescription ? `${response.error}: ${response.errorDescription}` : response.error)
745
- }
746
-
747
- const result = await abortable(runSdkAuth(pendingAuth.authProvider, {
748
- serverUrl: pendingAuth.serverUrl,
749
- authorizationCode: response.code,
750
- ...(iss !== undefined ? { iss } : {}),
751
- ...pendingAuth.discovery,
752
- }), signal)
753
- throwIfAborted(signal)
754
- if (result !== "AUTHORIZED") {
755
- throw new UnauthorizedError("Failed to authorize")
756
- }
757
- const request = pendingAuth.request
758
- if (request?.challenge?.requiredScope
759
- && request === getOAuthRequest(serverName, pendingAuth.serverUrl, getAuthBaseDir(authStorageOptions), runtime)) {
760
- const issuedScope = (await getAuthForUrl(serverName, pendingAuth.serverUrl, authStorageOptions))?.tokens?.scope
761
- if (issuedScope !== undefined && !isStrictScopeSuperset(request.challenge.requiredScope, issuedScope)) delete request.challenge
762
- }
763
- return "authenticated"
764
- } catch (error) {
765
- caughtError = error
766
- throw error
767
- } finally {
768
- if (!keepPendingForRetry) {
769
- try {
770
- await clearPendingAuth(runtime, serverName, oauthState, authStorageOptions)
771
- } catch (cleanupError) {
772
- if (caughtError !== undefined) {
773
- throw new AggregateError([caughtError, cleanupError], "OAuth completion cleanup failed")
774
- }
775
- throw cleanupError
776
- }
777
- }
778
- }
779
- }
780
-
781
- /**
782
- * Perform the complete OAuth authentication flow for a server.
783
- *
784
- * @param serverName - The name of the MCP server
785
- * @param serverUrl - The URL of the MCP server
786
- * @param definition - The server definition (optional)
787
- * @returns The final auth status
788
- */
789
- export async function authenticate(
790
- serverName: string,
791
- serverUrl: string,
792
- definition?: ServerEntry,
793
- options: AuthenticateOptions = {},
794
- ): Promise<AuthStatus> {
795
- if (isServerDisabled(definition)) throw new Error(`MCP server "${serverName}" is disabled`)
796
- const runtime = getRuntime(options)
797
- const runtimeState = getRuntimeState(runtime)
798
- const authStorageOptions = options.authStorageOptions ?? {}
799
- const signal = combineAbortSignals(runtime.signal, options.signal)
800
- throwIfAborted(signal)
801
- const authKey = JSON.stringify([serverName, serverUrl, getAuthBaseDir(authStorageOptions)])
802
- const inFlight = runtimeState.pendingAuthentications.get(authKey)
803
- if (inFlight) {
804
- return inFlight
805
- }
806
-
807
- const operation = (async (): Promise<AuthStatus> => {
808
- // Start auth flow
809
- const { authorizationUrl } = await startAuth(serverName, serverUrl, definition, {
810
- ...options,
811
- ...(signal ? { signal } : {}),
812
- runtime,
813
- })
814
-
815
- // If no auth URL needed, already authenticated
816
- if (!authorizationUrl) {
817
- return "authenticated"
818
- }
819
-
820
- let oauthState: string | undefined
821
- try {
822
- // Get the state that was already generated and stored in startAuth().
823
- // Keep this lookup and its abort check inside the cleanup boundary because
824
- // startAuth has already reserved callback state at this point.
825
- oauthState = runtimeState.pendingAuthStates.get(getPendingAuthKey(serverName, authStorageOptions))
826
- throwIfAborted(signal)
827
- if (!oauthState) {
828
- throw new Error("OAuth state not found - this should not happen")
829
- }
830
-
831
- // Register the callback BEFORE opening the browser.
832
- const callbackPromise = waitForCallback(oauthState)
833
- void callbackPromise.catch(() => {})
834
-
835
- // Open browser. Always surface the URL first so remote/headless users can copy it
836
- // even when the OS browser handoff is unavailable or invisible.
837
- if (options.onAuthorizationUrl) {
838
- await abortable(Promise.resolve(options.onAuthorizationUrl(authorizationUrl)), signal)
839
- } else {
840
- console.log(`MCP Auth: Open this URL to authenticate ${serverName}:\n${authorizationUrl}`)
841
- }
842
- try {
843
- await abortable(open(authorizationUrl), signal)
844
- } catch (error) {
845
- if (isAbortError(error, signal)) throw error
846
- console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
847
- }
848
-
849
- // Wait for callback
850
- const callbackResult = await abortable(callbackPromise, signal)
851
-
852
- // The callback server accepted only the flow-local reserved state.
853
- throwIfAborted(signal)
854
-
855
- // Complete the auth
856
- return await completeAuth(serverName, callbackResult, {
857
- ...options,
858
- ...(signal ? { signal } : {}),
859
- runtime,
860
- })
861
- } catch (error) {
862
- if (oauthState) cancelPendingCallback(oauthState)
863
- try {
864
- await clearPendingAuth(runtime, serverName, oauthState, authStorageOptions)
865
- } catch (cleanupError) {
866
- throw new AggregateError([error, cleanupError], "OAuth cancellation cleanup failed")
867
- }
868
- throw error
869
- }
870
- })()
871
-
872
- runtimeState.pendingAuthentications.set(authKey, operation)
873
-
874
- try {
875
- return await operation
876
- } finally {
877
- if (runtimeState.pendingAuthentications.get(authKey) === operation) {
878
- runtimeState.pendingAuthentications.delete(authKey)
879
- }
880
- }
881
- }
882
-
883
- /**
884
- * Remove all OAuth credentials for a server.
885
- *
886
- * @param serverName - The name of the MCP server
887
- */
888
- export const removeAuth: typeof removeAuthOperation = async (...args) =>
889
- withOAuthOperation(getRuntime(args[1]), () => removeAuthOperation(...args))
890
-
891
- async function removeAuthOperation(serverName: string, options: AuthenticateOptions = {}): Promise<void> {
892
- const runtime = getRuntime(options)
893
- const signal = combineAbortSignals(runtime.signal, options.signal)
894
- throwIfAborted(signal)
895
- const authStorageOptions = options.authStorageOptions ?? {}
896
- const oauthState = await getOAuthState(serverName, authStorageOptions)
897
- throwIfAborted(signal)
898
- if (oauthState) {
899
- cancelPendingCallback(oauthState)
900
- }
901
- await clearPendingAuth(runtime, serverName, oauthState, authStorageOptions)
902
- throwIfAborted(signal)
903
- for (const [key, request] of getRuntimeState(runtime).requests) {
904
- if (request.serverName === serverName) getRuntimeState(runtime).requests.delete(key)
905
- }
906
- clearAllCredentials(serverName, authStorageOptions)
907
- await clearOAuthState(serverName, authStorageOptions)
908
- throwIfAborted(signal)
909
- console.log(`MCP Auth: Removed credentials for ${serverName}`)
910
- }
911
-
912
- /**
913
- * Check if OAuth is supported for a server configuration.
914
- * OAuth is supported for HTTP servers unless explicitly disabled.
915
- *
916
- * @param definition - The server definition
917
- * @returns True if OAuth is supported
918
- */
919
- export function supportsOAuth(definition: ServerEntry): boolean {
920
- // OAuth requires a URL
921
- if (!definition.url) return false
922
-
923
- // Explicitly disabled via auth: false or oauth: false
924
- if (definition.auth === false) return false
925
- if (definition.oauth === false) return false
926
- if (definition.auth === "oauth") return true
927
-
928
- // Configured custom headers take precedence over implicit OAuth auto-detection.
929
- if (definition.headers && Object.keys(definition.headers).length > 0) return false
930
-
931
- // OAuth is enabled when auth is not specified (auto-detect)
932
- return definition.auth === undefined
933
- }
934
-
935
- /**
936
- * Initialize the OAuth system on startup.
937
- * OAuth callback binding is lazy and starts from startAuth() only.
938
- */
939
- export async function initializeOAuth(
940
- runtimeOrSignal?: McpOAuthRuntime | AbortSignal,
941
- ): Promise<McpOAuthRuntime> {
942
- if (runtimeOrSignal && "signal" in runtimeOrSignal) {
943
- runtimeOrSignal.signal.throwIfAborted()
944
- activeRuntimes.add(runtimeOrSignal)
945
- return runtimeOrSignal
946
- }
947
-
948
- await shutdownOAuth(legacyRuntime)
949
- legacyRuntime = createOAuthRuntime(runtimeOrSignal as AbortSignal | undefined)
950
- return legacyRuntime
951
- }
952
-
953
- /**
954
- * Shutdown one OAuth runtime. The callback server remains process-shared while
955
- * another runtime has pending/reserved callback state or is still active.
956
- */
957
- export async function shutdownOAuth(runtime: McpOAuthRuntime = legacyRuntime): Promise<void> {
958
- const state = getRuntimeState(runtime)
959
- if (state.controller.signal.aborted) return
960
- state.checkpoint?.invalidate()
961
- state.generation += 1
962
- state.controller.abort(new Error("OAuth runtime stopped"))
963
- for (const callbackState of Array.from(state.pendingAuthStates.values())) cancelPendingCallback(callbackState)
964
- const results = await Promise.allSettled(Array.from(state.pendingAuths.values(), pendingAuth =>
965
- clearPendingAuth(runtime, pendingAuth.serverName, undefined, pendingAuth.authStorageOptions)))
966
- const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : [])
967
- state.pendingAuthentications.clear()
968
- state.requests.clear()
969
- activeRuntimes.delete(runtime)
970
-
971
- if (activeRuntimes.size === 0) {
972
- try { await stopCallbackServer() } catch (error) { failures.push(error) }
973
- }
974
- if (failures.length) throw new AggregateError(failures, "MCP OAuth cleanup failed")
975
- }