Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ export function makeCursorAgentSdkReplayRunner(

const waitForRun = <Error>(
runId: string,
sendInput: CursorAgentSdk.CursorAgentSdkSendInput<Error>,
runInput: {
readonly onDelta?: (update: InteractionUpdate) => Effect.Effect<void, Error>;
},
): Effect.Effect<RunResult, CursorAgentSdkReplayError> =>
Effect.gen(function* () {
while (true) {
Expand Down Expand Up @@ -364,7 +366,7 @@ export function makeCursorAgentSdkReplayRunner(
);
}
advance();
yield* (sendInput.onDelta?.(frame.update) ?? Effect.void).pipe(
yield* (runInput.onDelta?.(frame.update) ?? Effect.void).pipe(
Effect.mapError((cause) =>
recordFailure(
new CursorReplayRuntimeError({
Expand Down Expand Up @@ -446,6 +448,34 @@ export function makeCursorAgentSdkReplayRunner(
},
catch: (cause) => replayRunnerError(transcript, cause, "replay.session.send"),
}),
summarize: (summarizeInput) =>
Effect.try({
try: () => {
assertOutbound({
type: "run.summarize",
});
const started = consumeInbound("run.started");
const run: CursorAgentSdk.CursorAgentSdkRun = {
runId: started.runId,
agentId: started.agentId,
wait: waitForRun(started.runId, summarizeInput ?? {}).pipe(
Effect.mapError((cause) =>
replayRunnerError(transcript, cause, "replay.run.wait"),
),
),
cancel: Effect.try({
try: () =>
assertOutbound({
type: "run.cancel",
runId: started.runId,
}),
catch: (cause) => replayRunnerError(transcript, cause, "replay.run.cancel"),
}),
};
return run;
},
catch: (cause) => replayRunnerError(transcript, cause, "replay.session.summarize"),
}),
listMessages: Effect.try({
try: () => {
assertOutbound({
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
16 changes: 14 additions & 2 deletions 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
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,10 +47,11 @@ 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()`;
- native conversation compaction;
- provider conversation snapshots;
- Cursor `task` subagents, projected as read-only child app threads with their tool activity and
final result.

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
164 changes: 164 additions & 0 deletions packages/provider-cursor/src/server/CursorAgentSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export interface CursorAgentSdkSendInput<Error> {
readonly onDelta?: (update: InteractionUpdate) => Effect.Effect<void, Error>;
}

export interface CursorAgentSdkSummarizeInput<Error> {
readonly onDelta?: (update: InteractionUpdate) => Effect.Effect<void, Error>;
}

export interface CursorAgentSdkRun {
readonly runId: string;
readonly agentId: string;
Expand All @@ -103,6 +107,9 @@ export interface CursorAgentSdkSession {
readonly send: <Error>(
input: CursorAgentSdkSendInput<Error>,
) => Effect.Effect<CursorAgentSdkRun, CursorAgentSdkRunnerError>;
readonly summarize: <Error>(
input?: CursorAgentSdkSummarizeInput<Error>,
) => Effect.Effect<CursorAgentSdkRun, CursorAgentSdkRunnerError>;
readonly listMessages: Effect.Effect<ReadonlyArray<AgentMessage>, CursorAgentSdkRunnerError>;
readonly close: Effect.Effect<void, CursorAgentSdkRunnerError>;
}
Expand Down Expand Up @@ -139,6 +146,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 @@ -174,6 +183,13 @@ export type CursorAgentSdkProtocolLogEvent =
readonly options: CursorAgentSdkLoggedSendOptions;
};
}
| {
readonly direction: "outgoing";
readonly stage: "decoded";
readonly payload: {
readonly type: "run.summarize";
};
}
| {
readonly direction: "incoming";
readonly stage: "decoded";
Expand Down Expand Up @@ -298,9 +314,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 All @@ -313,6 +332,25 @@ export function loggedCursorSendOptions(
};
}

function summaryMessageToInteractionUpdate(message: {
readonly type: string;
readonly status?: string;
readonly fallback?: boolean;
}): InteractionUpdate | undefined {
if (message.type !== "summary") return undefined;
if (message.status === "running") {
return { type: "summary-started" };
}
if (message.status === "completed" || message.status === "error") {
return {
type: "summary-completed",
...(message.status === "error" ? { failed: true } : {}),
...(message.fallback === true ? { fallback: true } : {}),
};
}
return undefined;
}

function makeCursorAgentSdkProtocolLogger(input: {
readonly nativeEventLogger: ProviderEventLoggers.EventNdjsonLogger | undefined;
readonly threadId: ThreadId;
Expand Down Expand Up @@ -414,6 +452,106 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu
};
let abandonedRunRecoveryAvailable = input.operation === "resume";

const wrapRun = Effect.fnUntraced(function* <Error>(input: {
readonly run: Awaited<ReturnType<typeof agent.send>>;
readonly method: string;
readonly onDelta?: (update: InteractionUpdate) => Effect.Effect<void, Error>;
readonly pumpStream?: boolean;
}) {
const context = yield* Effect.context();
let callbackFailure: { readonly cause: unknown } | undefined;
let callbackChain = Promise.resolve();
const dispatchUpdate = (update: InteractionUpdate): Promise<void> => {
callbackChain = callbackChain
.then(() => {
if (callbackFailure !== undefined) {
return;
}
return Effect.runPromiseWith(context)(
log({
direction: "incoming",
stage: "decoded",
payload: {
type: "interaction.update",
runId: input.run.id,
update,
},
}).pipe(Effect.andThen(input.onDelta?.(update) ?? Effect.void)),
);
})
.catch((cause) => {
callbackFailure ??= { cause };
});
return callbackChain;
};

// Keep the stream pump off callbackChain — awaiting dispatchUpdate from inside a
// callbackChain task deadlocks when the next link waits for that same task to finish.
const pumpDone =
input.pumpStream === true
? (async () => {
for await (const message of input.run.stream()) {
const update = summaryMessageToInteractionUpdate(message);
if (update !== undefined) {
await dispatchUpdate(update);
}
}
})().catch((cause) => {
callbackFailure ??= { cause };
})
: Promise.resolve();

return {
runId: input.run.id,
agentId: input.run.agentId,
wait: Effect.tryPromise({
try: async () => {
const result = await input.run.wait();
await Promise.all([callbackChain, pumpDone]);
if (callbackFailure !== undefined) {
throw callbackFailure.cause;
}
return result;
},
catch: (cause) => runnerError(cause, `${input.method}.wait`),
}).pipe(
Effect.tap((result) =>
log({
direction: "incoming",
stage: "decoded",
payload: {
type: "run.completed",
result,
},
}),
),
),
cancel: log({
direction: "outgoing",
stage: "decoded",
payload: {
type: "run.cancel",
runId: input.run.id,
},
}).pipe(
Effect.andThen(
Effect.tryPromise({
try: async () => {
try {
await input.run.cancel();
} catch (cause) {
if (!isCursorCancellationError(cause)) {
throw cause;
}
}
},
catch: (cause) => runnerError(cause, `${input.method}.cancel`),
}),
),
),
} satisfies CursorAgentSdkRun;
});

return {
agentId: agent.agentId,
send: Effect.fn("CursorAgentSdkSession.send")(function* (sendInput) {
Expand Down Expand Up @@ -570,6 +708,32 @@ export const makeCursorAgentSdkRunner = Effect.fn("makeCursorAgentSdkRunner")(fu
),
} satisfies CursorAgentSdkRun;
}),
summarize: Effect.fn("CursorAgentSdkSession.summarize")(function* (summarizeInput) {
yield* log({
direction: "outgoing",
stage: "decoded",
payload: { type: "run.summarize" },
});
const run = yield* Effect.tryPromise({
try: () => agent.summarize(),
catch: (cause) => runnerError(cause, "run.summarize"),
});
yield* log({
direction: "incoming",
stage: "decoded",
payload: {
type: "run.started",
runId: run.id,
agentId: run.agentId,
},
});
return yield* wrapRun({
run,
method: "run.summarize",
...(summarizeInput?.onDelta === undefined ? {} : { onDelta: summarizeInput.onDelta }),
pumpStream: true,
});
}),
listMessages: log({
direction: "outgoing",
stage: "decoded",
Expand Down
Loading
Loading