fix(mcp): scope auth status to server URL (#33924)
This commit is contained in:
parent
21c0e03a70
commit
3fbc005f26
4 changed files with 49 additions and 21 deletions
|
|
@ -669,14 +669,20 @@ export const McpDebugCommand = effectCmd({
|
||||||
const config = yield* Config.Service.use((cfg) => cfg.get())
|
const config = yield* Config.Service.use((cfg) => cfg.get())
|
||||||
const mcp = yield* MCP.Service
|
const mcp = yield* MCP.Service
|
||||||
const auth = yield* McpAuth.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 () => {
|
yield* Effect.promise(async () => {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("MCP OAuth Debug")
|
prompts.intro("MCP OAuth Debug")
|
||||||
|
|
||||||
const mcpServers = config.mcp ?? {}
|
|
||||||
const serverName = args.name
|
const serverName = args.name
|
||||||
|
|
||||||
const serverConfig = mcpServers[serverName]
|
|
||||||
if (!serverConfig) {
|
if (!serverConfig) {
|
||||||
prompts.log.error(`MCP server not found: ${serverName}`)
|
prompts.log.error(`MCP server not found: ${serverName}`)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
|
|
@ -698,13 +704,7 @@ export const McpDebugCommand = effectCmd({
|
||||||
prompts.log.info(`Server: ${serverName}`)
|
prompts.log.info(`Server: ${serverName}`)
|
||||||
prompts.log.info(`URL: ${serverConfig.url}`)
|
prompts.log.info(`URL: ${serverConfig.url}`)
|
||||||
|
|
||||||
// Check stored auth status — services already in hand, run inline.
|
const { authStatus, entry } = authInfo!
|
||||||
const { authStatus, entry } = await Effect.runPromise(
|
|
||||||
Effect.all({
|
|
||||||
authStatus: mcp.getAuthStatus(serverName),
|
|
||||||
entry: auth.get(serverName),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
prompts.log.info(`Auth status: ${getAuthStatusIcon(authStatus)} ${getAuthStatusText(authStatus)}`)
|
||||||
|
|
||||||
if (entry?.tokens) {
|
if (entry?.tokens) {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ export interface Interface {
|
||||||
readonly updateOAuthState: (mcpName: string, oauthState: string) => Effect.Effect<void>
|
readonly updateOAuthState: (mcpName: string, oauthState: string) => Effect.Effect<void>
|
||||||
readonly getOAuthState: (mcpName: string) => Effect.Effect<string | undefined>
|
readonly getOAuthState: (mcpName: string) => Effect.Effect<string | undefined>
|
||||||
readonly clearOAuthState: (mcpName: string) => Effect.Effect<void>
|
readonly clearOAuthState: (mcpName: string) => Effect.Effect<void>
|
||||||
readonly isTokenExpired: (mcpName: string) => Effect.Effect<boolean | null>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {}
|
||||||
|
|
@ -142,13 +141,6 @@ export const layer = Layer.effect(
|
||||||
return entry?.oauthState
|
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({
|
return Service.of({
|
||||||
all,
|
all,
|
||||||
get,
|
get,
|
||||||
|
|
@ -162,7 +154,6 @@ export const layer = Layer.effect(
|
||||||
updateOAuthState,
|
updateOAuthState,
|
||||||
getOAuthState,
|
getOAuthState,
|
||||||
clearOAuthState,
|
clearOAuthState,
|
||||||
isTokenExpired,
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -963,10 +963,15 @@ export const layer = Layer.effect(
|
||||||
})
|
})
|
||||||
|
|
||||||
const getAuthStatus = Effect.fn("MCP.getAuthStatus")(function* (mcpName: string) {
|
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"
|
if (!entry?.tokens) return "not_authenticated"
|
||||||
const expired = yield* auth.isTokenExpired(mcpName)
|
if (entry.tokens.expiresAt && entry.tokens.expiresAt < Date.now() / 1000) return "expired"
|
||||||
return expired ? "expired" : "authenticated"
|
return "authenticated"
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
|
|
|
||||||
|
|
@ -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(
|
mcpTest.instance(
|
||||||
"authenticate() stores a connected client when auth completes without redirect",
|
"authenticate() stores a connected client when auth completes without redirect",
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue