pi-mcp-adapter 2.2.2 → 2.3.2

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.3.2] - 2026-04-11
11
+
12
+ ### Added
13
+ - Optional `oauth.grantType: "client_credentials"` for non-interactive machine-to-machine OAuth on HTTP MCP servers.
14
+
15
+ ### Fixed
16
+ - `/mcp-auth <server>` now handles `client_credentials` without browser/callback flow.
17
+ - MCP panel status no longer marks `client_credentials` servers as auth-blocked solely because no stored user tokens exist yet.
18
+ - OAuth auth flow now closes temporary transports consistently on success, refresh, and auth removal paths.
19
+ - Init paths now preserve debug-level context for previously silent direct-tool bootstrap and lazy-connect failures.
20
+
21
+ ## [2.3.1] - 2026-04-11
22
+
23
+ ### Fixed
24
+ - Removed `/mcp-auth-callback`. OAuth auth now hard-cuts to `/mcp-auth <server>` only.
25
+
26
+ ## [2.3.0] - 2026-04-11
27
+
28
+ ### Added
29
+ - OAuth callback server initialization on session start and a deprecated `/mcp-auth-callback` command that now points users to `/mcp-auth <server>`.
30
+
31
+ ### Fixed
32
+ - OAuth `needs-auth` handling across `/mcp` status/panel, `mcp({ connect })`, `mcp({ tool })`, reconnect flow, lazy/direct tool execution, and startup bootstrap.
33
+ - OAuth callback cleanup now cancels by stored OAuth state and closes pending transports on failure/cancel paths.
34
+ - Callback server now fails fast when the OAuth callback port is occupied by another process.
35
+ - Package manifest test now ignores root `*.test.ts` files.
36
+
10
37
  ## [2.2.2] - 2026-04-03
11
38
 
12
39
  ### Fixed
package/README.md CHANGED
@@ -85,6 +85,7 @@ Two calls instead of 26 tools cluttering the context.
85
85
  | `cwd` | Working directory |
86
86
  | `url` | HTTP endpoint (StreamableHTTP with SSE fallback) |
87
87
  | `auth` | `"bearer"` or `"oauth"` |
88
+ | `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
88
89
  | `bearerToken` / `bearerTokenEnv` | Token or env var name |
89
90
  | `lifecycle` | `"lazy"` (default), `"eager"`, or `"keep-alive"` |
90
91
  | `idleTimeout` | Minutes before idle disconnect (overrides global) |
@@ -291,6 +292,4 @@ Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_li
291
292
 
292
293
  ## Limitations
293
294
 
294
- - OAuth tokens obtained externally (no browser flow)
295
- - No automatic token refresh
296
295
  - Cross-session server sharing not yet implemented (each Pi session runs its own server processes)
package/commands.ts CHANGED
@@ -4,8 +4,9 @@ import type { McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult } from "
4
4
  import { getServerProvenance, writeDirectToolsConfig } from "./config.js";
5
5
  import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.js";
6
6
  import { loadMetadataCache } from "./metadata-cache.js";
7
- import { getStoredTokens } from "./oauth-handler.js";
8
7
  import { buildToolMetadata } from "./tool-metadata.js";
8
+ import { supportsOAuth, authenticate } from "./mcp-auth-flow.js";
9
+ import { hasStoredTokens } from "./mcp-auth.js";
9
10
 
10
11
  export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
11
12
  if (!ctx.hasUI) return;
@@ -24,6 +25,9 @@ export async function showStatus(state: McpExtensionState, ctx: ExtensionContext
24
25
  if (connection?.status === "connected") {
25
26
  status = "connected";
26
27
  statusIcon = "✓";
28
+ } else if (connection?.status === "needs-auth") {
29
+ status = "needs auth";
30
+ statusIcon = "⚠";
27
31
  } else if (failedAgo !== null) {
28
32
  status = `failed ${failedAgo}s ago`;
29
33
  statusIcon = "✗";
@@ -85,6 +89,12 @@ export async function reconnectServers(
85
89
  await state.manager.close(name);
86
90
 
87
91
  const connection = await state.manager.connect(name, definition);
92
+ if (connection.status === "needs-auth") {
93
+ if (ctx.hasUI) {
94
+ ctx.ui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, "warning");
95
+ }
96
+ continue;
97
+ }
88
98
  const prefix = state.config.settings?.toolPrefix ?? "server";
89
99
 
90
100
  const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
@@ -126,10 +136,10 @@ export async function authenticateServer(
126
136
  return;
127
137
  }
128
138
 
129
- if (definition.auth !== "oauth") {
139
+ if (!supportsOAuth(definition)) {
130
140
  ctx.ui.notify(
131
141
  `Server "${serverName}" does not use OAuth authentication.\n` +
132
- `Current auth mode: ${definition.auth ?? "none"}`,
142
+ `Set "auth": "oauth" or omit auth for auto-detection.`,
133
143
  "error"
134
144
  );
135
145
  return;
@@ -143,21 +153,31 @@ export async function authenticateServer(
143
153
  return;
144
154
  }
145
155
 
146
- const tokenPath = `~/.pi/agent/mcp-oauth/${serverName}/tokens.json`;
147
-
148
- ctx.ui.notify(
149
- `OAuth setup for "${serverName}":\n\n` +
150
- `1. Obtain an access token from your OAuth provider\n` +
151
- `2. Create the token file:\n` +
152
- ` ${tokenPath}\n\n` +
153
- `3. Add your token:\n` +
154
- ` {\n` +
155
- ` "access_token": "your-token-here",\n` +
156
- ` "token_type": "bearer"\n` +
157
- ` }\n\n` +
158
- `4. Run /mcp reconnect to connect with the token`,
159
- "info"
160
- );
156
+ // Full automatic OAuth flow using SDK
157
+ try {
158
+ ctx.ui.setStatus("mcp-auth", `Authenticating ${serverName}...`);
159
+
160
+ // Runs the configured OAuth flow (interactive browser or non-interactive client_credentials)
161
+ const status = await authenticate(serverName, definition.url, definition);
162
+
163
+ if (status === "authenticated") {
164
+ ctx.ui.notify(
165
+ `OAuth authentication successful for "${serverName}"!\n` +
166
+ `Run /mcp reconnect ${serverName} to connect with the new token.`,
167
+ "success"
168
+ );
169
+ } else {
170
+ ctx.ui.notify(
171
+ `OAuth authentication failed for "${serverName}".`,
172
+ "error"
173
+ );
174
+ }
175
+ } catch (error) {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ ctx.ui.notify(`Failed to authenticate "${serverName}": ${message}`, "error");
178
+ } finally {
179
+ ctx.ui.setStatus("mcp-auth", undefined);
180
+ }
161
181
  }
162
182
 
163
183
  export async function openMcpPanel(
@@ -176,10 +196,18 @@ export async function openMcpPanel(
176
196
  },
177
197
  getConnectionStatus: (serverName: string) => {
178
198
  const definition = config.mcpServers[serverName];
179
- if (definition?.auth === "oauth" && getStoredTokens(serverName) === undefined) {
199
+ const connection = state.manager.getConnection(serverName);
200
+ if (connection?.status === "needs-auth") {
201
+ return "needs-auth";
202
+ }
203
+ if (
204
+ definition?.auth === "oauth"
205
+ && definition.oauth !== false
206
+ && definition.oauth?.grantType !== "client_credentials"
207
+ && !hasStoredTokens(serverName)
208
+ ) {
180
209
  return "needs-auth";
181
210
  }
182
- const connection = state.manager.getConnection(serverName);
183
211
  if (connection?.status === "connected") return "connected";
184
212
  if (getFailureAgeSeconds(state, serverName) !== null) return "failed";
185
213
  return "idle";
package/direct-tools.ts CHANGED
@@ -198,6 +198,13 @@ export function createDirectToolExecutor(
198
198
 
199
199
  const connected = await lazyConnect(state, spec.serverName);
200
200
  if (!connected) {
201
+ const authConnection = state.manager.getConnection(spec.serverName);
202
+ if (authConnection?.status === "needs-auth") {
203
+ return {
204
+ content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" requires OAuth authentication. Run /mcp-auth ${spec.serverName} first.` }],
205
+ details: { error: "auth_required", server: spec.serverName },
206
+ };
207
+ }
201
208
  const failedAgo = getFailureAgeSeconds(state, spec.serverName);
202
209
  return {
203
210
  content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not available${failedAgo !== null ? ` (failed ${failedAgo}s ago)` : ""}` }],
package/index.ts CHANGED
@@ -8,6 +8,7 @@ import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.js";
8
8
  import { loadMetadataCache } from "./metadata-cache.js";
9
9
  import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.js";
10
10
  import { getConfigPathFromArgv, truncateAtWord } from "./utils.js";
11
+ import { initializeOAuth } from "./mcp-auth-flow.js";
11
12
 
12
13
  export default function mcpAdapter(pi: ExtensionAPI) {
13
14
  let state: McpExtensionState | null = null;
@@ -93,6 +94,10 @@ export default function mcpAdapter(pi: ExtensionAPI) {
93
94
  return;
94
95
  }
95
96
 
97
+ await initializeOAuth().catch(err => {
98
+ console.error("MCP OAuth initialization failed:", err);
99
+ });
100
+
96
101
  const promise = initializeMcp(pi, ctx);
97
102
  initPromise = promise;
98
103
 
package/init.ts CHANGED
@@ -108,6 +108,9 @@ export async function initializeMcp(
108
108
  const results = await parallelLimit(startupServers, 10, async ([name, definition]) => {
109
109
  try {
110
110
  const connection = await manager.connect(name, definition);
111
+ if (connection.status === "needs-auth") {
112
+ return { name, definition, connection: null, error: `OAuth authentication required. Run /mcp-auth ${name}.` };
113
+ }
111
114
  return { name, definition, connection, error: null };
112
115
  } catch (error) {
113
116
  const message = error instanceof Error ? error.message : String(error);
@@ -169,11 +172,16 @@ export async function initializeMcp(
169
172
  const definition = config.mcpServers[name];
170
173
  try {
171
174
  const connection = await manager.connect(name, definition);
175
+ if (connection.status === "needs-auth") {
176
+ return { name, ok: false };
177
+ }
172
178
  const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
173
179
  toolMetadata.set(name, metadata);
174
180
  updateMetadataCache(state, name);
175
181
  return { name, ok: true };
176
- } catch {
182
+ } catch (error) {
183
+ const message = error instanceof Error ? error.message : String(error);
184
+ logger.debug(`MCP: direct-tools bootstrap failed for ${name}: ${message}`);
177
185
  return { name, ok: false };
178
186
  }
179
187
  },
@@ -279,6 +287,9 @@ export function getFailureAgeSeconds(state: McpExtensionState, serverName: strin
279
287
 
280
288
  export async function lazyConnect(state: McpExtensionState, serverName: string): Promise<boolean> {
281
289
  const connection = state.manager.getConnection(serverName);
290
+ if (connection?.status === "needs-auth") {
291
+ return false;
292
+ }
282
293
  if (connection?.status === "connected") {
283
294
  updateServerMetadata(state, serverName);
284
295
  return true;
@@ -294,14 +305,19 @@ export async function lazyConnect(state: McpExtensionState, serverName: string):
294
305
  if (state.ui) {
295
306
  state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
296
307
  }
297
- await state.manager.connect(serverName, definition);
308
+ const newConnection = await state.manager.connect(serverName, definition);
309
+ if (newConnection.status === "needs-auth") {
310
+ return false;
311
+ }
298
312
  state.failureTracker.delete(serverName);
299
313
  updateServerMetadata(state, serverName);
300
314
  updateMetadataCache(state, serverName);
301
315
  updateStatusBar(state);
302
316
  return true;
303
- } catch {
317
+ } catch (error) {
304
318
  state.failureTracker.set(serverName, Date.now());
319
+ const message = error instanceof Error ? error.message : String(error);
320
+ logger.debug(`MCP: lazy connect failed for ${serverName}: ${message}`);
305
321
  updateStatusBar(state);
306
322
  return false;
307
323
  }
@@ -0,0 +1,378 @@
1
+ /**
2
+ * MCP Auth Flow
3
+ *
4
+ * High-level OAuth flow management using the MCP SDK's built-in auth functions.
5
+ * Follows the OpenCode pattern: let the SDK handle discovery internally via transport.
6
+ */
7
+
8
+ import {
9
+ UnauthorizedError,
10
+ } from "@modelcontextprotocol/sdk/client/auth.js"
11
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
12
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
13
+ import open from "open"
14
+ import { McpOAuthProvider, type McpOAuthConfig } from "./mcp-oauth-provider.js"
15
+ import {
16
+ ensureCallbackServer,
17
+ waitForCallback,
18
+ cancelPendingCallback,
19
+ stopCallbackServer,
20
+ } from "./mcp-callback-server.js"
21
+ import {
22
+ getAuthForUrl,
23
+ isTokenExpired,
24
+ hasStoredTokens,
25
+ clearAllCredentials,
26
+ updateOAuthState,
27
+ getOAuthState,
28
+ clearOAuthState,
29
+ type StoredTokens,
30
+ } from "./mcp-auth.js"
31
+ import type { ServerEntry } from "./types.js"
32
+
33
+ /** Auth status for a server */
34
+ export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
35
+
36
+ // Track pending transports for auth completion
37
+ const pendingTransports = new Map<string, StreamableHTTPClientTransport>()
38
+
39
+ /**
40
+ * Generate a cryptographically secure random state parameter.
41
+ */
42
+ function generateState(): string {
43
+ return Array.from(crypto.getRandomValues(new Uint8Array(32)))
44
+ .map((b) => b.toString(16).padStart(2, "0"))
45
+ .join("")
46
+ }
47
+
48
+ /**
49
+ * Extract OAuth configuration from a ServerEntry.
50
+ */
51
+ function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
52
+ // If oauth is explicitly false, return empty config
53
+ if (definition.oauth === false) {
54
+ return {}
55
+ }
56
+ return {
57
+ grantType: definition.oauth?.grantType,
58
+ clientId: definition.oauth?.clientId,
59
+ clientSecret: definition.oauth?.clientSecret,
60
+ scope: definition.oauth?.scope,
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Start OAuth authentication flow for a server.
66
+ * Returns the authorization URL that should be opened in a browser.
67
+ *
68
+ * This follows the OpenCode pattern:
69
+ * 1. Create transport with auth provider
70
+ * 2. Try to connect - SDK handles discovery internally
71
+ * 3. If UnauthorizedError, capture the auth URL from onRedirect
72
+ */
73
+ export async function startAuth(
74
+ serverName: string,
75
+ serverUrl: string,
76
+ definition?: ServerEntry
77
+ ): Promise<{ authorizationUrl: string; transport: StreamableHTTPClientTransport }> {
78
+ const config = definition ? extractOAuthConfig(definition) : {}
79
+
80
+ if (config.grantType === "client_credentials") {
81
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
82
+ onRedirect: async () => {
83
+ throw new Error("Browser redirect is not used for client_credentials flow")
84
+ },
85
+ })
86
+ const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
87
+ authProvider,
88
+ })
89
+ const client = new Client({
90
+ name: "pi-mcp",
91
+ version: "3.0.0",
92
+ })
93
+
94
+ try {
95
+ await client.connect(transport)
96
+ return { authorizationUrl: "", transport }
97
+ } finally {
98
+ await client.close().catch(() => {})
99
+ await transport.close().catch(() => {})
100
+ }
101
+ }
102
+
103
+ // Start the callback server
104
+ await ensureCallbackServer()
105
+
106
+ // Generate and store OAuth state BEFORE creating the provider
107
+ // The SDK will call provider.state() to read this value
108
+ const oauthState = generateState()
109
+ await updateOAuthState(serverName, oauthState)
110
+
111
+ // Create the auth provider
112
+ let capturedUrl: URL | undefined
113
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
114
+ onRedirect: async (url) => {
115
+ capturedUrl = url
116
+ },
117
+ })
118
+
119
+ // Create transport with auth provider
120
+ // The SDK handles OAuth discovery internally when connecting
121
+ const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
122
+ authProvider,
123
+ })
124
+ const client = new Client({
125
+ name: "pi-mcp",
126
+ version: "3.0.0",
127
+ })
128
+
129
+ // Try to connect - this triggers the OAuth flow
130
+ try {
131
+ await client.connect(transport)
132
+ // If we get here, we're already authenticated
133
+ await client.close().catch(() => {})
134
+ await transport.close().catch(() => {})
135
+ return { authorizationUrl: "", transport }
136
+ } catch (error) {
137
+ if (error instanceof UnauthorizedError && capturedUrl) {
138
+ await client.close().catch(() => {})
139
+ // Store transport for later finishAuth
140
+ pendingTransports.set(serverName, transport)
141
+ return { authorizationUrl: capturedUrl.toString(), transport }
142
+ }
143
+ await client.close().catch(() => {})
144
+ await transport.close().catch(() => {})
145
+ throw error
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Complete OAuth authentication with the authorization code.
151
+ */
152
+ export async function completeAuth(
153
+ serverName: string,
154
+ authorizationCode: string
155
+ ): Promise<AuthStatus> {
156
+ const transport = pendingTransports.get(serverName)
157
+ if (!transport) {
158
+ throw new Error(`No pending OAuth flow for server: ${serverName}`)
159
+ }
160
+
161
+ try {
162
+ // Complete the auth using the transport's finishAuth method
163
+ await transport.finishAuth(authorizationCode)
164
+ return "authenticated"
165
+ } finally {
166
+ pendingTransports.delete(serverName)
167
+ await transport.close().catch(() => {})
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Perform the complete OAuth authentication flow for a server.
173
+ *
174
+ * @param serverName - The name of the MCP server
175
+ * @param serverUrl - The URL of the MCP server
176
+ * @param definition - The server definition (optional)
177
+ * @returns The final auth status
178
+ */
179
+ export async function authenticate(
180
+ serverName: string,
181
+ serverUrl: string,
182
+ definition?: ServerEntry,
183
+ ): Promise<AuthStatus> {
184
+ // Start auth flow
185
+ const { authorizationUrl } = await startAuth(serverName, serverUrl, definition)
186
+
187
+ // If no auth URL needed, already authenticated
188
+ if (!authorizationUrl) {
189
+ return "authenticated"
190
+ }
191
+
192
+ // Get the state that was already generated and stored in startAuth()
193
+ const oauthState = await getOAuthState(serverName)
194
+ if (!oauthState) {
195
+ throw new Error("OAuth state not found - this should not happen")
196
+ }
197
+
198
+ // Register the callback BEFORE opening the browser
199
+ const callbackPromise = waitForCallback(oauthState)
200
+
201
+ // Open browser
202
+ console.log(`MCP Auth: Opening browser for ${serverName}`)
203
+ try {
204
+ await open(authorizationUrl)
205
+ } catch (error) {
206
+ console.warn(`MCP Auth: Failed to open browser for ${serverName}`, { error })
207
+ throw new Error(
208
+ `Could not open browser. Please open this URL manually: ${authorizationUrl}`
209
+ )
210
+ }
211
+
212
+ try {
213
+ // Wait for callback
214
+ const code = await callbackPromise
215
+
216
+ // Validate state
217
+ const storedState = await getOAuthState(serverName)
218
+ if (storedState !== oauthState) {
219
+ await clearOAuthState(serverName)
220
+ throw new Error("OAuth state mismatch - potential CSRF attack")
221
+ }
222
+ await clearOAuthState(serverName)
223
+
224
+ // Complete the auth
225
+ return await completeAuth(serverName, code)
226
+ } catch (error) {
227
+ cancelPendingCallback(oauthState)
228
+ const pendingTransport = pendingTransports.get(serverName)
229
+ if (pendingTransport) {
230
+ pendingTransports.delete(serverName)
231
+ await pendingTransport.close().catch(() => {})
232
+ }
233
+ throw error
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Get a valid access token for a server, refreshing if necessary.
239
+ *
240
+ * @param serverName - The name of the MCP server
241
+ * @param serverUrl - The URL of the MCP server
242
+ * @returns The valid tokens or null if not authenticated
243
+ */
244
+ export async function getValidToken(
245
+ serverName: string,
246
+ serverUrl: string,
247
+ ): Promise<StoredTokens | null> {
248
+ // Check if we have valid tokens
249
+ const entry = await getAuthForUrl(serverName, serverUrl)
250
+ if (!entry?.tokens) {
251
+ return null
252
+ }
253
+
254
+ // Check expiration
255
+ const expired = await isTokenExpired(serverName)
256
+ if (expired === false) {
257
+ return entry.tokens
258
+ }
259
+
260
+ if (expired === true && entry.tokens.refreshToken) {
261
+ // Token is expired, try to refresh
262
+ console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`)
263
+
264
+ try {
265
+ // Create auth provider for token refresh
266
+ const authProvider = new McpOAuthProvider(serverName, serverUrl, {}, {
267
+ onRedirect: async () => {},
268
+ })
269
+
270
+ const clientInfo = await authProvider.clientInformation()
271
+ if (!clientInfo) {
272
+ console.log(`MCP Auth: No client info for refresh for ${serverName}`)
273
+ return null
274
+ }
275
+
276
+ // Try to get tokens to find the token endpoint
277
+ const existingTokens = await authProvider.tokens()
278
+ if (!existingTokens) {
279
+ return null
280
+ }
281
+
282
+ // Create transport to trigger refresh
283
+ const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
284
+ authProvider,
285
+ })
286
+
287
+ // Try to connect - SDK will attempt token refresh internally
288
+ const client = new Client({ name: "pi-mcp", version: "3.0.0" })
289
+ try {
290
+ await client.connect(transport)
291
+ // Get refreshed tokens
292
+ const refreshed = await getAuthForUrl(serverName, serverUrl)
293
+ return refreshed?.tokens ?? null
294
+ } catch (error) {
295
+ console.error(`MCP Auth: Token refresh failed for ${serverName}`, { error })
296
+ return null
297
+ } finally {
298
+ await client.close().catch(() => {})
299
+ await transport.close().catch(() => {})
300
+ }
301
+ } catch (error) {
302
+ console.error(`MCP Auth: Token refresh failed for ${serverName}`, { error })
303
+ return null
304
+ }
305
+ }
306
+
307
+ // No expiration info or no refresh token, assume valid
308
+ return entry.tokens
309
+ }
310
+
311
+ /**
312
+ * Check the authentication status for a server.
313
+ *
314
+ * @param serverName - The name of the MCP server
315
+ * @returns The current auth status
316
+ */
317
+ export async function getAuthStatus(serverName: string): Promise<AuthStatus> {
318
+ const hasTokens = await hasStoredTokens(serverName)
319
+ if (!hasTokens) return "not_authenticated"
320
+
321
+ const expired = await isTokenExpired(serverName)
322
+ return expired ? "expired" : "authenticated"
323
+ }
324
+
325
+ /**
326
+ * Remove all OAuth credentials for a server.
327
+ *
328
+ * @param serverName - The name of the MCP server
329
+ */
330
+ export async function removeAuth(serverName: string): Promise<void> {
331
+ const oauthState = await getOAuthState(serverName)
332
+ if (oauthState) {
333
+ cancelPendingCallback(oauthState)
334
+ }
335
+ const pendingTransport = pendingTransports.get(serverName)
336
+ if (pendingTransport) {
337
+ pendingTransports.delete(serverName)
338
+ await pendingTransport.close().catch(() => {})
339
+ }
340
+ clearAllCredentials(serverName)
341
+ await clearOAuthState(serverName)
342
+ console.log(`MCP Auth: Removed credentials for ${serverName}`)
343
+ }
344
+
345
+ /**
346
+ * Check if OAuth is supported for a server configuration.
347
+ * OAuth is supported for HTTP servers unless explicitly disabled.
348
+ *
349
+ * @param definition - The server definition
350
+ * @returns True if OAuth is supported
351
+ */
352
+ export function supportsOAuth(definition: ServerEntry): boolean {
353
+ // OAuth requires a URL
354
+ if (!definition.url) return false
355
+
356
+ // Explicitly disabled via auth: false or oauth: false
357
+ if (definition.auth === false) return false
358
+ if (definition.oauth === false) return false
359
+
360
+ // OAuth is enabled if auth is 'oauth' or not specified (auto-detect)
361
+ return definition.auth === "oauth" || definition.auth === undefined
362
+ }
363
+
364
+ /**
365
+ * Initialize the OAuth system on startup.
366
+ * Starts the callback server if there are any OAuth servers configured.
367
+ */
368
+ export async function initializeOAuth(): Promise<void> {
369
+ await ensureCallbackServer()
370
+ }
371
+
372
+ /**
373
+ * Shutdown the OAuth system.
374
+ * Stops the callback server and cancels pending auths.
375
+ */
376
+ export async function shutdownOAuth(): Promise<void> {
377
+ await stopCallbackServer()
378
+ }