From 0b31e51ec76398418be309148ec0de22d9efcef8 Mon Sep 17 00:00:00 2001 From: Igor Bedesqui Date: Mon, 20 Jul 2026 14:07:06 -0300 Subject: [PATCH] fix(mcp): revoke credentials by provider session --- .../server/src/mcp/McpSessionRegistry.test.ts | 25 +++++++++++++++++++ apps/server/src/mcp/McpSessionRegistry.ts | 12 +++++++++ 2 files changed, 37 insertions(+) diff --git a/apps/server/src/mcp/McpSessionRegistry.test.ts b/apps/server/src/mcp/McpSessionRegistry.test.ts index a91d98febd8..fd6da9c389b 100644 --- a/apps/server/src/mcp/McpSessionRegistry.test.ts +++ b/apps/server/src/mcp/McpSessionRegistry.test.ts @@ -55,6 +55,31 @@ it.effect("stores only a token hash, resolves the bearer token, and revokes by t }), ); +it.effect("revokes one provider session without revoking another credential for the thread", () => + Effect.gen(function* () { + const registry = yield* makeRegistry(() => 1_000); + const threadId = ThreadId.make("thread-selective-revocation"); + const first = yield* registry.issue({ + threadId, + providerInstanceId: ProviderInstanceId.make("codex"), + }); + const second = yield* registry.issue({ + threadId, + providerInstanceId: ProviderInstanceId.make("claudeAgent"), + }); + const firstToken = first.config.authorizationHeader.replace(/^Bearer\s+/, ""); + const secondToken = second.config.authorizationHeader.replace(/^Bearer\s+/, ""); + + yield* registry.revokeProviderSession(second.config.providerSessionId); + + expect(yield* registry.resolve(firstToken)).toMatchObject({ + threadId, + providerSessionId: first.config.providerSessionId, + }); + expect(yield* registry.resolve(secondToken)).toBeUndefined(); + }), +); + it.effect("builds MCP endpoints from the bound server host", () => Effect.gen(function* () { const cases = [ diff --git a/apps/server/src/mcp/McpSessionRegistry.ts b/apps/server/src/mcp/McpSessionRegistry.ts index 67c4f2f0ff0..1e6f85b5fef 100644 --- a/apps/server/src/mcp/McpSessionRegistry.ts +++ b/apps/server/src/mcp/McpSessionRegistry.ts @@ -202,6 +202,18 @@ export const issueActiveMcpCredential = ( .pipe(Effect.andThen(activeMcpSessionRegistry.issue(request))) : Effect.sync((): McpIssuedCredential | undefined => undefined); +export const issueUncommittedMcpCredential = ( + request: McpCredentialRequest, +): Effect.Effect => + activeMcpSessionRegistry + ? activeMcpSessionRegistry.issue(request) + : Effect.sync((): McpIssuedCredential | undefined => undefined); + +export const revokeActiveMcpProviderSession = (providerSessionId: string): Effect.Effect => + activeMcpSessionRegistry + ? activeMcpSessionRegistry.revokeProviderSession(providerSessionId) + : Effect.void; + export const revokeActiveMcpThread = (threadId: ThreadId): Effect.Effect => activeMcpSessionRegistry ? activeMcpSessionRegistry.revokeThread(threadId) : Effect.void;