cursor-opencode-provider 0.7.6 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +9 -7
  2. package/dist/agent-url.js +2 -2
  3. package/dist/auth-renewal.d.ts +85 -0
  4. package/dist/auth-renewal.js +284 -0
  5. package/dist/auth.d.ts +38 -22
  6. package/dist/auth.js +79 -96
  7. package/dist/classic-tools.d.ts +15 -0
  8. package/dist/classic-tools.js +45 -0
  9. package/dist/context/build.d.ts +42 -5
  10. package/dist/context/build.js +96 -92
  11. package/dist/context/dynamic-catalog.d.ts +2 -58
  12. package/dist/context/dynamic-catalog.js +10 -160
  13. package/dist/context/epoch.d.ts +6 -4
  14. package/dist/context/epoch.js +22 -10
  15. package/dist/context/frozen.js +37 -14
  16. package/dist/context/index.d.ts +1 -1
  17. package/dist/context/index.js +1 -1
  18. package/dist/context/overlay.d.ts +2 -23
  19. package/dist/context/overlay.js +6 -30
  20. package/dist/context/paths.d.ts +7 -4
  21. package/dist/context/paths.js +13 -9
  22. package/dist/context/rules.d.ts +3 -15
  23. package/dist/context/rules.js +10 -186
  24. package/dist/debug.d.ts +2 -0
  25. package/dist/debug.js +4 -0
  26. package/dist/errors.d.ts +13 -0
  27. package/dist/errors.js +27 -3
  28. package/dist/host-agent-mode.js +2 -2
  29. package/dist/image-save.js +3 -3
  30. package/dist/index.d.ts +10 -0
  31. package/dist/language-model.d.ts +41 -8
  32. package/dist/language-model.js +391 -98
  33. package/dist/opencode2/integration.d.ts +30 -7
  34. package/dist/opencode2/integration.js +75 -46
  35. package/dist/plan-execution-kickoff.js +2 -2
  36. package/dist/plugin-opencode2.js +10 -16
  37. package/dist/plugin.d.ts +0 -10
  38. package/dist/plugin.js +138 -109
  39. package/dist/protocol/blob-store.js +3 -2
  40. package/dist/protocol/conversation-bind.js +1 -1
  41. package/dist/protocol/conversation-persistence.d.ts +2 -0
  42. package/dist/protocol/conversation-persistence.js +11 -1
  43. package/dist/protocol/conversation-state.d.ts +2 -0
  44. package/dist/protocol/conversation-state.js +19 -0
  45. package/dist/protocol/create-plan.js +2 -1
  46. package/dist/protocol/messages.js +22 -4
  47. package/dist/protocol/request.d.ts +11 -9
  48. package/dist/protocol/request.js +15 -25
  49. package/dist/protocol/tools.d.ts +17 -10
  50. package/dist/protocol/tools.js +53 -93
  51. package/dist/protocol/turn-provenance.d.ts +52 -0
  52. package/dist/protocol/turn-provenance.js +174 -0
  53. package/dist/session.d.ts +9 -1
  54. package/dist/shared.d.ts +7 -0
  55. package/dist/shared.js +7 -0
  56. package/package.json +1 -1
  57. package/dist/context/agents.d.ts +0 -12
  58. package/dist/context/agents.js +0 -136
  59. package/dist/context/skills.d.ts +0 -11
  60. package/dist/context/skills.js +0 -110
@@ -9,6 +9,7 @@
9
9
  * At a successful TurnEnded it is compacted to the graph reachable from the
10
10
  * latest checkpoint, matching Cursor CLI's conversation-export traversal.
11
11
  */
12
+ import { errorMessage } from "../debug.js";
12
13
  import { collectReachableConversationBlobIds } from "./blob-reachability.js";
13
14
  function hex(b) {
14
15
  let s = "";
@@ -76,7 +77,7 @@ export function inspectConversationBlobGraph(conversationId, checkpoint) {
76
77
  return { count, bytes, complete: true };
77
78
  }
78
79
  catch (error) {
79
- return retainedStats(error instanceof Error ? error.message : String(error));
80
+ return retainedStats(errorMessage(error));
80
81
  }
81
82
  }
82
83
  /** Copy all durable blobs so a completed turn can be persisted atomically. */
@@ -131,7 +132,7 @@ export function compactConversationBlobs(conversationId, checkpoint) {
131
132
  };
132
133
  }
133
134
  catch (error) {
134
- return fallback(error instanceof Error ? error.message : String(error));
135
+ return fallback(errorMessage(error));
135
136
  }
136
137
  }
137
138
  /** Replace the in-memory blob bucket when restoring a persisted conversation. */
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { clearCheckpoint } from "./checkpoint.js";
3
3
  import { clearConversationBlobs } from "./blob-store.js";
4
- import { transferFrozenRequestContext, } from "../context/frozen.js";
4
+ import { transferFrozenRequestContext } from "../context/frozen.js";
5
5
  /**
6
6
  * OpenCode session key → active Cursor conversation_id.
7
7
  *
@@ -11,6 +11,8 @@ export type PersistedConversation = {
11
11
  postCompactionRebase: boolean;
12
12
  hostAgent?: string;
13
13
  systemPromptHash?: string;
14
+ /** JSON-encoded TurnProvenance (what this provider emitted last). */
15
+ turnProvenance?: string;
14
16
  };
15
17
  export type ConversationLoadStatus = "restored" | "missing" | "invalid" | "expired";
16
18
  export type PersistedConversationLoad = {
@@ -47,7 +47,8 @@ function cloneConversation(value) {
47
47
  *
48
48
  * ConversationCache: schema_version=1, session_key=2, conversation_id=3,
49
49
  * updated_at=4, checkpoint=5, blobs=6, request_context=7, tool_catalog=8,
50
- * post_compaction_rebase=9, host_agent=10, system_prompt_hash=11.
50
+ * post_compaction_rebase=9, host_agent=10, system_prompt_hash=11,
51
+ * turn_provenance_json=12.
51
52
  * Blob: id=1, data=2. Tool: name=1, description=2,
52
53
  * input_schema_json=3, source_name=4.
53
54
  *
@@ -176,6 +177,8 @@ function encodeCacheFile(value) {
176
177
  writer.uint32(fieldTag(10, 2)).string(value.hostAgent);
177
178
  if (value.systemPromptHash)
178
179
  writer.uint32(fieldTag(11, 2)).string(value.systemPromptHash);
180
+ if (value.turnProvenance)
181
+ writer.uint32(fieldTag(12, 2)).string(value.turnProvenance);
179
182
  return { protobufBytes: writer.finish(), requestContextBytes: requestContext.length };
180
183
  }
181
184
  function decodeProtobuf(data) {
@@ -191,6 +194,7 @@ function decodeProtobuf(data) {
191
194
  let postCompactionRebase = false;
192
195
  let hostAgent;
193
196
  let systemPromptHash;
197
+ let turnProvenance;
194
198
  while (reader.pos < reader.len) {
195
199
  const tag = reader.uint32();
196
200
  const wireType = tag & 7;
@@ -250,6 +254,11 @@ function decodeProtobuf(data) {
250
254
  throw new Error("invalid system prompt hash");
251
255
  systemPromptHash = reader.string();
252
256
  break;
257
+ case 12:
258
+ if (wireType !== 2)
259
+ throw new Error("invalid turn provenance");
260
+ turnProvenance = reader.string();
261
+ break;
253
262
  default:
254
263
  reader.skipType(wireType);
255
264
  }
@@ -270,6 +279,7 @@ function decodeProtobuf(data) {
270
279
  postCompactionRebase,
271
280
  ...(hostAgent ? { hostAgent } : {}),
272
281
  ...(systemPromptHash ? { systemPromptHash } : {}),
282
+ ...(turnProvenance ? { turnProvenance } : {}),
273
283
  };
274
284
  }
275
285
  function decodeCacheFile(compressed, expectedSessionKey) {
@@ -7,6 +7,8 @@ export declare function hydrateConversationState(cacheDir: string, sessionKey: s
7
7
  hostAgent?: string;
8
8
  systemPromptHash?: string;
9
9
  } | undefined>;
10
+ /** Restore only turn provenance when its in-memory entry was evicted. */
11
+ export declare function hydrateTurnProvenance(cacheDir: string, sessionKey: string): Promise<void>;
10
12
  /** Persist the complete resumable state only after Cursor confirms TurnEnded. */
11
13
  export declare function persistConversationState(cacheDir: string, input: {
12
14
  sessionKey: string;
@@ -3,6 +3,7 @@ import { getFrozenRequestContext, setFrozenRequestContext, } from "../context/fr
3
3
  import { hasConversationBinding, isActiveConversationBinding, restoreConversationBinding, } from "./conversation-bind.js";
4
4
  import { compactConversationBlobs, restoreConversationBlobs, } from "./blob-store.js";
5
5
  import { getCheckpoint, setCheckpoint } from "./checkpoint.js";
6
+ import { getTurnProvenance, parseTurnProvenance, restoreTurnProvenance, serializeTurnProvenance, } from "./turn-provenance.js";
6
7
  import { deletePersistedConversation, loadPersistedConversation, persistConversation, } from "./conversation-persistence.js";
7
8
  /** Restore one OpenCode session before its conversation binding is resolved. */
8
9
  export async function hydrateConversationState(cacheDir, sessionKey) {
@@ -20,6 +21,9 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
20
21
  setCheckpoint(persisted.conversationId, persisted.checkpoint);
21
22
  restoreConversationBlobs(persisted.conversationId, persisted.blobs);
22
23
  setFrozenRequestContext(persisted.conversationId, persisted.requestContext);
24
+ const provenance = persisted.turnProvenance ? parseTurnProvenance(persisted.turnProvenance) : undefined;
25
+ if (provenance?.conversationId === persisted.conversationId)
26
+ restoreTurnProvenance(sessionKey, provenance);
23
27
  trace(`conversation persistence: restored sessionKey=${sessionKey} ` +
24
28
  `conversationId=${persisted.conversationId} checkpoint=${persisted.checkpoint?.length ?? 0}B ` +
25
29
  `blobs=${persisted.blobs.length}`);
@@ -31,6 +35,17 @@ export async function hydrateConversationState(cacheDir, sessionKey) {
31
35
  ...(persisted.systemPromptHash ? { systemPromptHash: persisted.systemPromptHash } : {}),
32
36
  };
33
37
  }
38
+ /** Restore only turn provenance when its in-memory entry was evicted. */
39
+ export async function hydrateTurnProvenance(cacheDir, sessionKey) {
40
+ if (getTurnProvenance(sessionKey))
41
+ return;
42
+ const persisted = (await loadPersistedConversation(cacheDir, sessionKey)).value;
43
+ if (!persisted?.turnProvenance)
44
+ return;
45
+ const provenance = parseTurnProvenance(persisted.turnProvenance);
46
+ if (provenance?.conversationId === persisted.conversationId)
47
+ restoreTurnProvenance(sessionKey, provenance);
48
+ }
34
49
  /** Persist the complete resumable state only after Cursor confirms TurnEnded. */
35
50
  export async function persistConversationState(cacheDir, input) {
36
51
  // A newer Run may have reset/superseded this conversation while its final
@@ -44,6 +59,7 @@ export async function persistConversationState(cacheDir, input) {
44
59
  const blobCompaction = compactConversationBlobs(input.conversationId, checkpoint);
45
60
  const blobs = blobCompaction.blobs;
46
61
  const requestContext = getFrozenRequestContext(input.conversationId) ?? input.requestContext;
62
+ const provenance = getTurnProvenance(input.sessionKey);
47
63
  await persistConversation(cacheDir, {
48
64
  sessionKey: input.sessionKey,
49
65
  conversationId: input.conversationId,
@@ -54,6 +70,9 @@ export async function persistConversationState(cacheDir, input) {
54
70
  postCompactionRebase: input.postCompactionRebase,
55
71
  hostAgent: input.hostAgent,
56
72
  systemPromptHash: input.systemPromptHash,
73
+ ...(provenance?.conversationId === input.conversationId
74
+ ? { turnProvenance: serializeTurnProvenance(provenance) }
75
+ : {}),
57
76
  });
58
77
  trace(`conversation persistence: saved sessionKey=${input.sessionKey} ` +
59
78
  `conversationId=${input.conversationId} checkpoint=${checkpoint?.length ?? 0}B ` +
@@ -21,6 +21,7 @@ import { pathToFileURL } from "node:url";
21
21
  import { hostPlansDir } from "../context/paths.js";
22
22
  import { parseAnswerSegments, } from "./ask-question.js";
23
23
  import { decodeMessageSparse } from "./messages.js";
24
+ import { errorMessage } from "../debug.js";
24
25
  const PLAN_ADJECTIVES = [
25
26
  "brave",
26
27
  "calm",
@@ -344,7 +345,7 @@ export function writeOpencodePlanFile(args, workspaceRoot, created = Date.now())
344
345
  writeFileSync(planPath, markdown, "utf-8");
345
346
  }
346
347
  catch (err) {
347
- const message = err instanceof Error ? err.message : String(err);
348
+ const message = errorMessage(err);
348
349
  return { ok: false, error: `Failed to write plan file: ${message}` };
349
350
  }
350
351
  return {
@@ -1062,9 +1062,27 @@ export function createMessageTypes() {
1062
1062
  { id: 20, name: "is_working_dir_home_dir", type: "bool" },
1063
1063
  { id: 21, name: "process_working_directory", type: "string" },
1064
1064
  ]);
1065
+ // Cursor applies a rule by its type: `global` is the CLI's `alwaysApply: true`
1066
+ // (its AGENTS.md / CLAUDE.md default). An untyped rule is only attachable on
1067
+ // request, so its content never reaches the model on its own.
1068
+ addType(root, "CursorRuleTypeGlobal", []);
1069
+ addType(root, "CursorRuleTypeFileGlobs", [
1070
+ { id: 1, name: "globs", type: "string", repeated: true },
1071
+ ]);
1072
+ addType(root, "CursorRuleTypeAgentFetched", [
1073
+ { id: 1, name: "description", type: "string" },
1074
+ ]);
1075
+ addType(root, "CursorRuleTypeManuallyAttached", []);
1076
+ addType(root, "CursorRuleType", [
1077
+ { id: 1, name: "global", type: "CursorRuleTypeGlobal" },
1078
+ { id: 2, name: "file_globbed", type: "CursorRuleTypeFileGlobs" },
1079
+ { id: 3, name: "agent_fetched", type: "CursorRuleTypeAgentFetched" },
1080
+ { id: 4, name: "manually_attached", type: "CursorRuleTypeManuallyAttached" },
1081
+ ], [{ name: "type", fields: ["global", "file_globbed", "agent_fetched", "manually_attached"] }]);
1065
1082
  addType(root, "CursorRule", [
1066
1083
  { id: 1, name: "full_path", type: "string" },
1067
1084
  { id: 2, name: "content", type: "string" },
1085
+ { id: 3, name: "type", type: "CursorRuleType" },
1068
1086
  ]);
1069
1087
  addType(root, "RepositoryIndexingInfo", [
1070
1088
  { id: 1, name: "relative_workspace_path", type: "string" },
@@ -1151,8 +1169,8 @@ export function createMessageTypes() {
1151
1169
  { id: 1, name: "success", type: "RequestContextSuccess" },
1152
1170
  ]);
1153
1171
  // Cursor probes MCP server availability before emitting an MCP-backed tool
1154
- // call. OpenCode owns those servers, so answer from the descriptors already
1155
- // advertised in RequestContext rather than surfacing this as a user tool.
1172
+ // call. OpenCode owns those servers, so answer from the live host catalog
1173
+ // rather than surfacing this as a user tool.
1156
1174
  addType(root, "McpStateExecArgs", [
1157
1175
  { id: 1, name: "server_identifiers", type: "string", repeated: true },
1158
1176
  { id: 2, name: "kick_only", type: "bool" },
@@ -1334,8 +1352,8 @@ export function createMessageTypes() {
1334
1352
  { id: 2, name: "message_id", type: "string" },
1335
1353
  { id: 3, name: "selected_context", type: "SelectedContext" },
1336
1354
  ]);
1337
- // RequestContext — UserMessageAction #2. Live per-turn tools go here
1338
- // (AgentRunRequest.mcp_tools #4 is prewarm-only / empty on real turns).
1355
+ // RequestContext — UserMessageAction #2. Slim mcp_meta_tool_options names
1356
+ // the advertised catalog; AgentRunRequest.mcp_tools (#4) is empty on real turns.
1339
1357
  addType(root, "RequestContext", [
1340
1358
  { id: 2, name: "rules", type: "CursorRule", repeated: true },
1341
1359
  { id: 4, name: "env", type: "RequestContextEnv" },
@@ -1,4 +1,4 @@
1
- import { type OpencodeToolDef } from "./tools.js";
1
+ import type { OpencodeToolDef } from "./tools.js";
2
2
  import type { CursorImageInput } from "../image-input.js";
3
3
  export type SeedHistoryMessage = {
4
4
  role: "system" | "user" | "assistant";
@@ -11,7 +11,6 @@ export type RunRequestInput = {
11
11
  conversationId: string;
12
12
  /** Stable parent group; unlike conversationId, this survives compaction/rebase. */
13
13
  conversationGroupId?: string;
14
- systemPrompt?: string;
15
14
  /**
16
15
  * Prior chat turns for a seed ConversationStateStructure (no checkpoint).
17
16
  * Tool outputs, when required for compaction/recovery, are represented as
@@ -22,7 +21,7 @@ export type RunRequestInput = {
22
21
  * Opaque ConversationStateStructure bytes from the last
23
22
  * conversation_checkpoint_update for this conversation_id. When set, echoed
24
23
  * as AgentRunRequest.conversation_state (CLI parity). When absent, a seed
25
- * state with the system prompt (or empty) is built for turn 1.
24
+ * state carrying only `history` (or empty) is built for turn 1.
26
25
  */
27
26
  conversationState?: Uint8Array;
28
27
  parameterValues?: Array<{
@@ -42,19 +41,22 @@ export type RunRequestInput = {
42
41
  /**
43
42
  * Seed ConversationStateStructure for the first turn (no checkpoint yet).
44
43
  *
45
- * OpenCode needs a system prompt channel; we put it in root_prompt_messages_json
46
- * as a JSON chat message. After the first checkpoint arrives we stop inventing
47
- * state and echo the server's opaque structure instead (CLI behavior).
44
+ * After the first checkpoint arrives we stop inventing state and echo the
45
+ * server's opaque structure instead (CLI behavior). Compaction resets and
46
+ * rebases also use this seed, with `history` carrying OpenCode's prompt turns
47
+ * so Cursor can continue without the old checkpoint.
48
48
  *
49
- * Compaction resets also use this seed, with `history` carrying OpenCode's
50
- * compacted prompt turns so Cursor can summarize without the old checkpoint.
49
+ * No `system` entry is seeded: Cursor does not follow a client-authored
50
+ * `system` root message, and neither Cursor client writes
51
+ * `root_prompt_messages_json`. Host system context travels as the
52
+ * system-instructions rule in RequestContext (`systemInstructionsRule`), so
53
+ * `system` history entries are dropped here rather than duplicated.
51
54
  *
52
55
  * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
53
56
  * field is the internal `--system-prompt` CLI override and the server rejects
54
57
  * it for normal accounts.
55
58
  */
56
59
  export declare function buildSeedConversationState(input?: {
57
- systemPrompt?: string;
58
60
  history?: SeedHistoryMessage[];
59
61
  }): Uint8Array;
60
62
  /**
@@ -1,14 +1,17 @@
1
1
  import { encodeMessage, getMessageTypes } from "./messages.js";
2
- import { toolsToDescriptors } from "./tools.js";
3
2
  /**
4
3
  * Seed ConversationStateStructure for the first turn (no checkpoint yet).
5
4
  *
6
- * OpenCode needs a system prompt channel; we put it in root_prompt_messages_json
7
- * as a JSON chat message. After the first checkpoint arrives we stop inventing
8
- * state and echo the server's opaque structure instead (CLI behavior).
5
+ * After the first checkpoint arrives we stop inventing state and echo the
6
+ * server's opaque structure instead (CLI behavior). Compaction resets and
7
+ * rebases also use this seed, with `history` carrying OpenCode's prompt turns
8
+ * so Cursor can continue without the old checkpoint.
9
9
  *
10
- * Compaction resets also use this seed, with `history` carrying OpenCode's
11
- * compacted prompt turns so Cursor can summarize without the old checkpoint.
10
+ * No `system` entry is seeded: Cursor does not follow a client-authored
11
+ * `system` root message, and neither Cursor client writes
12
+ * `root_prompt_messages_json`. Host system context travels as the
13
+ * system-instructions rule in RequestContext (`systemInstructionsRule`), so
14
+ * `system` history entries are dropped here rather than duplicated.
12
15
  *
13
16
  * We deliberately do NOT use `AgentRunRequest.custom_system_prompt` (#8): that
14
17
  * field is the internal `--system-prompt` CLI override and the server rejects
@@ -18,14 +21,8 @@ export function buildSeedConversationState(input) {
18
21
  const root = getMessageTypes();
19
22
  const type = root.lookupType("ConversationStateStructure");
20
23
  const messages = [];
21
- if (input?.systemPrompt && input.systemPrompt.length > 0) {
22
- messages.push(JSON.stringify({ role: "system", content: input.systemPrompt }));
23
- }
24
24
  for (const entry of input?.history ?? []) {
25
- if (!entry.content)
26
- continue;
27
- // Avoid duplicating the system prompt when history also carries one.
28
- if (entry.role === "system" && input?.systemPrompt)
25
+ if (!entry.content || entry.role === "system")
29
26
  continue;
30
27
  messages.push(JSON.stringify({ role: entry.role, content: entry.content }));
31
28
  }
@@ -42,11 +39,9 @@ export function buildSeedConversationState(input) {
42
39
  */
43
40
  export function buildRunRequest(input) {
44
41
  const msgId = input.messageId ?? crypto.randomUUID();
45
- // Advertise opencode's tools on the LIVE path: UserMessageAction.request_context
46
- // (#2). AgentRunRequest.mcp_tools (#4) is prewarm-only / empty on real turns —
47
- // putting tools only there is why the model fell back to native Grep/Read.
48
- const tools = input.tools ?? [];
49
- const mcpTools = input.toolDescriptors ?? (tools.length > 0 ? toolsToDescriptors(tools) : []);
42
+ // Advertise host tools on UserMessageAction.request_context (#2) via slim
43
+ // mcp_meta_tool_options. AgentRunRequest.mcp_tools (#4) stays empty on real
44
+ // turns (CLI prewarm-only). Full defs are session.toolDescriptors + exec #36.
50
45
  const requestContext = input.requestContext;
51
46
  const userMessage = {
52
47
  text: input.text,
@@ -70,10 +65,7 @@ export function buildRunRequest(input) {
70
65
  : { user_message_action: userMessageAction };
71
66
  const conversationState = input.conversationState && input.conversationState.length > 0
72
67
  ? input.conversationState
73
- : buildSeedConversationState({
74
- systemPrompt: input.systemPrompt,
75
- history: input.history,
76
- });
68
+ : buildSeedConversationState({ history: input.history });
77
69
  const runRequest = {
78
70
  conversation_id: input.conversationId,
79
71
  conversation_group_id: input.conversationGroupId ?? input.conversationId,
@@ -88,9 +80,7 @@ export function buildRunRequest(input) {
88
80
  parameters: input.parameterValues ?? [],
89
81
  },
90
82
  conversation_state: conversationState,
91
- // Keep #4 populated too (harmless on real turns; useful for prewarm /
92
- // older server builds that still read it).
93
- mcp_tools: { mcp_tools: mcpTools },
83
+ mcp_tools: { mcp_tools: [] },
94
84
  unknown_flag: 0,
95
85
  field_12: 0,
96
86
  };
@@ -61,7 +61,9 @@ export declare function sanitizeMcpServerId(value: string): string;
61
61
  export declare function resolveToolServerIdentity(opencodeName: string, defaultServer?: string, knownMcpServers?: Iterable<string>): ToolServerIdentity;
62
62
  /**
63
63
  * Convert opencode's per-turn tool list into Cursor `McpToolDefinition`
64
- * entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`.
64
+ * entries for session exec remap / bridges. These are not sent on
65
+ * RequestContext.tools (#7) or AgentRunRequest.mcp_tools (both omitted/empty
66
+ * on the wire). Exec #36 still uses the same identity fields.
65
67
  *
66
68
  * Builtins and unknown plugin/custom tools are advertised under the synthetic
67
69
  * default server (`opencode`). Tools whose prefixes match configured MCP
@@ -79,12 +81,13 @@ export declare function toolsToDescriptors(tools: OpencodeToolDef[], providerIde
79
81
  export declare function buildCustomWebToolAliases(tools: OpencodeToolDef[]): AliasedToolCatalog;
80
82
  export declare function resolveCustomWebToolAlias(toolName: string, aliases: ToolAliasRegistry | undefined): string;
81
83
  /**
82
- * Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
83
- * requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
84
- * and unknown tools under the synthetic default; configured MCP tools under
85
- * their upstream server id).
84
+ * Nested MCP descriptors. Full name/description/schema is for exec #36.
85
+ * RequestContext `mcp_meta_tool_options` uses `{ namesOnly: true }` (`tool_name`
86
+ * only). File-system `mcp_descriptors` are omitted on the wire.
86
87
  */
87
- export declare function toolsToMcpDescriptors(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable<string>): Array<Record<string, unknown>>;
88
+ export declare function toolsToMcpDescriptors(tools: OpencodeToolDef[], providerIdentifier?: string, knownMcpServers?: Iterable<string>, options?: {
89
+ namesOnly?: boolean;
90
+ }): Array<Record<string, unknown>>;
88
91
  /**
89
92
  * @deprecated Prefer `buildRequestContext` from `../context/build.js`.
90
93
  * Kept as a sync tools-only fallback for unit tests that don't need collectors.
@@ -340,11 +343,15 @@ export declare function detectExecVariantField(agentServerPayload: Uint8Array):
340
343
  */
341
344
  export declare function buildRequestContextResult(execId: number, requestContext: Record<string, unknown>): Uint8Array;
342
345
  /**
343
- * Answer Cursor's exec #36 MCP-state probe from the same descriptors advertised
344
- * in RequestContext. OpenCode remains the executor; this only confirms that the
345
- * provider's virtual MCP servers and their tools are available.
346
+ * Answer Cursor's exec #36 MCP-state probe from the session's live tool
347
+ * descriptors: `toolsToDescriptors` output for the advertised catalog (after
348
+ * web-tool aliasing), refreshed on every `doStream`. These are the same
349
+ * identities the names-only RequestContext advertises and exec remap uses, so
350
+ * Cursor's native get_mcp_tools can correlate the later
351
+ * provider_identifier/tool_name request. OpenCode remains the executor; this
352
+ * only confirms those tools are available, with full name/description/schema.
346
353
  */
347
- export declare function buildMcpStateResult(execId: number, args: Record<string, unknown>, requestContext: Record<string, unknown>): Uint8Array;
354
+ export declare function buildMcpStateResult(execId: number, args: Record<string, unknown>, toolDescriptors: ReadonlyArray<Record<string, unknown>>): Uint8Array;
348
355
  /**
349
356
  * Total fallback for Cursor's native MCP-resource exec channel (agent.v1
350
357
  * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B
@@ -132,7 +132,9 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
132
132
  }
133
133
  /**
134
134
  * Convert opencode's per-turn tool list into Cursor `McpToolDefinition`
135
- * entries for `request_context.tools` (#7) and `AgentRunRequest.mcp_tools`.
135
+ * entries for session exec remap / bridges. These are not sent on
136
+ * RequestContext.tools (#7) or AgentRunRequest.mcp_tools (both omitted/empty
137
+ * on the wire). Exec #36 still uses the same identity fields.
136
138
  *
137
139
  * Builtins and unknown plugin/custom tools are advertised under the synthetic
138
140
  * default server (`opencode`). Tools whose prefixes match configured MCP
@@ -249,15 +251,15 @@ export function resolveCustomWebToolAlias(toolName, aliases) {
249
251
  return toolName;
250
252
  }
251
253
  /**
252
- * Build the nested McpFileSystemOptions / McpMetaToolOptions shape used by
253
- * requestContext.#23 / #34. One `McpDescriptor` per resolved server (builtins
254
- * and unknown tools under the synthetic default; configured MCP tools under
255
- * their upstream server id).
254
+ * Nested MCP descriptors. Full name/description/schema is for exec #36.
255
+ * RequestContext `mcp_meta_tool_options` uses `{ namesOnly: true }` (`tool_name`
256
+ * only). File-system `mcp_descriptors` are omitted on the wire.
256
257
  */
257
- export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
258
+ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = [], options) {
258
259
  if (tools.length === 0)
259
260
  return [];
260
261
  const byServer = new Map();
262
+ const namesOnly = options?.namesOnly === true;
261
263
  // Walk advertised order: first-seen server, then append tools onto that
262
264
  // server. Same-set host reorder is resolved by `resolveTurnToolState`
263
265
  // before encode so this walk stays epoch-stable.
@@ -268,11 +270,14 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
268
270
  list = [];
269
271
  byServer.set(id.server, list);
270
272
  }
271
- list.push({
272
- tool_name: t.sourceName ? t.name : id.toolName,
273
- description: t.description ?? "",
274
- input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
275
- });
273
+ const toolName = t.sourceName ? t.name : id.toolName;
274
+ list.push(namesOnly
275
+ ? { tool_name: toolName }
276
+ : {
277
+ tool_name: toolName,
278
+ description: t.description ?? "",
279
+ input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
280
+ });
276
281
  }
277
282
  return [...byServer.keys()].map((server) => ({
278
283
  server_name: server,
@@ -285,20 +290,17 @@ export function toolsToMcpDescriptors(tools, providerIdentifier = "opencode", kn
285
290
  * Kept as a sync tools-only fallback for unit tests that don't need collectors.
286
291
  */
287
292
  export function buildLiveRequestContext(tools, providerIdentifier = "opencode", knownMcpServers = []) {
288
- const flat = toolsToDescriptors(tools, providerIdentifier, knownMcpServers);
289
- const nested = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers);
293
+ const slim = toolsToMcpDescriptors(tools, providerIdentifier, knownMcpServers, { namesOnly: true });
290
294
  const cwd = process.cwd();
291
295
  const ctx = {
292
296
  env: buildEnv(cwd),
293
- tools: flat,
294
297
  mcp_file_system_options: {
295
298
  enabled: true,
296
299
  workspace_project_dir: ensureOpencodeProjectDir(cwd),
297
- mcp_descriptors: nested,
298
300
  },
299
301
  mcp_meta_tool_options: {
300
302
  enabled: true,
301
- mcp_descriptors: nested,
303
+ ...(slim.length > 0 ? { mcp_descriptors: slim } : {}),
302
304
  },
303
305
  web_search_enabled: false,
304
306
  web_fetch_enabled: false,
@@ -3196,41 +3198,45 @@ export function buildRequestContextResult(execId, requestContext) {
3196
3198
  });
3197
3199
  }
3198
3200
  /**
3199
- * Answer Cursor's exec #36 MCP-state probe from the same descriptors advertised
3200
- * in RequestContext. OpenCode remains the executor; this only confirms that the
3201
- * provider's virtual MCP servers and their tools are available.
3201
+ * Answer Cursor's exec #36 MCP-state probe from the session's live tool
3202
+ * descriptors: `toolsToDescriptors` output for the advertised catalog (after
3203
+ * web-tool aliasing), refreshed on every `doStream`. These are the same
3204
+ * identities the names-only RequestContext advertises and exec remap uses, so
3205
+ * Cursor's native get_mcp_tools can correlate the later
3206
+ * provider_identifier/tool_name request. OpenCode remains the executor; this
3207
+ * only confirms those tools are available, with full name/description/schema.
3202
3208
  */
3203
- export function buildMcpStateResult(execId, args, requestContext) {
3209
+ export function buildMcpStateResult(execId, args, toolDescriptors) {
3204
3210
  const requested = new Set(Array.isArray(args.server_identifiers)
3205
3211
  ? args.server_identifiers.filter((id) => typeof id === "string" && id.length > 0)
3206
3212
  : []);
3207
- const fsOptions = recordValue(requestContext.mcp_file_system_options);
3208
- const nested = Array.isArray(fsOptions?.mcp_descriptors)
3209
- ? fsOptions.mcp_descriptors.map(recordValue).filter((d) => !!d)
3210
- : [];
3211
- const descriptors = nested.length > 0 ? nested : descriptorsFromFlatTools(requestContext.tools);
3212
- const flatTools = Array.isArray(requestContext.tools)
3213
- ? requestContext.tools.map(recordValue).filter((tool) => !!tool)
3214
- : [];
3215
- const servers = descriptors
3216
- .filter((descriptor) => {
3217
- const id = stringValue(descriptor.server_identifier);
3218
- return requested.size === 0 || (id !== undefined && requested.has(id));
3219
- })
3220
- .map((descriptor) => {
3221
- const serverIdentifier = stringValue(descriptor.server_identifier) ?? stringValue(descriptor.server_name) ?? "";
3222
- const tools = Array.isArray(descriptor.tools)
3223
- ? descriptor.tools
3224
- .map(recordValue)
3225
- .filter((tool) => !!tool)
3226
- .map((tool) => mcpStateToolDefinition(serverIdentifier, tool, flatTools))
3227
- : [];
3228
- return {
3229
- server_name: stringValue(descriptor.server_name) ?? serverIdentifier,
3230
- server_identifier: serverIdentifier,
3231
- tools,
3232
- };
3233
- });
3213
+ // Group in advertised order: first-seen server, then its tools.
3214
+ const byServer = new Map();
3215
+ for (const tool of toolDescriptors) {
3216
+ const server = stringValue(tool.provider_identifier);
3217
+ const toolName = stringValue(tool.tool_name);
3218
+ if (!server || !toolName)
3219
+ continue;
3220
+ if (requested.size > 0 && !requested.has(server))
3221
+ continue;
3222
+ let list = byServer.get(server);
3223
+ if (!list) {
3224
+ list = [];
3225
+ byServer.set(server, list);
3226
+ }
3227
+ list.push({
3228
+ name: stringValue(tool.name) ?? `${server}-${toolName}`,
3229
+ description: stringValue(tool.description) ?? "",
3230
+ input_schema: tool.input_schema,
3231
+ provider_identifier: server,
3232
+ tool_name: toolName,
3233
+ });
3234
+ }
3235
+ const servers = [...byServer].map(([server, tools]) => ({
3236
+ server_name: server,
3237
+ server_identifier: server,
3238
+ tools,
3239
+ }));
3234
3240
  return encodeMessage("AgentClientMessage", {
3235
3241
  exec_client_message: {
3236
3242
  id: execId,
@@ -3238,24 +3244,6 @@ export function buildMcpStateResult(execId, args, requestContext) {
3238
3244
  },
3239
3245
  });
3240
3246
  }
3241
- /**
3242
- * Exec #36 uses McpToolDefinition, not the narrower McpToolDescriptor used by
3243
- * RequestContext's filesystem/meta-tool catalogs. Rehydrate the full identity
3244
- * from RequestContext.tools so Cursor's native get_mcp_tools can correlate the
3245
- * discovered definition with the later provider_identifier/tool_name request.
3246
- */
3247
- function mcpStateToolDefinition(serverIdentifier, descriptor, flatTools) {
3248
- const toolName = stringValue(descriptor.tool_name) ?? "";
3249
- const advertised = flatTools.find((tool) => stringValue(tool.provider_identifier) === serverIdentifier
3250
- && stringValue(tool.tool_name) === toolName);
3251
- return {
3252
- name: stringValue(advertised?.name) ?? `${serverIdentifier}-${toolName}`,
3253
- description: stringValue(advertised?.description) ?? stringValue(descriptor.description) ?? "",
3254
- input_schema: advertised?.input_schema ?? descriptor.input_schema,
3255
- provider_identifier: serverIdentifier,
3256
- tool_name: toolName,
3257
- };
3258
- }
3259
3247
  /**
3260
3248
  * Total fallback for Cursor's native MCP-resource exec channel (agent.v1
3261
3249
  * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B
@@ -3285,31 +3273,3 @@ export function buildReadMcpResourceFallback(execId, server, uri) {
3285
3273
  },
3286
3274
  });
3287
3275
  }
3288
- function recordValue(value) {
3289
- return value && typeof value === "object" && !Array.isArray(value)
3290
- ? value
3291
- : undefined;
3292
- }
3293
- function descriptorsFromFlatTools(value) {
3294
- if (!Array.isArray(value))
3295
- return [];
3296
- const byServer = new Map();
3297
- for (const raw of value) {
3298
- const tool = recordValue(raw);
3299
- if (!tool)
3300
- continue;
3301
- const server = stringValue(tool.provider_identifier) ?? "opencode";
3302
- const tools = byServer.get(server) ?? [];
3303
- tools.push({
3304
- tool_name: stringValue(tool.tool_name) ?? stringValue(tool.name) ?? "",
3305
- description: stringValue(tool.description) ?? "",
3306
- input_schema: tool.input_schema,
3307
- });
3308
- byServer.set(server, tools);
3309
- }
3310
- return [...byServer].map(([server, tools]) => ({
3311
- server_name: server,
3312
- server_identifier: server,
3313
- tools,
3314
- }));
3315
- }