@njuptlzf/mcp-adapter 2.29.0-0.0.1

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