Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.276",
"@cursor/sdk": "1.0.38",
"@cursor/sdk": "1.2.0",
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
"@ff-labs/fff-node": "0.11.0",
Expand Down
7 changes: 5 additions & 2 deletions docs/orchestration-v2/orchestrator-mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,11 @@ server headers or token.
### Cursor Agent SDK V2

Cursor receives the same authenticated HTTP MCP endpoint through the SDK's
`mcpServers` agent and send options. The adapter passes the authorization header
to the SDK but projects only redacted option metadata into protocol diagnostics.
`mcpServers` on `Agent.create` / `Agent.resume` and each `send`. With
`@cursor/sdk` 1.0.39+, send-level servers replace only the create-time inline
set; ambient servers from `settingSources` (`project`, `user`, `team`, `mdm`,
`plugins`) stay available. The adapter passes the authorization header to the
SDK but projects only redacted option metadata into protocol diagnostics.

### Grok ACP V2

Expand Down
13 changes: 12 additions & 1 deletion docs/user/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ The adapter currently uses the SDK's local-agent runtime so runs operate in the
workspace. Cursor cloud agents need repository and cloud-environment configuration that T3 Code does
not expose yet.

Custom Cursor MCP servers and plugins are not configured in T3 Code. They load from the environment
machine's Cursor config when the agent opens: project `.cursor/mcp.json`, user `~/.cursor/mcp.json`,
and installed plugins under `~/.cursor/plugins`. Plugin tools
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
can appear without a login; OAuth MCP servers still need credentials. Run `agent mcp login <server>`
with the cwd set to the T3 project root (tokens land in
`~/.cursor/projects/<folder-slug>/mcp-auth.json`). Cursor desktop Connections auth does not write
that file, and the local SDK cannot complete interactive MCP OAuth itself—auth attempts fail with
messages such as "MCP authentication is not supported in local SDK runs" or that the auth server
does not support dynamic client registration. After you add or change skills, plugins, or MCP
servers, or after a successful CLI login, use **Restart agent session**.

## Sign in

Choose **Sign in**, then open the sign-in page and complete it in your browser. T3 Code
Expand All @@ -36,7 +47,7 @@ The adapter supports:
- creating and resuming local Cursor agent threads;
- changing the model and model parameters between turns;
- assistant text, reasoning, tool activity, plans, and todo streaming;
- thread-scoped T3 Code MCP tools;
- thread-scoped T3 Code MCP tools alongside ambient Cursor MCP/plugins from disk;
- image attachments;
- interruption, queued app messages, and orchestrator-owned interrupt/restart steering;
- provider conversation snapshots through `Agent.messages.list()`;
Expand Down
2 changes: 1 addition & 1 deletion packages/provider-cursor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"test": "vp test run --config ../../vite.config.ts --dir ."
},
"dependencies": {
"@cursor/sdk": "1.0.38",
"@cursor/sdk": "1.2.0",
"@napi-rs/keyring": "^1.3.0",
"@t3tools/contracts": "workspace:*",
"@t3tools/provider-core": "workspace:*",
Expand Down
29 changes: 19 additions & 10 deletions packages/provider-cursor/src/server/CursorAgentSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export interface CursorAgentSdkSendInput<Error> {
readonly message: string | SDKUserMessage;
readonly options?: Omit<SendOptions, "onDelta">;
readonly onDelta?: (update: InteractionUpdate) => Effect.Effect<void, Error>;
/** Runs `agent.summarize()` instead, which takes no message or options. */
readonly summarize?: boolean;
}

export interface CursorAgentSdkRun {
Expand Down Expand Up @@ -139,6 +141,8 @@ export interface CursorAgentSdkLoggedAgentOptions {
export interface CursorAgentSdkLoggedSendOptions {
readonly model?: SendOptions["model"];
readonly mode?: SendOptions["mode"];
readonly hasMcpServers?: boolean;
readonly mcpServerNames?: ReadonlyArray<string>;
readonly local?: {
readonly force?: boolean;
readonly hasCustomTools?: boolean;
Expand Down Expand Up @@ -298,9 +302,12 @@ export function loggedCursorSendOptions(
if (options === undefined) {
return {};
}
const mcpServerNames =
options.mcpServers === undefined ? undefined : Object.keys(options.mcpServers).toSorted();
return {
...(options.model === undefined ? {} : { model: options.model }),
...(options.mode === undefined ? {} : { mode: options.mode }),
...(mcpServerNames === undefined ? {} : { hasMcpServers: true, mcpServerNames }),
...(options.local === undefined
? {}
: {
Expand Down Expand Up @@ -460,16 +467,18 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu
const startRun = () =>
Effect.tryPromise({
try: () =>
agent.send(sendInput.message, {
...sendInput.options,
onDelta: async ({ update }) => {
if (!callbacksReady) {
pendingUpdates.push(update);
return;
}
await dispatchUpdate(update);
},
}),
sendInput.summarize === true
? agent.summarize()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
: agent.send(sendInput.message, {
...sendInput.options,
onDelta: async ({ update }) => {
if (!callbacksReady) {
pendingUpdates.push(update);
return;
}
await dispatchUpdate(update);
},
}),
catch: (cause) => runnerError(cause, "run.start"),
});
const run = yield* startRun().pipe(
Expand Down
26 changes: 13 additions & 13 deletions packages/provider-cursor/src/server/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ describe("CursorAdapterV2", () => {
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
);
assert.isFalse(sentWithMcpOverride);
assert.isTrue(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);
Expand Down Expand Up @@ -508,11 +508,11 @@ describe("CursorAdapterV2", () => {
),
);

it.effect("fails standalone SDK transport diagnostics and sends compaction as /compress", () =>
it.effect("fails standalone SDK transport diagnostics and compacts with SDK summarize", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "cursor-v2-errors-" });
const sentMessages: Array<string> = [];
const summarizeRequests: Array<boolean> = [];
let sendCalls = 0;
const instanceId = ProviderInstanceId.make("cursor");
const threadId = ThreadId.make("cursor-transport-error-thread");
Expand All @@ -536,9 +536,7 @@ describe("CursorAdapterV2", () => {
send: (input) =>
Effect.sync(() => {
sendCalls += 1;
sentMessages.push(
typeof input.message === "string" ? input.message : input.message.text,
);
summarizeRequests.push(input.summarize === true);
const runId = `native-cursor-run-${sendCalls}`;
return {
agentId: "native-cursor-error",
Expand All @@ -549,9 +547,10 @@ describe("CursorAdapterV2", () => {
status: "finished" as const,
model: { id: "composer-2.5" },
durationMs: 1,
...(sendCalls === 1
? { result: "Error: RetriableError: WritableIterable is closed" }
: {}),
result:
sendCalls === 1
? "Error: RetriableError: WritableIterable is closed"
: "Summary of the conversation so far",
}),
cancel: Effect.void,
};
Expand Down Expand Up @@ -644,11 +643,12 @@ describe("CursorAdapterV2", () => {
attachments: [],
},
});
yield* runtime.events.pipe(
Stream.filter((event) => event.type === "turn.terminal"),
Stream.runHead,
const compactionEvents = yield* runtime.events.pipe(
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
);
assert.equal(sentMessages[1], "/compress");
assert.deepEqual(summarizeRequests, [false, true]);
assert.notInclude(JSON.stringify(compactionEvents), "Summary of the conversation so far");
}).pipe(
Effect.scoped,
Effect.provide(
Expand Down
8 changes: 6 additions & 2 deletions packages/provider-cursor/src/server/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2385,15 +2385,18 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* (
hasDeviceTools,
delivered,
);
const compacted = message === "/compress";
const pendingUpdates: Array<InteractionUpdate> = [];
let context: ActiveCursorTurn | null = null;
// MCP send overrides replace the ambient servers loaded when the agent opens.
// Create-time and send-time mcpServers merge with settingSources (SDK >= 1.0.39).
const sdkRun = yield* agent.session
.send({
message,
summarize: compacted,
options: {
model: cursorSdkModelSelection(turnInput.modelSelection),
mode: turnInput.runtimePolicy.interactionMode === "plan" ? "plan" : "agent",
...(mcpServers === undefined ? {} : { mcpServers }),
},
onDelta: (update) => {
if (context === null) {
Expand All @@ -2405,7 +2408,6 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* (
},
})
.pipe(Effect.mapError(classifyCursorModelRejection(turnInput.modelSelection)));
const compacted = message === "/compress";
yield* Ref.set(told, {
agent: agent.nativeThreadId,
orchestration: !compacted && (delivered.orchestration || mcpServers !== undefined),
Expand Down Expand Up @@ -2469,6 +2471,8 @@ export const makeCursorAdapterV2 = Effect.fn("makeCursorAdapterV2")(function* (
if (
context !== null &&
context.assistant.nextSegment === 0 &&
// A summarize run's result is the summary, not an answer.
!compacted &&
// Text from a resume that made no progress is not the turn's answer.
!isResumeFailureAfterTurnEnded(context, result) &&
result.result !== undefined &&
Expand Down
54 changes: 27 additions & 27 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ minimumReleaseAgeExclude:
- "@pierre/highlights@0.2.0"
- "@pierre/theming@1.0.2"
- agent-device@0.21.32
- "@cursor/sdk-darwin-arm64@1.2.0"
- "@cursor/sdk-darwin-x64@1.2.0"
- "@cursor/sdk-linux-arm64@1.2.0"
- "@cursor/sdk-linux-x64@1.2.0"
- "@cursor/sdk-win32-x64@1.2.0"
- "@cursor/sdk@1.2.0"

overrides:
# Pin every patched package, including transitive dependencies, so lockfile-free
Expand Down
Loading