pi-soly 1.9.3 → 1.11.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 (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
@@ -0,0 +1,559 @@
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
+ UnauthorizedError,
10
+ } from "@modelcontextprotocol/sdk/client/auth.js"
11
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
12
+ import open from "open"
13
+ import { McpOAuthProvider, type McpOAuthConfig } from "./mcp-oauth-provider.ts"
14
+ import {
15
+ ensureCallbackServer,
16
+ waitForCallback,
17
+ cancelPendingCallback,
18
+ stopCallbackServer,
19
+ releaseCallbackServer,
20
+ } from "./mcp-callback-server.ts"
21
+ import {
22
+ getAuthForUrl,
23
+ isTokenExpired,
24
+ hasStoredTokens,
25
+ clearAllCredentials,
26
+ clearClientInfo,
27
+ clearTokens,
28
+ clearCodeVerifier,
29
+ updateOAuthState,
30
+ getOAuthState,
31
+ clearOAuthState,
32
+ type StoredTokens,
33
+ } from "./mcp-auth.ts"
34
+ import type { ServerEntry } from "./types.ts"
35
+
36
+ /** Auth status for a server */
37
+ export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
38
+
39
+ // Track pending transports for auth completion
40
+ const pendingTransports = new Map<string, StreamableHTTPClientTransport>()
41
+ const pendingAuthStates = new Map<string, string>()
42
+ const pendingAuthCleanupTimers = new Map<string, ReturnType<typeof setTimeout>>()
43
+
44
+ // Deduplicate concurrent authenticate() calls per server.
45
+ const pendingAuthentications = new Map<string, Promise<AuthStatus>>()
46
+
47
+ /** Timeout for manual auth completion (5 minutes) */
48
+ const MANUAL_AUTH_TIMEOUT_MS = 5 * 60 * 1000
49
+
50
+ /**
51
+ * Generate a cryptographically secure random state parameter.
52
+ */
53
+ function generateState(): string {
54
+ return Array.from(crypto.getRandomValues(new Uint8Array(32)))
55
+ .map((b) => b.toString(16).padStart(2, "0"))
56
+ .join("")
57
+ }
58
+
59
+ /**
60
+ * Extract OAuth configuration from a ServerEntry.
61
+ */
62
+ export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
63
+ if (definition.oauth === false) {
64
+ return {}
65
+ }
66
+
67
+ const config: McpOAuthConfig = {}
68
+ if (definition.oauth?.grantType !== undefined) config.grantType = definition.oauth.grantType
69
+ if (definition.oauth?.clientId !== undefined) config.clientId = definition.oauth.clientId
70
+ if (definition.oauth?.clientSecret !== undefined) config.clientSecret = definition.oauth.clientSecret
71
+ if (definition.oauth?.scope !== undefined) config.scope = definition.oauth.scope
72
+ if (definition.oauth?.redirectUri !== undefined) {
73
+ if (typeof definition.oauth.redirectUri !== "string") {
74
+ throw new Error("OAuth redirectUri must be a string")
75
+ }
76
+ const redirectUri = definition.oauth.redirectUri.trim()
77
+ if (!redirectUri) {
78
+ throw new Error("OAuth redirectUri must not be empty")
79
+ }
80
+ config.redirectUri = redirectUri
81
+ }
82
+ if (definition.oauth?.clientName !== undefined) {
83
+ if (typeof definition.oauth.clientName !== "string") {
84
+ throw new Error("OAuth clientName must be a string")
85
+ }
86
+ const clientName = definition.oauth.clientName.trim()
87
+ if (!clientName) {
88
+ throw new Error("OAuth clientName must not be empty")
89
+ }
90
+ config.clientName = clientName
91
+ }
92
+ if (definition.oauth?.clientUri !== undefined) {
93
+ if (typeof definition.oauth.clientUri !== "string") {
94
+ throw new Error("OAuth clientUri must be a string")
95
+ }
96
+ const clientUri = definition.oauth.clientUri.trim()
97
+ if (!clientUri) {
98
+ throw new Error("OAuth clientUri must not be empty")
99
+ }
100
+ config.clientUri = clientUri
101
+ }
102
+ return config
103
+ }
104
+
105
+ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHost: string; callbackPath: string } {
106
+ let url: URL
107
+ try {
108
+ url = new URL(redirectUri)
109
+ } catch (error) {
110
+ throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error })
111
+ }
112
+
113
+ const hostname = url.hostname.toLowerCase()
114
+ const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
115
+ if (url.protocol !== "http:" || !isLocalhost) {
116
+ throw new Error("OAuth redirectUri must be an http:// localhost or loopback URI")
117
+ }
118
+
119
+ if (url.username || url.password) {
120
+ throw new Error("OAuth redirectUri must not include username or password")
121
+ }
122
+
123
+ if (url.hash) {
124
+ throw new Error("OAuth redirectUri must not include a fragment")
125
+ }
126
+
127
+ if (!url.port) {
128
+ throw new Error("OAuth redirectUri must include an explicit numeric port")
129
+ }
130
+
131
+ const port = Number.parseInt(url.port, 10)
132
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
133
+ throw new Error("OAuth redirectUri must include an explicit numeric port")
134
+ }
135
+
136
+ const callbackHost = hostname === "[::1]" ? "::1" : hostname
137
+ return { port, callbackHost, callbackPath: url.pathname }
138
+ }
139
+
140
+ /**
141
+ * Start OAuth authentication flow for a server.
142
+ * Returns the authorization URL when browser authorization is required.
143
+ */
144
+ export async function startAuth(
145
+ serverName: string,
146
+ serverUrl: string,
147
+ definition?: ServerEntry
148
+ ): Promise<{ authorizationUrl: string }> {
149
+ const config = definition ? extractOAuthConfig(definition) : {}
150
+
151
+ if (config.grantType === "client_credentials") {
152
+ const storedAuth = await getAuthForUrl(serverName, serverUrl)
153
+ if (storedAuth?.clientInfo && !storedAuth.tokens && !config.clientId) {
154
+ clearClientInfo(serverName)
155
+ clearCodeVerifier(serverName)
156
+ await clearOAuthState(serverName)
157
+ }
158
+
159
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
160
+ onRedirect: async () => {
161
+ throw new Error("Browser redirect is not used for client_credentials flow")
162
+ },
163
+ })
164
+ const result = await runSdkAuth(authProvider, { serverUrl })
165
+ if (result !== "AUTHORIZED") {
166
+ throw new UnauthorizedError("Failed to authorize")
167
+ }
168
+ return { authorizationUrl: "" }
169
+ }
170
+
171
+ const redirectCallback = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
172
+ const oauthState = generateState()
173
+
174
+ try {
175
+ await ensureCallbackServer({
176
+ strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
177
+ oauthState,
178
+ reserveState: true,
179
+ ...(redirectCallback ? { port: redirectCallback.port, callbackHost: redirectCallback.callbackHost, callbackPath: redirectCallback.callbackPath } : {}),
180
+ })
181
+ } catch (error) {
182
+ await clearOAuthState(serverName)
183
+ throw error
184
+ }
185
+
186
+ let capturedUrl: URL | undefined
187
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
188
+ onRedirect: async (url) => {
189
+ capturedUrl = url
190
+ },
191
+ })
192
+
193
+ try {
194
+ const storedAuth = await getAuthForUrl(serverName, serverUrl)
195
+ if (storedAuth?.clientInfo && !config.clientId) {
196
+ if (!storedAuth.tokens) {
197
+ clearClientInfo(serverName)
198
+ clearCodeVerifier(serverName)
199
+ await clearOAuthState(serverName)
200
+ } else {
201
+ const redirectUris = storedAuth.clientInfo.redirectUris
202
+ if (!Array.isArray(redirectUris) || !redirectUris.includes(authProvider.redirectUrl ?? "")) {
203
+ clearClientInfo(serverName)
204
+ clearTokens(serverName)
205
+ clearCodeVerifier(serverName)
206
+ await clearOAuthState(serverName)
207
+ }
208
+ }
209
+ }
210
+
211
+ await updateOAuthState(serverName, oauthState, serverUrl)
212
+
213
+ const result = await runSdkAuth(authProvider, { serverUrl })
214
+ if (result === "AUTHORIZED") {
215
+ releaseCallbackServer(oauthState)
216
+ await clearOAuthState(serverName)
217
+ return { authorizationUrl: "" }
218
+ }
219
+ if (!capturedUrl) {
220
+ throw new UnauthorizedError("OAuth authorization URL was not provided")
221
+ }
222
+ const pendingTransport = new StreamableHTTPClientTransport(new URL(serverUrl), { authProvider })
223
+ await setPendingTransport(serverName, pendingTransport, oauthState)
224
+ return { authorizationUrl: capturedUrl.toString() }
225
+ } catch (error) {
226
+ await clearPendingAuth(serverName, oauthState)
227
+ throw error
228
+ }
229
+ }
230
+
231
+ async function setPendingTransport(
232
+ serverName: string,
233
+ transport: StreamableHTTPClientTransport,
234
+ oauthState: string,
235
+ ): Promise<void> {
236
+ await clearPendingAuth(serverName)
237
+ pendingTransports.set(serverName, transport)
238
+ pendingAuthStates.set(serverName, oauthState)
239
+ const cleanupTimer = setTimeout(() => {
240
+ void clearPendingAuth(serverName, oauthState)
241
+ }, MANUAL_AUTH_TIMEOUT_MS)
242
+ cleanupTimer.unref?.()
243
+ pendingAuthCleanupTimers.set(serverName, cleanupTimer)
244
+ }
245
+
246
+ async function clearPendingAuth(serverName: string, oauthState?: string): Promise<void> {
247
+ const pendingState = pendingAuthStates.get(serverName)
248
+ if (oauthState && pendingState && pendingState !== oauthState) return
249
+
250
+ const timer = pendingAuthCleanupTimers.get(serverName)
251
+ if (timer) {
252
+ clearTimeout(timer)
253
+ pendingAuthCleanupTimers.delete(serverName)
254
+ }
255
+
256
+ const transport = pendingTransports.get(serverName)
257
+ pendingTransports.delete(serverName)
258
+ pendingAuthStates.delete(serverName)
259
+ const stateToRelease = pendingState ?? oauthState
260
+ if (stateToRelease) {
261
+ releaseCallbackServer(stateToRelease)
262
+ const storedState = await getOAuthState(serverName)
263
+ if (storedState === stateToRelease) {
264
+ await clearOAuthState(serverName)
265
+ }
266
+ }
267
+ if (transport) {
268
+ await transport.close().catch(() => {})
269
+ }
270
+ }
271
+
272
+ function getSearchParamsFromInput(input: string): URLSearchParams | undefined {
273
+ try {
274
+ const url = new URL(input)
275
+ const params = new URLSearchParams(url.search)
276
+ if (url.hash) {
277
+ const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash
278
+ const hashParams = new URLSearchParams(hash)
279
+ for (const [key, value] of hashParams) {
280
+ if (!params.has(key)) params.set(key, value)
281
+ }
282
+ }
283
+ return params
284
+ } catch {
285
+ const query = input.includes("?") ? input.slice(input.indexOf("?") + 1) : input
286
+ const params = new URLSearchParams(query.startsWith("#") ? query.slice(1) : query)
287
+ return params.has("code") || params.has("state") || params.has("error") ? params : undefined
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Extract an OAuth authorization code from either a raw code, a query string,
293
+ * or the full localhost redirect URL copied from the browser address bar.
294
+ */
295
+ export function parseAuthorizationCodeInput(input: string, expectedState?: string): string {
296
+ const trimmed = input.trim()
297
+ if (!trimmed) {
298
+ throw new Error("Authorization code or redirect URL is required")
299
+ }
300
+
301
+ const params = getSearchParamsFromInput(trimmed)
302
+ if (params) {
303
+ const error = params.get("error")
304
+ if (error) {
305
+ const description = params.get("error_description")
306
+ throw new Error(description ? `${error}: ${description}` : error)
307
+ }
308
+
309
+ const state = params.get("state")
310
+ if (expectedState && !state) {
311
+ throw new Error("OAuth state missing from redirect URL")
312
+ }
313
+ if (expectedState && state !== expectedState) {
314
+ throw new Error("OAuth state mismatch - potential CSRF attack")
315
+ }
316
+
317
+ const code = params.get("code")
318
+ if (code) return code
319
+ }
320
+
321
+ if (/^[A-Za-z0-9._~+/=-]+$/.test(trimmed)) {
322
+ return trimmed
323
+ }
324
+
325
+ throw new Error("Could not find an OAuth authorization code in the provided input")
326
+ }
327
+
328
+ /**
329
+ * Complete OAuth authentication from manual user input.
330
+ */
331
+ export async function completeAuthFromInput(
332
+ serverName: string,
333
+ input: string,
334
+ ): Promise<AuthStatus> {
335
+ const oauthState = await getOAuthState(serverName)
336
+ const code = parseAuthorizationCodeInput(input, oauthState)
337
+ return completeAuth(serverName, code)
338
+ }
339
+
340
+ /**
341
+ * Complete OAuth authentication with the authorization code.
342
+ */
343
+ export async function completeAuth(
344
+ serverName: string,
345
+ authorizationCode: string
346
+ ): Promise<AuthStatus> {
347
+ const transport = pendingTransports.get(serverName)
348
+ if (!transport) {
349
+ throw new Error(`No pending OAuth flow for server: ${serverName}`)
350
+ }
351
+
352
+ const oauthState = await getOAuthState(serverName)
353
+
354
+ try {
355
+ // Complete the auth using the transport's finishAuth method
356
+ await transport.finishAuth(authorizationCode)
357
+ return "authenticated"
358
+ } finally {
359
+ await clearPendingAuth(serverName, oauthState)
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Perform the complete OAuth authentication flow for a server.
365
+ *
366
+ * @param serverName - The name of the MCP server
367
+ * @param serverUrl - The URL of the MCP server
368
+ * @param definition - The server definition (optional)
369
+ * @returns The final auth status
370
+ */
371
+ export async function authenticate(
372
+ serverName: string,
373
+ serverUrl: string,
374
+ definition?: ServerEntry,
375
+ ): Promise<AuthStatus> {
376
+ const inFlight = pendingAuthentications.get(serverName)
377
+ if (inFlight) {
378
+ return inFlight
379
+ }
380
+
381
+ const operation = (async (): Promise<AuthStatus> => {
382
+ // Start auth flow
383
+ const { authorizationUrl } = await startAuth(serverName, serverUrl, definition)
384
+
385
+ // If no auth URL needed, already authenticated
386
+ if (!authorizationUrl) {
387
+ return "authenticated"
388
+ }
389
+
390
+ // Get the state that was already generated and stored in startAuth()
391
+ const oauthState = await getOAuthState(serverName)
392
+ if (!oauthState) {
393
+ throw new Error("OAuth state not found - this should not happen")
394
+ }
395
+
396
+ // Register the callback BEFORE opening the browser
397
+ const callbackPromise = waitForCallback(oauthState)
398
+
399
+ try {
400
+ // Open browser. Always print the URL first so remote/headless users can copy it
401
+ // even when the OS browser handoff is unavailable or invisible.
402
+ console.log(`MCP Auth: Open this URL to authenticate ${serverName}:\n${authorizationUrl}`)
403
+ try {
404
+ await open(authorizationUrl)
405
+ } catch (error) {
406
+ console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
407
+ }
408
+
409
+ // Wait for callback
410
+ const code = await callbackPromise
411
+
412
+ // Validate state
413
+ const storedState = await getOAuthState(serverName)
414
+ if (storedState !== oauthState) {
415
+ await clearOAuthState(serverName)
416
+ throw new Error("OAuth state mismatch - potential CSRF attack")
417
+ }
418
+ await clearOAuthState(serverName)
419
+
420
+ // Complete the auth
421
+ return await completeAuth(serverName, code)
422
+ } catch (error) {
423
+ cancelPendingCallback(oauthState)
424
+ await clearPendingAuth(serverName, oauthState)
425
+ throw error
426
+ }
427
+ })()
428
+
429
+ pendingAuthentications.set(serverName, operation)
430
+
431
+ try {
432
+ return await operation
433
+ } finally {
434
+ if (pendingAuthentications.get(serverName) === operation) {
435
+ pendingAuthentications.delete(serverName)
436
+ }
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Get a valid access token for a server, refreshing if necessary.
442
+ *
443
+ * @param serverName - The name of the MCP server
444
+ * @param serverUrl - The URL of the MCP server
445
+ * @returns The valid tokens or null if not authenticated
446
+ */
447
+ export async function getValidToken(
448
+ serverName: string,
449
+ serverUrl: string,
450
+ ): Promise<StoredTokens | null> {
451
+ // Check if we have valid tokens
452
+ const entry = await getAuthForUrl(serverName, serverUrl)
453
+ if (!entry?.tokens) {
454
+ return null
455
+ }
456
+
457
+ // Check expiration
458
+ const expired = await isTokenExpired(serverName)
459
+ if (expired === false) {
460
+ return entry.tokens
461
+ }
462
+
463
+ if (expired === true && entry.tokens.refreshToken) {
464
+ // Token is expired, try to refresh
465
+ console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`)
466
+
467
+ try {
468
+ // Create auth provider for token refresh
469
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, {}, {
470
+ onRedirect: async () => {},
471
+ })
472
+
473
+ const clientInfo = await authProvider.clientInformation()
474
+ if (!clientInfo) {
475
+ console.log(`MCP Auth: No client info for refresh for ${serverName}`)
476
+ return null
477
+ }
478
+
479
+ const result = await runSdkAuth(authProvider, { serverUrl })
480
+ if (result !== "AUTHORIZED") {
481
+ return null
482
+ }
483
+ const refreshed = await getAuthForUrl(serverName, serverUrl)
484
+ return refreshed?.tokens ?? null
485
+ } catch (error) {
486
+ console.error(`MCP Auth: Token refresh failed for ${serverName}`, { error })
487
+ return null
488
+ }
489
+ }
490
+
491
+ // No expiration info or no refresh token, assume valid
492
+ return entry.tokens
493
+ }
494
+
495
+ /**
496
+ * Check the authentication status for a server.
497
+ *
498
+ * @param serverName - The name of the MCP server
499
+ * @returns The current auth status
500
+ */
501
+ export async function getAuthStatus(serverName: string): Promise<AuthStatus> {
502
+ const hasTokens = await hasStoredTokens(serverName)
503
+ if (!hasTokens) return "not_authenticated"
504
+
505
+ const expired = await isTokenExpired(serverName)
506
+ return expired ? "expired" : "authenticated"
507
+ }
508
+
509
+ /**
510
+ * Remove all OAuth credentials for a server.
511
+ *
512
+ * @param serverName - The name of the MCP server
513
+ */
514
+ export async function removeAuth(serverName: string): Promise<void> {
515
+ const oauthState = await getOAuthState(serverName)
516
+ if (oauthState) {
517
+ cancelPendingCallback(oauthState)
518
+ }
519
+ await clearPendingAuth(serverName, oauthState)
520
+ clearAllCredentials(serverName)
521
+ await clearOAuthState(serverName)
522
+ console.log(`MCP Auth: Removed credentials for ${serverName}`)
523
+ }
524
+
525
+ /**
526
+ * Check if OAuth is supported for a server configuration.
527
+ * OAuth is supported for HTTP servers unless explicitly disabled.
528
+ *
529
+ * @param definition - The server definition
530
+ * @returns True if OAuth is supported
531
+ */
532
+ export function supportsOAuth(definition: ServerEntry): boolean {
533
+ // OAuth requires a URL
534
+ if (!definition.url) return false
535
+
536
+ // Explicitly disabled via auth: false or oauth: false
537
+ if (definition.auth === false) return false
538
+ if (definition.oauth === false) return false
539
+
540
+ // OAuth is enabled if auth is 'oauth' or not specified (auto-detect)
541
+ return definition.auth === "oauth" || definition.auth === undefined
542
+ }
543
+
544
+ /**
545
+ * Initialize the OAuth system on startup.
546
+ * OAuth callback binding is lazy and starts from startAuth() only.
547
+ */
548
+ export async function initializeOAuth(): Promise<void> {}
549
+
550
+ /**
551
+ * Shutdown the OAuth system.
552
+ * Stops the callback server and cancels pending auths.
553
+ */
554
+ export async function shutdownOAuth(): Promise<void> {
555
+ for (const serverName of Array.from(pendingTransports.keys())) {
556
+ await clearPendingAuth(serverName)
557
+ }
558
+ await stopCallbackServer()
559
+ }