From df5f1fe6fae25d537626287cccc7646f11b90abb Mon Sep 17 00:00:00 2001 From: Arav Jain Date: Sun, 4 Oct 2026 22:37:49 -0500 Subject: [PATCH 1/3] fix(server): preserve ambient Cursor MCP servers --- .../Adapters/CursorAdapterV2.test.ts | 214 ++++++++++++++++++ .../Adapters/CursorAdapterV2.ts | 23 +- 2 files changed, 228 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts index 60ece7a42f3a..acc3803560c8 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts @@ -1,6 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import type { InteractionUpdate } from "@cursor/sdk"; +import type { CursorAgentSdkOpenInput, CursorAgentSdkSendInput } from "./CursorAgentSdk.ts"; import { CursorSettings, EnvironmentId, @@ -39,6 +40,219 @@ import { isCursorCancellationError, loggedCursorAgentOptions } from "./CursorAge const decodeCursorSettings = Schema.decodeEffect(CursorSettings); describe("CursorAdapterV2", () => { + it.effect.each([false, true])( + "keeps MCP configuration on create/resume across credential changes (initial MCP: %s)", + (initialMcp) => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "cursor-mcp-" }); + const instanceId = ProviderInstanceId.make("cursor"); + const threadId = ThreadId.make("cursor-mcp-lifecycle"); + yield* Effect.addFinalizer(() => + Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId)), + ); + const modelSelection = { instanceId, model: "composer-2.5" }; + const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: workspace, + }); + const opened: Array = []; + const sent: Array["options"]> = []; + const calls: Array = []; + const setCredential = ( + authorizationHeader: string, + endpoint = "http://127.0.0.1:43123/mcp", + ) => + McpProviderSession.setMcpProviderSession({ + environmentId: EnvironmentId.make("cursor-mcp-environment"), + threadId, + providerSessionId: "cursor-mcp-session", + providerInstanceId: instanceId, + endpoint, + authorizationHeader, + browserToolsAvailable: true, + }); + const adapter = makeCursorAdapterV2({ + instanceId, + settings: yield* decodeCursorSettings({}), + environment: { HOME: workspace, CURSOR_API_KEY: "synthetic-cursor-api-key" }, + fileSystem, + path, + idAllocator: yield* IdAllocator.IdAllocatorV2, + serverConfig: yield* ServerConfig.ServerConfig.pipe( + Effect.provide(ServerConfig.layerTest(workspace, { prefix: "cursor-mcp-config-" })), + ), + runner: { + assertComplete: Effect.void, + open: (input) => + Effect.sync(() => { + opened.push(input); + calls.push(input.operation); + if (input.operation === "resume") assert.equal(input.agentId, "native-cursor-mcp"); + return { + agentId: "native-cursor-mcp", + listMessages: Effect.succeed([]), + close: Effect.sync(() => { + calls.push("close"); + }), + send: (sendInput) => + Effect.sync(() => { + sent.push(sendInput.options); + calls.push("send"); + const runId = `native-run-${sent.length}`; + return { + agentId: "native-cursor-mcp", + runId, + cancel: Effect.void, + wait: Effect.succeed({ + id: runId, + requestId: runId, + status: "finished" as const, + model: { id: "composer-2.5" }, + durationMs: 1, + }), + }; + }), + }; + }), + }, + }); + const openRuntime = () => + adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("cursor-mcp-session"), + modelSelection, + runtimePolicy, + }); + if (initialMcp) setCredential("Bearer synthetic-created"); + let runtime = yield* openRuntime(); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + const now = yield* DateTime.now; + const turn = Effect.fnUntraced(function* () { + const ordinal = sent.length + 1; + yield* runtime.startTurn({ + threadId, + providerThread, + modelSelection, + runtimePolicy, + runId: RunId.make(`cursor-mcp-run-${ordinal}`), + runOrdinal: ordinal, + providerTurnOrdinal: ordinal, + attemptId: RunAttemptId.make(`cursor-mcp-attempt-${ordinal}`), + rootNodeId: NodeId.make(`cursor-mcp-root-${ordinal}`), + appThread: { + id: threadId, + projectId: ProjectId.make("cursor-mcp-project"), + createdBy: "user", + creationSource: "web", + title: "Cursor lifecycle", + providerInstanceId: instanceId, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + activeProviderThreadId: providerThread.id, + lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, + forkedFrom: null, + createdAt: now, + updatedAt: now, + archivedAt: null, + settledOverride: null, + settledAt: null, + lastVisitedAt: null, + deletedAt: null, + }, + message: { + messageId: MessageId.make(`cursor-mcp-message-${ordinal}`), + createdBy: "user", + creationSource: "web", + text: "List available tools.", + attachments: [], + }, + }); + const events = yield* runtime.events.pipe( + Stream.takeUntil((event) => event.type === "turn.terminal"), + Stream.runCollect, + ); + assert.equal(events.at(-1)?.type, "turn.terminal"); + assert.isFalse(Object.hasOwn(sent.at(-1)!, "mcpServers")); + }); + const assertCredential = ( + authorizationHeader: string, + endpoint = "http://127.0.0.1:43123/mcp", + ) => { + assert.deepEqual(opened.at(-1)?.options.mcpServers, { + "t3-code": { + type: "http", + url: endpoint, + headers: { Authorization: authorizationHeader }, + }, + }); + assert.deepEqual(opened.at(-1)?.options.local?.settingSources, [ + "project", + "user", + "team", + "mdm", + "plugins", + ]); + }; + + // No injected server leaves the SDK free to load ambient settings. + yield* turn(); + assert.deepEqual(calls, ["create", "send"]); + if (initialMcp) assertCredential("Bearer synthetic-created"); + else assert.isUndefined(opened[0]?.options.mcpServers); + + // Installing, rotating, changing the endpoint, and removing the credential + // must each reopen the same native thread before its next send. + setCredential("Bearer synthetic-first"); + yield* turn(); + assert.deepEqual(calls.slice(-3), ["close", "resume", "send"]); + assertCredential("Bearer synthetic-first"); + const beforeReuse = opened.length; + setCredential("Bearer synthetic-first"); + yield* turn(); + assert.equal(opened.length, beforeReuse); + assert.equal(calls.at(-2), "send"); + + setCredential("Bearer synthetic-rotated"); + yield* turn(); + assert.equal(opened.length, beforeReuse + 1); + assert.deepEqual(calls.slice(-3), ["close", "resume", "send"]); + assertCredential("Bearer synthetic-rotated"); + + setCredential("Bearer synthetic-rotated", "http://127.0.0.1:43124/mcp"); + yield* turn(); + assert.equal(opened.length, beforeReuse + 2); + assertCredential("Bearer synthetic-rotated", "http://127.0.0.1:43124/mcp"); + + McpProviderSession.clearMcpProviderSession(threadId); + yield* turn(); + assert.equal(opened.length, beforeReuse + 3); + assert.isUndefined(opened.at(-1)?.options.mcpServers); + assert.deepEqual(calls.slice(-3), ["close", "resume", "send"]); + + // A fresh adapter runtime resumes durable identity with current credentials. + setCredential("Bearer synthetic-resumed"); + runtime = yield* openRuntime(); + const resumed = yield* runtime.resumeThread({ providerThread }); + assert.equal(resumed.id, providerThread.id); + assert.deepEqual(resumed.nativeThreadRef, providerThread.nativeThreadRef); + assertCredential("Bearer synthetic-resumed"); + const beforeResumedSend = opened.length; + yield* turn(); + assert.equal(opened.length, beforeResumedSend); + assert.deepEqual(calls.slice(-2), ["resume", "send"]); + }).pipe(Effect.scoped, Effect.provide(Layer.merge(NodeServices.layer, IdAllocator.layer))), + ); + it.effect.each([ { status: "finished", model: undefined }, { status: "cancelled", model: "claude-opus-4-6" }, diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts index 948d74762492..d4ac933ac04e 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts @@ -33,6 +33,7 @@ import { import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; +import * as Equal from "effect/Equal"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; @@ -839,6 +840,7 @@ interface ActiveCursorTurn { interface CursorLiveAgent { readonly nativeThreadId: string; readonly session: CursorAgentSdk.CursorAgentSdkSession; + readonly mcpServers: AgentOptions["mcpServers"]; } export interface CursorAdapterV2Options { @@ -2072,11 +2074,20 @@ export function makeCursorAdapterV2( readonly runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy; readonly agentId?: string; }) { + // Send-level MCP overrides replace Cursor's ambient servers. Refresh + // rotated credentials by reopening the same native thread instead. + const options = makeCursorAgentOptions({ + ...(apiKey === undefined ? {} : { apiKey }), + modelSelection: openInput.modelSelection, + runtimePolicy: openInput.runtimePolicy, + threadId: openInput.threadId, + }); const existing = yield* Ref.get(liveAgent); if ( existing !== null && openInput.operation === "resume" && - existing.nativeThreadId === openInput.agentId + existing.nativeThreadId === openInput.agentId && + Equal.equals(existing.mcpServers, options.mcpServers) ) { return existing; } @@ -2087,18 +2098,14 @@ export function makeCursorAdapterV2( const sdkSession = yield* runner.open({ operation: openInput.operation, ...(openInput.agentId === undefined ? {} : { agentId: openInput.agentId }), - options: makeCursorAgentOptions({ - ...(apiKey === undefined ? {} : { apiKey }), - modelSelection: openInput.modelSelection, - runtimePolicy: openInput.runtimePolicy, - threadId: openInput.threadId, - }), + options, threadId: openInput.threadId, providerSessionId: input.providerSessionId, }); const next = { nativeThreadId: sdkSession.agentId, session: sdkSession, + mcpServers: options.mcpServers, } satisfies CursorLiveAgent; yield* Ref.set(liveAgent, next); return next; @@ -2202,7 +2209,6 @@ export function makeCursorAdapterV2( runtimePolicy: turnInput.runtimePolicy, }); const message = yield* resolveUserMessage(turnInput); - const mcpServers = cursorMcpServers(turnInput.threadId); const pendingUpdates: Array = []; let context: ActiveCursorTurn | null = null; const sdkRun = yield* agent.session.send({ @@ -2210,7 +2216,6 @@ export function makeCursorAdapterV2( options: { model: cursorSdkModelSelection(turnInput.modelSelection), mode: turnInput.runtimePolicy.interactionMode === "plan" ? "plan" : "agent", - ...(mcpServers === undefined ? {} : { mcpServers }), }, onDelta: (update) => { if (context === null) { From f0d23ff428a1cf89e861bca12aed5b7a3e8a609f Mon Sep 17 00:00:00 2001 From: Arav Jain Date: Sun, 4 Oct 2026 22:49:42 -0500 Subject: [PATCH 2/3] test(server): cover failed Cursor credential refresh --- .../Adapters/CursorAdapterV2.test.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts index acc3803560c8..b1344eb7e120 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts @@ -35,7 +35,11 @@ import { makeCursorAdapterV2, nestedToolCallFromEnvelope, } from "./CursorAdapterV2.ts"; -import { isCursorCancellationError, loggedCursorAgentOptions } from "./CursorAgentSdk.ts"; +import { + CursorAgentSdkRunnerError, + isCursorCancellationError, + loggedCursorAgentOptions, +} from "./CursorAgentSdk.ts"; const decodeCursorSettings = Schema.decodeEffect(CursorSettings); @@ -61,6 +65,8 @@ describe("CursorAdapterV2", () => { const opened: Array = []; const sent: Array["options"]> = []; const calls: Array = []; + let failNextResume = false; + let failNextClose = false; const setCredential = ( authorizationHeader: string, endpoint = "http://127.0.0.1:43123/mcp", @@ -87,15 +93,29 @@ describe("CursorAdapterV2", () => { runner: { assertComplete: Effect.void, open: (input) => - Effect.sync(() => { + Effect.gen(function* () { opened.push(input); calls.push(input.operation); if (input.operation === "resume") assert.equal(input.agentId, "native-cursor-mcp"); + if (failNextResume) { + failNextResume = false; + return yield* new CursorAgentSdkRunnerError({ + method: "agent.resume", + cause: new Error("Synthetic resume failure"), + }); + } return { agentId: "native-cursor-mcp", listMessages: Effect.succeed([]), - close: Effect.sync(() => { + close: Effect.gen(function* () { calls.push("close"); + if (failNextClose) { + failNextClose = false; + return yield* new CursorAgentSdkRunnerError({ + method: "agent.close", + cause: new Error("Synthetic close failure"), + }); + } }), send: (sendInput) => Effect.sync(() => { @@ -250,6 +270,31 @@ describe("CursorAdapterV2", () => { yield* turn(); assert.equal(opened.length, beforeResumedSend); assert.deepEqual(calls.slice(-2), ["resume", "send"]); + + // A failed replacement must not send through, or cache, the old handle. + // Exercise this after both successful and failed close attempts. + setCredential("Bearer synthetic-retry"); + failNextResume = true; + failNextClose = initialMcp; + const sentBeforeFailure = sent.length; + const openedBeforeFailure = opened.length; + const failure = yield* Effect.flip(turn()); + assert.equal(failure._tag, "ProviderAdapterTurnStartError"); + assert.equal(sent.length, sentBeforeFailure); + assert.equal(opened.length, openedBeforeFailure + 1); + assert.deepEqual(calls.slice(-2), ["close", "resume"]); + assertCredential("Bearer synthetic-retry"); + + yield* turn(); + assert.equal(opened.length, openedBeforeFailure + 2); + assert.equal(sent.length, sentBeforeFailure + 1); + assert.deepEqual(calls.slice(-3), ["resume", "resume", "send"]); + assertCredential("Bearer synthetic-retry"); + for (const attempt of opened) { + const logged = loggedCursorAgentOptions(attempt.options); + assert.notProperty(logged, "apiKey"); + assert.notProperty(logged, "mcpServers"); + } }).pipe(Effect.scoped, Effect.provide(Layer.merge(NodeServices.layer, IdAllocator.layer))), ); From a37b1c1479dc58bc6a65db54c90d793177cdd23c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 11 Oct 2026 09:38:08 -0700 Subject: [PATCH 3/3] fix(server): simplify Cursor MCP preservation --- .../Adapters/CursorAdapterV2.test.ts | 277 ++---------------- .../Adapters/CursorAdapterV2.ts | 22 +- 2 files changed, 25 insertions(+), 274 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts index b1344eb7e120..1e1ddadba243 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.test.ts @@ -1,7 +1,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import type { InteractionUpdate } from "@cursor/sdk"; -import type { CursorAgentSdkOpenInput, CursorAgentSdkSendInput } from "./CursorAgentSdk.ts"; import { CursorSettings, EnvironmentId, @@ -35,269 +34,11 @@ import { makeCursorAdapterV2, nestedToolCallFromEnvelope, } from "./CursorAdapterV2.ts"; -import { - CursorAgentSdkRunnerError, - isCursorCancellationError, - loggedCursorAgentOptions, -} from "./CursorAgentSdk.ts"; +import { isCursorCancellationError, loggedCursorAgentOptions } from "./CursorAgentSdk.ts"; const decodeCursorSettings = Schema.decodeEffect(CursorSettings); describe("CursorAdapterV2", () => { - it.effect.each([false, true])( - "keeps MCP configuration on create/resume across credential changes (initial MCP: %s)", - (initialMcp) => - Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "cursor-mcp-" }); - const instanceId = ProviderInstanceId.make("cursor"); - const threadId = ThreadId.make("cursor-mcp-lifecycle"); - yield* Effect.addFinalizer(() => - Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId)), - ); - const modelSelection = { instanceId, model: "composer-2.5" }; - const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ - runtimeMode: "full-access", - interactionMode: "default", - cwd: workspace, - }); - const opened: Array = []; - const sent: Array["options"]> = []; - const calls: Array = []; - let failNextResume = false; - let failNextClose = false; - const setCredential = ( - authorizationHeader: string, - endpoint = "http://127.0.0.1:43123/mcp", - ) => - McpProviderSession.setMcpProviderSession({ - environmentId: EnvironmentId.make("cursor-mcp-environment"), - threadId, - providerSessionId: "cursor-mcp-session", - providerInstanceId: instanceId, - endpoint, - authorizationHeader, - browserToolsAvailable: true, - }); - const adapter = makeCursorAdapterV2({ - instanceId, - settings: yield* decodeCursorSettings({}), - environment: { HOME: workspace, CURSOR_API_KEY: "synthetic-cursor-api-key" }, - fileSystem, - path, - idAllocator: yield* IdAllocator.IdAllocatorV2, - serverConfig: yield* ServerConfig.ServerConfig.pipe( - Effect.provide(ServerConfig.layerTest(workspace, { prefix: "cursor-mcp-config-" })), - ), - runner: { - assertComplete: Effect.void, - open: (input) => - Effect.gen(function* () { - opened.push(input); - calls.push(input.operation); - if (input.operation === "resume") assert.equal(input.agentId, "native-cursor-mcp"); - if (failNextResume) { - failNextResume = false; - return yield* new CursorAgentSdkRunnerError({ - method: "agent.resume", - cause: new Error("Synthetic resume failure"), - }); - } - return { - agentId: "native-cursor-mcp", - listMessages: Effect.succeed([]), - close: Effect.gen(function* () { - calls.push("close"); - if (failNextClose) { - failNextClose = false; - return yield* new CursorAgentSdkRunnerError({ - method: "agent.close", - cause: new Error("Synthetic close failure"), - }); - } - }), - send: (sendInput) => - Effect.sync(() => { - sent.push(sendInput.options); - calls.push("send"); - const runId = `native-run-${sent.length}`; - return { - agentId: "native-cursor-mcp", - runId, - cancel: Effect.void, - wait: Effect.succeed({ - id: runId, - requestId: runId, - status: "finished" as const, - model: { id: "composer-2.5" }, - durationMs: 1, - }), - }; - }), - }; - }), - }, - }); - const openRuntime = () => - adapter.openSession({ - threadId, - providerSessionId: ProviderSessionId.make("cursor-mcp-session"), - modelSelection, - runtimePolicy, - }); - if (initialMcp) setCredential("Bearer synthetic-created"); - let runtime = yield* openRuntime(); - const providerThread = yield* runtime.ensureThread({ - threadId, - modelSelection, - runtimePolicy, - }); - const now = yield* DateTime.now; - const turn = Effect.fnUntraced(function* () { - const ordinal = sent.length + 1; - yield* runtime.startTurn({ - threadId, - providerThread, - modelSelection, - runtimePolicy, - runId: RunId.make(`cursor-mcp-run-${ordinal}`), - runOrdinal: ordinal, - providerTurnOrdinal: ordinal, - attemptId: RunAttemptId.make(`cursor-mcp-attempt-${ordinal}`), - rootNodeId: NodeId.make(`cursor-mcp-root-${ordinal}`), - appThread: { - id: threadId, - projectId: ProjectId.make("cursor-mcp-project"), - createdBy: "user", - creationSource: "web", - title: "Cursor lifecycle", - providerInstanceId: instanceId, - modelSelection, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - activeProviderThreadId: providerThread.id, - lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, - forkedFrom: null, - createdAt: now, - updatedAt: now, - archivedAt: null, - settledOverride: null, - settledAt: null, - lastVisitedAt: null, - deletedAt: null, - }, - message: { - messageId: MessageId.make(`cursor-mcp-message-${ordinal}`), - createdBy: "user", - creationSource: "web", - text: "List available tools.", - attachments: [], - }, - }); - const events = yield* runtime.events.pipe( - Stream.takeUntil((event) => event.type === "turn.terminal"), - Stream.runCollect, - ); - assert.equal(events.at(-1)?.type, "turn.terminal"); - assert.isFalse(Object.hasOwn(sent.at(-1)!, "mcpServers")); - }); - const assertCredential = ( - authorizationHeader: string, - endpoint = "http://127.0.0.1:43123/mcp", - ) => { - assert.deepEqual(opened.at(-1)?.options.mcpServers, { - "t3-code": { - type: "http", - url: endpoint, - headers: { Authorization: authorizationHeader }, - }, - }); - assert.deepEqual(opened.at(-1)?.options.local?.settingSources, [ - "project", - "user", - "team", - "mdm", - "plugins", - ]); - }; - - // No injected server leaves the SDK free to load ambient settings. - yield* turn(); - assert.deepEqual(calls, ["create", "send"]); - if (initialMcp) assertCredential("Bearer synthetic-created"); - else assert.isUndefined(opened[0]?.options.mcpServers); - - // Installing, rotating, changing the endpoint, and removing the credential - // must each reopen the same native thread before its next send. - setCredential("Bearer synthetic-first"); - yield* turn(); - assert.deepEqual(calls.slice(-3), ["close", "resume", "send"]); - assertCredential("Bearer synthetic-first"); - const beforeReuse = opened.length; - setCredential("Bearer synthetic-first"); - yield* turn(); - assert.equal(opened.length, beforeReuse); - assert.equal(calls.at(-2), "send"); - - setCredential("Bearer synthetic-rotated"); - yield* turn(); - assert.equal(opened.length, beforeReuse + 1); - assert.deepEqual(calls.slice(-3), ["close", "resume", "send"]); - assertCredential("Bearer synthetic-rotated"); - - setCredential("Bearer synthetic-rotated", "http://127.0.0.1:43124/mcp"); - yield* turn(); - assert.equal(opened.length, beforeReuse + 2); - assertCredential("Bearer synthetic-rotated", "http://127.0.0.1:43124/mcp"); - - McpProviderSession.clearMcpProviderSession(threadId); - yield* turn(); - assert.equal(opened.length, beforeReuse + 3); - assert.isUndefined(opened.at(-1)?.options.mcpServers); - assert.deepEqual(calls.slice(-3), ["close", "resume", "send"]); - - // A fresh adapter runtime resumes durable identity with current credentials. - setCredential("Bearer synthetic-resumed"); - runtime = yield* openRuntime(); - const resumed = yield* runtime.resumeThread({ providerThread }); - assert.equal(resumed.id, providerThread.id); - assert.deepEqual(resumed.nativeThreadRef, providerThread.nativeThreadRef); - assertCredential("Bearer synthetic-resumed"); - const beforeResumedSend = opened.length; - yield* turn(); - assert.equal(opened.length, beforeResumedSend); - assert.deepEqual(calls.slice(-2), ["resume", "send"]); - - // A failed replacement must not send through, or cache, the old handle. - // Exercise this after both successful and failed close attempts. - setCredential("Bearer synthetic-retry"); - failNextResume = true; - failNextClose = initialMcp; - const sentBeforeFailure = sent.length; - const openedBeforeFailure = opened.length; - const failure = yield* Effect.flip(turn()); - assert.equal(failure._tag, "ProviderAdapterTurnStartError"); - assert.equal(sent.length, sentBeforeFailure); - assert.equal(opened.length, openedBeforeFailure + 1); - assert.deepEqual(calls.slice(-2), ["close", "resume"]); - assertCredential("Bearer synthetic-retry"); - - yield* turn(); - assert.equal(opened.length, openedBeforeFailure + 2); - assert.equal(sent.length, sentBeforeFailure + 1); - assert.deepEqual(calls.slice(-3), ["resume", "resume", "send"]); - assertCredential("Bearer synthetic-retry"); - for (const attempt of opened) { - const logged = loggedCursorAgentOptions(attempt.options); - assert.notProperty(logged, "apiKey"); - assert.notProperty(logged, "mcpServers"); - } - }).pipe(Effect.scoped, Effect.provide(Layer.merge(NodeServices.layer, IdAllocator.layer))), - ); - it.effect.each([ { status: "finished", model: undefined }, { status: "cancelled", model: "claude-opus-4-6" }, @@ -313,6 +54,19 @@ describe("CursorAdapterV2", () => { }); const instanceId = ProviderInstanceId.make("cursor"); const threadId = ThreadId.make("cursor-lifecycle-thread"); + McpProviderSession.setMcpProviderSession({ + environmentId: EnvironmentId.make("cursor-lifecycle-environment"), + threadId, + providerSessionId: "cursor-lifecycle-session", + providerInstanceId: instanceId, + endpoint: "http://127.0.0.1:43123/mcp", + authorizationHeader: "Bearer synthetic-cursor-token", + browserToolsAvailable: true, + }); + yield* Effect.addFinalizer(() => + Effect.sync(() => McpProviderSession.clearMcpProviderSession(threadId)), + ); + let sentWithMcpOverride = true; const modelSelection = { instanceId, model: "composer-2.5" }; const runtimePolicy = ProviderAdapterV2RuntimePolicy.make({ runtimeMode: "full-access", @@ -340,6 +94,7 @@ describe("CursorAdapterV2", () => { close: Effect.void, send: (input) => Effect.gen(function* () { + sentWithMcpOverride = Object.hasOwn(input.options ?? {}, "mcpServers"); // Recorded Cursor task calls (see fixtures/subagent) stream a // partial-tool-call before tool-call-started with the same args. // The run then ends without tool-call-completed, which a live @@ -436,6 +191,8 @@ describe("CursorAdapterV2", () => { Stream.takeUntil((event) => event.type === "turn.terminal"), Stream.runCollect, ); + // Keep injected MCP at agent creation so sends preserve Cursor's ambient servers. + assert.isFalse(sentWithMcpOverride); const rows = events.filter((event) => event.type === "subagent.updated"); assert.equal(rows[0]?.subagent.status, "running"); assert.equal(rows[0]?.subagent.model, model ?? null); diff --git a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts index d4ac933ac04e..0e20b072e176 100644 --- a/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CursorAdapterV2.ts @@ -33,7 +33,6 @@ import { import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; -import * as Equal from "effect/Equal"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; @@ -840,7 +839,6 @@ interface ActiveCursorTurn { interface CursorLiveAgent { readonly nativeThreadId: string; readonly session: CursorAgentSdk.CursorAgentSdkSession; - readonly mcpServers: AgentOptions["mcpServers"]; } export interface CursorAdapterV2Options { @@ -2074,20 +2072,11 @@ export function makeCursorAdapterV2( readonly runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy; readonly agentId?: string; }) { - // Send-level MCP overrides replace Cursor's ambient servers. Refresh - // rotated credentials by reopening the same native thread instead. - const options = makeCursorAgentOptions({ - ...(apiKey === undefined ? {} : { apiKey }), - modelSelection: openInput.modelSelection, - runtimePolicy: openInput.runtimePolicy, - threadId: openInput.threadId, - }); const existing = yield* Ref.get(liveAgent); if ( existing !== null && openInput.operation === "resume" && - existing.nativeThreadId === openInput.agentId && - Equal.equals(existing.mcpServers, options.mcpServers) + existing.nativeThreadId === openInput.agentId ) { return existing; } @@ -2098,14 +2087,18 @@ export function makeCursorAdapterV2( const sdkSession = yield* runner.open({ operation: openInput.operation, ...(openInput.agentId === undefined ? {} : { agentId: openInput.agentId }), - options, + options: makeCursorAgentOptions({ + ...(apiKey === undefined ? {} : { apiKey }), + modelSelection: openInput.modelSelection, + runtimePolicy: openInput.runtimePolicy, + threadId: openInput.threadId, + }), threadId: openInput.threadId, providerSessionId: input.providerSessionId, }); const next = { nativeThreadId: sdkSession.agentId, session: sdkSession, - mcpServers: options.mcpServers, } satisfies CursorLiveAgent; yield* Ref.set(liveAgent, next); return next; @@ -2211,6 +2204,7 @@ export function makeCursorAdapterV2( const message = yield* resolveUserMessage(turnInput); const pendingUpdates: Array = []; let context: ActiveCursorTurn | null = null; + // MCP send overrides replace the ambient servers loaded when the agent opens. const sdkRun = yield* agent.session.send({ message, options: {