diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index b42651dc17..b24cae9aec 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -669,14 +669,20 @@ export const McpDebugCommand = effectCmd({ const config = yield* Config.Service.use((cfg) => cfg.get()) const mcp = yield* MCP.Service const auth = yield* McpAuth.Service + const serverConfig = config.mcp?.[args.name] + const authInfo = + serverConfig && isMcpRemote(serverConfig) && serverConfig.oauth !== false + ? yield* Effect.all({ + authStatus: mcp.getAuthStatus(args.name), + entry: auth.get(args.name), + }) + : undefined yield* Effect.promise(async () => { UI.empty() prompts.intro("MCP OAuth Debug") - const mcpServers = config.mcp ?? {} const serverName = args.name - const serverConfig = mcpServers[serverName] if (!serverConfig) { prompts.log.error(`MCP server not found: ${serverName}`) prompts.outro("Done") @@ -698,13 +704,7 @@ export const McpDebugCommand = effectCmd({ prompts.log.info(`Server: ${serverName}`) prompts.log.info(`URL: ${serverConfig.url}`) - // Check stored auth status — services already in hand, run inline. - const { authStatus, entry } = await Effect.runPromise( - Effect.all({ - authStatus: mcp.getAuthStatus(serverName), - entry: auth.get(serverName), - }), - ) + const { authStatus, entry } = authInfo! prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`) if (entry?.tokens) { diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index 3866d1564a..f877824e02 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -50,7 +50,6 @@ export interface Interface { readonly updateOAuthState: (mcpName: string, oauthState: string) => Effect.Effect readonly getOAuthState: (mcpName: string) => Effect.Effect readonly clearOAuthState: (mcpName: string) => Effect.Effect - readonly isTokenExpired: (mcpName: string) => Effect.Effect } export class Service extends Context.Service()("@opencode/McpAuth") {} @@ -142,13 +141,6 @@ export const layer = Layer.effect( return entry?.oauthState }) - const isTokenExpired = Effect.fn("McpAuth.isTokenExpired")(function* (mcpName: string) { - const entry = yield* get(mcpName) - if (!entry?.tokens) return null - if (!entry.tokens.expiresAt) return false - return entry.tokens.expiresAt < Date.now() / 1000 - }) - return Service.of({ all, get, @@ -162,7 +154,6 @@ export const layer = Layer.effect( updateOAuthState, getOAuthState, clearOAuthState, - isTokenExpired, }) }), ) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 59f99aae38..023eab520e 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -963,10 +963,15 @@ export const layer = Layer.effect( }) const getAuthStatus = Effect.fn("MCP.getAuthStatus")(function* (mcpName: string) { - const entry = yield* auth.get(mcpName) + const runtimeConfig = (yield* InstanceState.has(state)) + ? (yield* InstanceState.get(state)).config[mcpName] + : undefined + const mcpConfig = runtimeConfig ?? (yield* cfgSvc.get()).mcp?.[mcpName] + if (!mcpConfig || !isMcpConfigured(mcpConfig) || mcpConfig.type !== "remote") return "not_authenticated" + const entry = yield* auth.getForUrl(mcpName, mcpConfig.url) if (!entry?.tokens) return "not_authenticated" - const expired = yield* auth.isTokenExpired(mcpName) - return expired ? "expired" : "authenticated" + if (entry.tokens.expiresAt && entry.tokens.expiresAt < Date.now() / 1000) return "expired" + return "authenticated" }) return Service.of({ diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts index 480792b1cb..d04e6ae1c8 100644 --- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts +++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts @@ -227,6 +227,38 @@ mcpTest.instance("state() returns existing state when one is saved", () => }), ) +mcpTest.instance( + "auth status only reports credentials stored for the configured server URL", + () => + Effect.gen(function* () { + const mcp = yield* MCP.Service + expect(transportCalls).toHaveLength(0) + yield* McpAuth.use.updateTokens( + "test-status-url", + { accessToken: "old-token" }, + "https://old.example.com/mcp", + ) + + expect(yield* mcp.getAuthStatus("test-status-url")).toBe("not_authenticated") + + yield* McpAuth.use.updateTokens( + "test-status-url", + { accessToken: "current-token" }, + "https://example.com/mcp", + ) + expect(yield* mcp.getAuthStatus("test-status-url")).toBe("authenticated") + + yield* McpAuth.use.updateTokens( + "test-status-url", + { accessToken: "expired-token", expiresAt: 1 }, + "https://example.com/mcp", + ) + expect(yield* mcp.getAuthStatus("test-status-url")).toBe("expired") + expect(transportCalls).toHaveLength(0) + }), + { config: config("test-status-url") }, +) + mcpTest.instance( "authenticate() stores a connected client when auth completes without redirect", () =>