cursor-opencode-provider 0.4.1 → 0.4.3

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.
package/README.md CHANGED
@@ -1,10 +1,38 @@
1
1
  # cursor-opencode-provider
2
2
 
3
- Use [Cursor](https://cursor.com) subscription models from [OpenCode](https://opencode.ai) by speaking Cursor's Connect-RPC agent protocol.
3
+ Use [Cursor](https://cursor.com) subscription models from [OpenCode](https://opencode.ai) and compatible coding agents by speaking Cursor's Connect-RPC agent protocol.
4
4
 
5
5
  This project is a custom **AI SDK provider** (`LanguageModelV3`) plus an **OpenCode plugin** that handles authentication and model discovery. Instead of calling a generic chat-completions API, it encodes and decodes Cursor's protobuf agent protocol over HTTP/2 to Cursor's agent backend.
6
6
 
7
- > **Status:** Usable end-to-end in OpenCode (auth, models, streaming, tools). See [Known limitations](#known-limitations).
7
+ > **Runs unchanged across OpenCode, OpenCode 2.0, and four additional coding agents.** OpenCode loads this plugin natively, including through the dedicated OpenCode 2.0 entrypoint. [OCP — OpenCode Plugin Compatibility](https://github.com/oakimov/opencode-plugin-compat) runs the same published plugin, without a provider fork, in **Kilo Code, MiMo Code, pi, and oh-my-pi**. See [Coding-agent compatibility](#coding-agent-compatibility).
8
+ >
9
+ > **Status:** Usable end-to-end for authentication, model discovery, streaming, and tools. See [Known limitations](#known-limitations).
10
+
11
+ ## Coding-agent compatibility
12
+
13
+ This package remains one OpenCode plugin and AI SDK provider. The external
14
+ **[OCP compatibility layer](https://github.com/oakimov/opencode-plugin-compat)**
15
+ adapts other hosts around that unchanged package: it translates their plugin
16
+ surfaces, model catalogs, tool vocabulary, authentication, and streaming events
17
+ instead of requiring a host-specific Cursor provider.
18
+
19
+ | Coding agent | How this package runs |
20
+ |---|---|
21
+ | **OpenCode** | Native plugin and AI SDK provider |
22
+ | **OpenCode 2.0** | Native dedicated `cursor-opencode-provider/plugin/opencode2` entrypoint |
23
+ | **Kilo Code** | Unchanged through OCP's OpenCode-clone compatibility layer |
24
+ | **MiMo Code** | Unchanged through OCP's OpenCode-clone compatibility layer |
25
+ | **pi** | Unchanged through `@opencode-compat/pi-bridge` and pi's provider extension API |
26
+ | **oh-my-pi (omp)** | Unchanged through the same Pi-family bridge |
27
+
28
+ Start with OCP's host guides:
29
+
30
+ - [OpenCode clones: Kilo Code and MiMo Code](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/opencode-clones.md)
31
+ - [Pi family: pi and oh-my-pi](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/pi-family.md)
32
+
33
+ OCP is designed to grow by adding host profiles and narrow adapters or bridges,
34
+ so support for other coding agents can be added there while this provider and
35
+ its package remain unchanged.
8
36
 
9
37
  ## Demo
10
38
 
@@ -14,6 +42,7 @@ OpenCode driving a Cursor-routed Grok model through this provider:
14
42
 
15
43
  ## Features
16
44
 
45
+ - **Multi-host compatibility** — the same plugin package runs natively in OpenCode and OpenCode 2.0, and unchanged through OCP in Kilo Code, MiMo Code, pi, and oh-my-pi; new host support belongs in the compatibility layer rather than a provider fork
17
46
  - **OpenCode integration** — registers a `cursor` provider with auth hooks and cached model list
18
47
  - **Authentication** — browser OAuth (PKCE), or API key from [cursor.com/settings](https://cursor.com/settings)
19
48
  - **Model discovery** — fetches available models from Cursor's API and caches them locally
@@ -25,7 +54,7 @@ OpenCode driving a Cursor-routed Grok model through this provider:
25
54
  ## Requirements
26
55
 
27
56
  - [Bun](https://bun.sh) (for development and tests)
28
- - [OpenCode](https://opencode.ai) with plugin support
57
+ - [OpenCode](https://opencode.ai), or a supported host configured through [OCP](https://github.com/oakimov/opencode-plugin-compat)
29
58
  - An active Cursor account with API access
30
59
 
31
60
  ## Installation
@@ -431,7 +460,7 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
431
460
  | Visible `<shell_metadata>` timeout text | Rebuild and restart a local install. Cursor's shell timeout is carried on its exec request; the provider now removes OpenCode's internal timeout envelope before it is rendered or stored, then returns Cursor's typed timeout or background-handoff event instead of treating the text as successful stdout. |
432
461
  | `Unsupported Cursor exec variant …` | The error names the canonical Cursor CLI request field, its expected result field, and this provider's handling classification. `handling=unsupported` is a known Cursor-native capability without a safe OpenCode AI SDK bridge; `unknown request field` indicates new protocol drift; `handling=opencode-tool` or `provider-control` indicates a provider decoder/dispatch regression. Enable the debug log and report the full named error. |
433
462
  | Need wire-level logs | Set `CURSOR_PROVIDER_DEBUG=1` (optional `CURSOR_PROVIDER_DEBUG_FILE`; the default is `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) and reproduce the issue. |
434
- | OpenCode compacts immediately after a tool-heavy task | Upgrade to a version containing the cumulative-usage fix. Cursor's final `TurnEnded` counters cover the entire held multi-step Run; older provider versions exposed that aggregate as the final request's context size, which made OpenCode compact far too early. Fixed versions keep the raw counters in `providerMetadata.cursor` and report a request-local context estimate instead. |
463
+ | OpenCode compacts immediately after a tool-heavy task | Upgrade to a version containing the cumulative-usage fix. Cursor's final `TurnEnded` counters cover the entire held multi-step Run; older provider versions exposed that aggregate as the final request's context size, which made OpenCode compact far too early. Fixed versions keep the raw counters in `providerMetadata.cursor` and report a request-local context estimate instead. An over-budget guard also catches single-step or parallel Runs whose counters are cumulative even without a visible tool boundary. |
435
464
  | Cache **write** tokens always show as `0` | Cursor's `TurnEnded` protobuf includes `cache_write` (field 4), but captured Runs set it to `0` even when `cache_read` is large. Single-step Runs expose those counters directly. For multi-step Runs they remain in `providerMetadata.cursor` because Cursor reports only a cumulative whole-Run total, which cannot safely be used as one OpenCode request's context usage. With debug logging, confirm `turn_ended raw wire fields: … f4:wt0=0`. |
436
465
 
437
466
  ## Security
@@ -441,12 +470,13 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
441
470
  ## Known limitations
442
471
 
443
472
  - **Personal use / ToS** — this provider speaks Cursor’s private agent protocol (CLI-shaped client identity). Use only with an account you own; Cursor may change or restrict the API without notice.
444
- - **`TurnEnded` usage is whole-Run usage** — on a held multi-step Cursor Run, the final `interaction_update.turn_ended` counters aggregate every model invocation across tool boundaries. OpenCode usage is request-local and uses the latest assistant message to decide compaction, so publishing the aggregate there both double-counts earlier steps and causes premature compaction. The provider therefore uses a current-context estimate for multi-step AI SDK usage and preserves the exact cumulative counters in `providerMetadata.cursor`; single-step Runs still map the counters directly. Cursor also consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
473
+ - **`TurnEnded` usage is whole-Run usage** — on a held multi-step Cursor Run, the final `interaction_update.turn_ended` counters aggregate every model invocation across tool boundaries. OpenCode usage is request-local and uses the latest assistant message to decide compaction, so publishing the aggregate there both double-counts earlier steps and causes premature compaction. The provider therefore uses a current-context estimate for multi-step AI SDK usage and preserves the exact cumulative counters in `providerMetadata.cursor`; single-step Runs map the counters directly only when they are close to the request-local estimate, while an over-budget guard handles cumulative no-boundary Runs. Cursor also consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
445
474
  - **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. Cursor-only cloud/sandbox marketplace surfaces are omitted. `env.workspace_paths` / `process_working_directory` stay on the real git workspace; Cursor's metadata root (`project_folder`, MCP `workspace_project_dir`, terminals/transcripts) is advertised under `<host-cache>/projects/<slug>/` (default `~/.cache/opencode/projects/…`) so dumps like `agent-tools/` do not land in the repo. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages.
446
475
  - **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools`; exec still reconstructs the full OpenCode tool id.
447
476
  - **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` / `create_plan_tool_call`) into advertised OpenCode `todowrite`; the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name; known-but-unsupported and future unknown exec variants fail explicitly rather than receiving a guessed response that could deadlock the Run.
448
477
  - **Tool availability is per OpenCode agent** — Cursor can request native capabilities such as Task even when a child or restricted OpenCode agent did not advertise the corresponding host tool. The provider prompts Cursor with the exact current catalog and checks every decoded host-tool exec request against it. An unavailable request is answered on Cursor's correlated typed result channel and is never emitted as OpenCode's `invalid` tool.
449
478
  - **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. The plugin registers its search fallback directly as `custom_websearch`, avoiding OpenCode's reserved `websearch` id filter for third-party providers and taking precedence over MCP search providers such as Brave without host environment configuration. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results.
479
+ - **MCP resource tools (`list_mcp_resources` / `read_mcp_resource`) also use collision-safe aliases, and Cursor's native resource exec is always answered** — OpenCode's resource tools (advertised only when a connected MCP server declares `capabilities.resources`) share bare names with two of Cursor's own native tool-call variants. Left unaliased, Cursor's client can route a call onto its native exec channel instead of the ordinary MCP tool path, which this provider does not translate — confirmed live: reading a resource was dispatched natively and killed the Run before this fix. The provider now aliases both names to `custom_list_mcp_resources` / `custom_read_mcp_resource` (same mechanism as web search/fetch) so normal calls execute through OpenCode's tools, and separately answers Cursor's native resource-exec fields directly on the protocol floor — an empty list, or a "server not found" error for read — so an unsolicited native request from Cursor can never fail the Run. Downloads (`download_path`) have no OpenCode equivalent and are always refused; binary resource content is subject to the same [50 KB / attachment-type limits](#known-limitations) as other tool results.
450
480
  - **Background shells are non-interactive** — Cursor's native background-shell spawn and soft-background shell-stream timeouts are bridged through OpenCode's foreground-only `bash` tool. With bash/zsh, the classic plugin keeps the original permission/UI command and executes the wrapper through `shell.env` (`BASH_ENV` / `ZDOTDIR`); OpenCode's non-interactive sh/dash argv ignores those startup variables, so the plugin uses a short `exec /bin/sh '<wrapper-file>'` command backed by the same private wrapper. Native background-spawn requests also carry a self-contained marker-producing fallback when the classic hooks are absent. Spawn/soft-bg wrappers detach with `nohup`, redirect output under `${TMPDIR:-/tmp}/cursor-opencode-{bg,shell}.*`, and return the real PID (or typed timeout/exit) to Cursor. Private markers and OpenCode's `<shell_metadata>` envelope are stripped before storage/render, with a short still-running / started / timed-out status line left for the bash bubble. Requests that require `write_shell_stdin` are rejected explicitly because OpenCode does not expose an interactive background-process lifecycle through its AI SDK tool interface. The POSIX wrap path is not implemented for native Windows PowerShell/`cmd`.
451
481
  - **Cursor-native interaction queries remain headless** — Cursor UI/approval *queries* (as distinct from display tool calls) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects questions, planning, plan-mode transitions, and available web capabilities to executable host tools (`question`, `todowrite`, `plan_enter` / `plan_exit`, `custom_websearch`, `custom_webfetch`); native web/PR/MCP/image/SCM requests are declined so they remain behind host tool permissions. Separately from display `create_plan_tool_call` → `todowrite` mirroring, interaction `create_plan_request_query` is auto-acked (CLI headless parity) with success and an empty `plan_uri`, so Cursor may treat the plan as accepted without an OpenCode UI confirm. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
452
482
  - **Compaction resets Cursor conversation state** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state.
@@ -6,7 +6,7 @@ import { trace, traceRequestContextPaths } from "./debug.js";
6
6
  import { buildRunRequest, buildHeartbeat } from "./protocol/request.js";
7
7
  import { decodeFramePayload } from "./protocol/framing.js";
8
8
  import { debugWalkTurnEnded, decodeMessage } from "./protocol/messages.js";
9
- import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, } from "./protocol/tools.js";
9
+ import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, isUriReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildListMcpResourcesFallback, buildReadMcpResourceFallback, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, CUSTOM_LIST_MCP_RESOURCES_TOOL, CUSTOM_READ_MCP_RESOURCE_TOOL, } from "./protocol/tools.js";
10
10
  import { describeCursorExecVariant } from "./protocol/exec-variants.js";
11
11
  import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, } from "./protocol/tool-call-bridge.js";
12
12
  import { handleKvServerMessage } from "./protocol/kv.js";
@@ -29,7 +29,7 @@ import { resolveCursorModelSupportsImages } from "./model-metadata.js";
29
29
  import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
30
30
  import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
31
31
  import { readAllFieldsStrict } from "./protocol/struct.js";
32
- import { buildLanguageModelV3UsageFromEstimate, buildLanguageModelV3UsageFromTurnEnded, turnEndedCounter, } from "./usage.js";
32
+ import { buildLanguageModelV3UsageFromEstimate, buildLanguageModelV3UsageFromTurnEnded, exceedsRequestLocalBudget, turnEndedCounter, } from "./usage.js";
33
33
  let _availableModels;
34
34
  // mtime of the cache file the last time we loaded it. Compared on each call
35
35
  // so discoverModels' background refresh is picked up without a process restart.
@@ -1089,6 +1089,14 @@ export async function pump(session, controller, ids, abortSignal) {
1089
1089
  const requested = typeof parsed.args.filePath === "string" ? parsed.args.filePath : "";
1090
1090
  if (!requested)
1091
1091
  return false;
1092
+ // A scheme-addressed target is the host's to resolve, not a local file to
1093
+ // stat. Rejecting it here makes host-mounted capabilities (oh-my-pi's
1094
+ // `xd://` device URLs, which is how every MCP tool is exposed by default)
1095
+ // permanently unreachable through this provider.
1096
+ if (isUriReadTarget(requested)) {
1097
+ trace(`exec: forwarding URI read target id=${parsed.id} target=${JSON.stringify(requested)}`);
1098
+ return false;
1099
+ }
1092
1100
  const workspaceRoot = workspaceRootFromRequestContext(session.requestContext);
1093
1101
  const absolutePath = resolveReadTargetPath(requested, workspaceRoot);
1094
1102
  const readResult = classifyMissingReadTarget(absolutePath);
@@ -1156,7 +1164,12 @@ export async function pump(session, controller, ids, abortSignal) {
1156
1164
  // a tool boundary. On a held multi-step Run Cursor aggregates every model
1157
1165
  // invocation into these counters; treating that billed total as the final
1158
1166
  // request's prompt size makes OpenCode compact after ordinary tasks.
1159
- const cumulativeTurnEnded = !!te && session.hadToolCallBoundary === true;
1167
+ const cumulativeByBoundary = !!te && session.hadToolCallBoundary === true;
1168
+ const cumulativeByBudget = !!te && !cumulativeByBoundary && exceedsRequestLocalBudget(te, {
1169
+ inputTokens: session.usageEstimate.inputTokens,
1170
+ outputTokens: session.usageEstimate.outputTokens,
1171
+ });
1172
+ const cumulativeTurnEnded = cumulativeByBoundary || cumulativeByBudget;
1160
1173
  if (te && !cumulativeTurnEnded) {
1161
1174
  session.usageEstimate = {
1162
1175
  inputTokens: turnEndedCounter(te, "input_tokens"),
@@ -1186,6 +1199,7 @@ export async function pump(session, controller, ids, abortSignal) {
1186
1199
  `rawOut=${te ? turnEndedCounter(te, "output_tokens") : est.outputTokens} ` +
1187
1200
  `rawCacheRead=${te ? turnEndedCounter(te, "cache_read") : est.cacheRead} ` +
1188
1201
  `rawCacheWrite=${te ? turnEndedCounter(te, "cache_write") : est.cacheWrite} ` +
1202
+ `guard=${cumulativeByBoundary ? "boundary" : cumulativeByBudget ? "over-budget" : "none"} ` +
1189
1203
  `source=${te ? (cumulativeTurnEnded ? "estimate-cumulative-turn-ended" : "turn_ended") : "estimate"}`);
1190
1204
  safeEnqueue({
1191
1205
  type: "finish",
@@ -1460,6 +1474,33 @@ export async function pump(session, controller, ids, abortSignal) {
1460
1474
  failRunProtocol("Cursor MCP-state reply failed", RUN_REPLY_FAILED);
1461
1475
  }
1462
1476
  }
1477
+ else if (esm.list_mcp_resources_exec_args) {
1478
+ // Native Cursor exec (agent.v1 field 17), no OpenCode route under
1479
+ // Option B — see tasks/plans/fix-cursor-mcp-resource-exec.md. Answer
1480
+ // with Cursor's own no-client-qualifies shape and keep pumping.
1481
+ const args = esm.list_mcp_resources_exec_args;
1482
+ const server = typeof args.server === "string" ? args.server : "";
1483
+ try {
1484
+ session.stream.write(buildListMcpResourcesFallback(esmId));
1485
+ trace(`exec list_mcp_resources: replied id=${esmId} server=${server || "(all)"} success{resources:[]}`);
1486
+ }
1487
+ catch {
1488
+ failRunProtocol("Cursor list_mcp_resources reply failed", RUN_REPLY_FAILED);
1489
+ }
1490
+ }
1491
+ else if (esm.read_mcp_resource_exec_args) {
1492
+ // Native Cursor exec (agent.v1 field 18) — same rationale as above.
1493
+ const args = esm.read_mcp_resource_exec_args;
1494
+ const server = typeof args.server === "string" ? args.server : "";
1495
+ const uri = typeof args.uri === "string" ? args.uri : "";
1496
+ try {
1497
+ session.stream.write(buildReadMcpResourceFallback(esmId, server, uri));
1498
+ trace(`exec read_mcp_resource: replied id=${esmId} server=${server} uri=${uri} error{server not found}`);
1499
+ }
1500
+ catch {
1501
+ failRunProtocol("Cursor read_mcp_resource reply failed", RUN_REPLY_FAILED);
1502
+ }
1503
+ }
1463
1504
  else {
1464
1505
  replaySafety.markBarrier("non-control-exec");
1465
1506
  const displayCallId = extractExecDisplayCallId(esm);
@@ -1759,6 +1800,12 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
1759
1800
  if (names.has(CUSTOM_WEBFETCH_TOOL)) {
1760
1801
  instructions.push(`- To fetch a known URL, call \`${CUSTOM_WEBFETCH_TOOL}\`; do not use Cursor's native WebFetch interaction.`);
1761
1802
  }
1803
+ if (names.has(CUSTOM_LIST_MCP_RESOURCES_TOOL)) {
1804
+ instructions.push(`- To list MCP resources, call \`${CUSTOM_LIST_MCP_RESOURCES_TOOL}\`; do not use Cursor's native resource-listing interaction.`);
1805
+ }
1806
+ if (names.has(CUSTOM_READ_MCP_RESOURCE_TOOL)) {
1807
+ instructions.push(`- To read an MCP resource, call \`${CUSTOM_READ_MCP_RESOURCE_TOOL}\`; do not use Cursor's native resource-reading interaction.`);
1808
+ }
1762
1809
  if (names.has("task") || names.has("actor")) {
1763
1810
  const target = names.has("actor") ? "`actor`" : "`task`";
1764
1811
  const available = subagents.agents.map((agent) => `\`${agent.name}\``).join(", ");
@@ -1781,6 +1828,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
1781
1828
  // leaving the turn with no file-editing guidance at all.
1782
1829
  instructions.push("- Use OpenCode `apply_patch` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available. Cursor-native write and edit requests are accepted and converted to `apply_patch` automatically.");
1783
1830
  }
1831
+ if (names.has("edit") || names.has("write") || names.has("apply_patch")) {
1832
+ instructions.push("- Never use a read result as complete file content when it says the output is capped, partial, or requires another offset. Read the remaining ranges first, or make a targeted edit/patch from complete context; do not pass a partial read back as a whole-file replacement.");
1833
+ }
1784
1834
  return [
1785
1835
  `OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
1786
1836
  `Workspace root: ${JSON.stringify(workspaceRoot)}. Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.`,
@@ -303,6 +303,9 @@ export declare const CURSOR_MODEL_CONTEXTS: {
303
303
  readonly "grok-4.5": {
304
304
  readonly maxContext: 256000;
305
305
  };
306
+ readonly "grok-4.6": {
307
+ readonly maxContext: 256000;
308
+ };
306
309
  readonly "kimi-k2.7-code": {
307
310
  readonly maxContext: 262000;
308
311
  };
@@ -405,6 +408,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
405
408
  readonly "grok-4.5": {
406
409
  readonly supportsImages: false;
407
410
  };
411
+ readonly "grok-4.6": {
412
+ readonly supportsImages: false;
413
+ };
408
414
  readonly "kimi-k2.7-code": {
409
415
  readonly supportsImages: true;
410
416
  };
@@ -303,6 +303,9 @@ export const CURSOR_MODEL_CONTEXTS = {
303
303
  "grok-4.5": {
304
304
  "maxContext": 256000
305
305
  },
306
+ "grok-4.6": {
307
+ "maxContext": 256000
308
+ },
306
309
  "kimi-k2.7-code": {
307
310
  "maxContext": 262000
308
311
  },
@@ -405,6 +408,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
405
408
  "grok-4.5": {
406
409
  "supportsImages": false
407
410
  },
411
+ "grok-4.6": {
412
+ "supportsImages": false
413
+ },
408
414
  "kimi-k2.7-code": {
409
415
  "supportsImages": true
410
416
  },
package/dist/pricing.d.ts CHANGED
@@ -34,7 +34,7 @@ export type CursorPricingCoverage = {
34
34
  * Models we expose that Cursor does not currently publish numeric rates for
35
35
  * on the Other Models table (Auto / first-party Cursor Models pool).
36
36
  */
37
- export declare const CURSOR_UNPRICED_MODEL_IDS: readonly ["default", "composer-2.5", "grok-4.5"];
37
+ export declare const CURSOR_UNPRICED_MODEL_IDS: readonly ["default", "composer-2.5", "grok-4.5", "grok-4.6"];
38
38
  export declare function validateOpenCodeModelCost(value: unknown, path?: string): {
39
39
  valid: boolean;
40
40
  errors: string[];
package/dist/pricing.js CHANGED
@@ -10,7 +10,7 @@ import { CURSOR_MODEL_COSTS } from "./pricing-data.js";
10
10
  * Models we expose that Cursor does not currently publish numeric rates for
11
11
  * on the Other Models table (Auto / first-party Cursor Models pool).
12
12
  */
13
- export const CURSOR_UNPRICED_MODEL_IDS = ["default", "composer-2.5", "grok-4.5"];
13
+ export const CURSOR_UNPRICED_MODEL_IDS = ["default", "composer-2.5", "grok-4.5", "grok-4.6"];
14
14
  const UNPRICED = new Set(CURSOR_UNPRICED_MODEL_IDS);
15
15
  function isRecord(value) {
16
16
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -10,8 +10,8 @@ export const CURSOR_EXEC_VARIANTS = [
10
10
  { requestField: 11, requestName: "mcp_args", resultField: 11, resultName: "mcp_result", handling: "opencode-tool" },
11
11
  { requestField: 14, requestName: "shell_stream_args", resultField: 14, resultName: "shell_stream", handling: "opencode-tool" },
12
12
  { requestField: 16, requestName: "background_shell_spawn_args", resultField: 16, resultName: "background_shell_spawn_result", handling: "opencode-tool" },
13
- { requestField: 17, requestName: "list_mcp_resources_exec_args", resultField: 17, resultName: "list_mcp_resources_exec_result", handling: "unsupported" },
14
- { requestField: 18, requestName: "read_mcp_resource_exec_args", resultField: 18, resultName: "read_mcp_resource_exec_result", handling: "unsupported" },
13
+ { requestField: 17, requestName: "list_mcp_resources_exec_args", resultField: 17, resultName: "list_mcp_resources_exec_result", handling: "provider-control" },
14
+ { requestField: 18, requestName: "read_mcp_resource_exec_args", resultField: 18, resultName: "read_mcp_resource_exec_result", handling: "provider-control" },
15
15
  { requestField: 20, requestName: "fetch_args", resultField: 20, resultName: "fetch_result", handling: "unsupported" },
16
16
  { requestField: 21, requestName: "record_screen_args", resultField: 21, resultName: "record_screen_result", handling: "unsupported" },
17
17
  { requestField: 22, requestName: "computer_use_args", resultField: 22, resultName: "computer_use_result", handling: "unsupported" },
@@ -679,6 +679,87 @@ export function createMessageTypes() {
679
679
  { id: 4, name: "provider_identifier", type: "string" },
680
680
  { id: 5, name: "tool_name", type: "string" },
681
681
  ]);
682
+ // ── MCP resource exec (fields 17/18) ──
683
+ // agent.v1 ListMcpResourcesExecArgs/Result, ReadMcpResourceExecArgs/Result.
684
+ // Native Cursor exec variants: no descriptor, no advertisement, and Cursor
685
+ // may emit either unsolicited regardless of what this provider advertises
686
+ // (tasks/plans/fix-cursor-mcp-resource-exec.md, Correction 2). Under Option
687
+ // B, list_mcp_resources/read_mcp_resource execute through the ordinary
688
+ // field-11 MCP path via the alias rules below, so any 17/18 that still
689
+ // reaches this provider gets the total fallback built in tools.ts.
690
+ addType(root, "SmartModeApproval", [
691
+ { id: 1, name: "request_id", type: "string" },
692
+ { id: 2, name: "reason", type: "string" },
693
+ ]);
694
+ addType(root, "OutputLocation", [
695
+ { id: 1, name: "file_path", type: "string" },
696
+ { id: 2, name: "size_bytes", type: "int64" },
697
+ { id: 3, name: "line_count", type: "int64" },
698
+ ]);
699
+ addType(root, "ListMcpResourcesExecArgs", [
700
+ { id: 1, name: "server", type: "string" },
701
+ ]);
702
+ {
703
+ const t = new protobuf.Type("ListMcpResourcesExecResult_McpResource");
704
+ t.add(new protobuf.Field("uri", 1, "string"));
705
+ t.add(new protobuf.Field("name", 2, "string"));
706
+ t.add(new protobuf.Field("description", 3, "string"));
707
+ t.add(new protobuf.Field("mime_type", 4, "string"));
708
+ t.add(new protobuf.Field("server", 5, "string"));
709
+ t.add(new protobuf.MapField("annotations", 6, "string", "string"));
710
+ root.add(t);
711
+ }
712
+ addType(root, "ListMcpResourcesSuccess", [
713
+ { id: 1, name: "resources", type: "ListMcpResourcesExecResult_McpResource", repeated: true },
714
+ ]);
715
+ addType(root, "ListMcpResourcesError", [{ id: 1, name: "error", type: "string" }]);
716
+ addType(root, "ListMcpResourcesRejected", [{ id: 1, name: "reason", type: "string" }]);
717
+ addType(root, "ListMcpResourcesExecResult", [
718
+ { id: 1, name: "success", type: "ListMcpResourcesSuccess" },
719
+ { id: 2, name: "error", type: "ListMcpResourcesError" },
720
+ { id: 3, name: "rejected", type: "ListMcpResourcesRejected" },
721
+ ], [{ name: "result", fields: ["success", "error", "rejected"] }]);
722
+ addType(root, "ReadMcpResourceExecArgs", [
723
+ { id: 1, name: "server", type: "string" },
724
+ { id: 2, name: "uri", type: "string" },
725
+ { id: 3, name: "download_path", type: "string" },
726
+ { id: 4, name: "tool_call_id", type: "string" },
727
+ { id: 5, name: "smart_mode_approval", type: "SmartModeApproval" },
728
+ ]);
729
+ {
730
+ // Interleaved field order matches agent.proto exactly: the text/blob oneof
731
+ // (5/6) sits between the shared metadata (1-4) and the trailing
732
+ // annotations/download/output fields (7-9).
733
+ const t = new protobuf.Type("ReadMcpResourceSuccess");
734
+ t.add(new protobuf.Field("uri", 1, "string"));
735
+ t.add(new protobuf.Field("name", 2, "string"));
736
+ t.add(new protobuf.Field("description", 3, "string"));
737
+ t.add(new protobuf.Field("mime_type", 4, "string"));
738
+ t.add(new protobuf.Field("text", 5, "string"));
739
+ t.add(new protobuf.Field("blob", 6, "bytes"));
740
+ t.add(new protobuf.MapField("annotations", 7, "string", "string"));
741
+ t.add(new protobuf.Field("download_path", 8, "string"));
742
+ t.add(new protobuf.Field("output_location", 9, "OutputLocation"));
743
+ t.add(new protobuf.OneOf("content", ["text", "blob"]));
744
+ root.add(t);
745
+ }
746
+ addType(root, "ReadMcpResourceError", [
747
+ { id: 1, name: "uri", type: "string" },
748
+ { id: 2, name: "error", type: "string" },
749
+ ]);
750
+ // Cursor quirk: on `rejected` only `reason` is set — `uri` (field 1) is
751
+ // deliberately left empty. Mirror this; do not backfill uri on encode.
752
+ addType(root, "ReadMcpResourceRejected", [
753
+ { id: 1, name: "uri", type: "string" },
754
+ { id: 2, name: "reason", type: "string" },
755
+ ]);
756
+ addType(root, "ReadMcpResourceNotFound", [{ id: 1, name: "uri", type: "string" }]);
757
+ addType(root, "ReadMcpResourceExecResult", [
758
+ { id: 1, name: "success", type: "ReadMcpResourceSuccess" },
759
+ { id: 2, name: "error", type: "ReadMcpResourceError" },
760
+ { id: 3, name: "rejected", type: "ReadMcpResourceRejected" },
761
+ { id: 4, name: "not_found", type: "ReadMcpResourceNotFound" },
762
+ ], [{ name: "result", fields: ["success", "error", "rejected", "not_found"] }]);
682
763
  // ── request_context (#10): server-initiated setup probe ──
683
764
  // At the start of an agent turn the server sends ExecServerMessage
684
765
  // {request_context_args} to ask the client for workspace/env/tool context.
@@ -841,6 +922,8 @@ export function createMessageTypes() {
841
922
  { id: 11, name: "mcp_args", type: "McpArgs" },
842
923
  { id: 14, name: "shell_stream_args", type: "ShellArgs" },
843
924
  { id: 16, name: "background_shell_spawn_args", type: "BackgroundShellSpawnArgs" },
925
+ { id: 17, name: "list_mcp_resources_exec_args", type: "ListMcpResourcesExecArgs" },
926
+ { id: 18, name: "read_mcp_resource_exec_args", type: "ReadMcpResourceExecArgs" },
844
927
  { id: 28, name: "subagent_args", type: "SubagentArgs" },
845
928
  { id: 36, name: "mcp_state_exec_args", type: "McpStateExecArgs" },
846
929
  { id: 45, name: "pi_read_args", type: "PiReadToolArgs" },
@@ -852,7 +935,8 @@ export function createMessageTypes() {
852
935
  { id: 51, name: "pi_ls_args", type: "PiLsToolArgs" },
853
936
  ], [{ name: "args", fields: [
854
937
  "write_args", "delete_args", "grep_args", "read_args", "ls_args",
855
- "request_context_args", "mcp_args", "shell_stream_args", "background_shell_spawn_args", "mcp_state_exec_args",
938
+ "request_context_args", "mcp_args", "shell_stream_args", "background_shell_spawn_args",
939
+ "list_mcp_resources_exec_args", "read_mcp_resource_exec_args", "mcp_state_exec_args",
856
940
  "subagent_args",
857
941
  "pi_read_args", "pi_bash_args", "pi_edit_args", "pi_write_args",
858
942
  "pi_grep_args", "pi_find_args", "pi_ls_args",
@@ -871,6 +955,8 @@ export function createMessageTypes() {
871
955
  { id: 11, name: "mcp_result", type: "McpResult" },
872
956
  { id: 14, name: "shell_stream", type: "ShellStream" },
873
957
  { id: 16, name: "background_shell_spawn_result", type: "BackgroundShellSpawnResult" },
958
+ { id: 17, name: "list_mcp_resources_exec_result", type: "ListMcpResourcesExecResult" },
959
+ { id: 18, name: "read_mcp_resource_exec_result", type: "ReadMcpResourceExecResult" },
874
960
  { id: 28, name: "subagent_result", type: "SubagentResult" },
875
961
  { id: 36, name: "mcp_state_exec_result", type: "McpStateExecResult" },
876
962
  { id: 46, name: "pi_read_result", type: "PiReadExecResult" },
@@ -882,7 +968,8 @@ export function createMessageTypes() {
882
968
  { id: 52, name: "pi_ls_result", type: "PiLsExecResult" },
883
969
  ], [{ name: "result", fields: [
884
970
  "write_result", "delete_result", "grep_result", "read_result", "ls_result",
885
- "request_context_result", "mcp_result", "shell_stream", "background_shell_spawn_result", "mcp_state_exec_result",
971
+ "request_context_result", "mcp_result", "shell_stream", "background_shell_spawn_result",
972
+ "list_mcp_resources_exec_result", "read_mcp_resource_exec_result", "mcp_state_exec_result",
886
973
  "subagent_result",
887
974
  "pi_read_result", "pi_bash_result", "pi_edit_result", "pi_write_result",
888
975
  "pi_grep_result", "pi_find_result", "pi_ls_result",
@@ -9,6 +9,8 @@ export type OpencodeToolDef = {
9
9
  };
10
10
  export declare const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
11
11
  export declare const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
12
+ export declare const CUSTOM_LIST_MCP_RESOURCES_TOOL = "custom_list_mcp_resources";
13
+ export declare const CUSTOM_READ_MCP_RESOURCE_TOOL = "custom_read_mcp_resource";
12
14
  /** Cursor-facing alias → exact host tool name accepted by the AI SDK call. */
13
15
  export type ToolAliasRegistry = ReadonlyMap<string, string>;
14
16
  export type AliasedToolCatalog = {
@@ -139,11 +141,31 @@ export declare function mapCursorArgsToOpencode(toolName: string, raw: Record<st
139
141
  * → github_create_pull_request). Builtins under the default server stay bare.
140
142
  */
141
143
  export declare function mcpRealToolName(mcpArgs: Record<string, unknown>, defaultServer?: string): string;
144
+ /**
145
+ * A read target addressed by URI scheme rather than by local filesystem path.
146
+ *
147
+ * Cursor's LocalReadExecutor only ever reads local files, so this provider
148
+ * mirrors its pre-execution validation. Host read tools are not so limited:
149
+ * oh-my-pi's `read` accepts "a path, URL, or internal URI", and mounts every
150
+ * discoverable tool — including all MCP server tools — under `xd://<tool>`
151
+ * device URLs that are read for their schema and written to for execution
152
+ * (`tools.xdev`, on by default). Resolving those against the workspace root
153
+ * mangles `xd://everything_echo` into `<root>/xd:/everything_echo`, which stats
154
+ * as missing and gets refused as `file_not_found` — so the host's read tool is
155
+ * never called and every xd://-mounted MCP tool is unreachable through this
156
+ * provider. Such targets must be forwarded untouched and left to the host.
157
+ *
158
+ * Requires a scheme of two or more characters followed by `://`, so Windows
159
+ * drive letters (`C:\src`, `C:/src`) stay local paths.
160
+ */
161
+ export declare function isUriReadTarget(requested: string): boolean;
142
162
  /**
143
163
  * Resolve a read target the way Cursor's LocalReadExecutor does before it
144
164
  * stats the file: untildify, then resolve a relative path against the workspace
145
165
  * root (`env.workspace_paths[0]`), else resolve as-is. Kept in lockstep with
146
166
  * agent utils `resolvePath(path, workspaceRoot)`.
167
+ *
168
+ * Only meaningful for local paths; check {@link isUriReadTarget} first.
147
169
  */
148
170
  export declare function resolveReadTargetPath(requested: string, workspaceRoot: string): string;
149
171
  /**
@@ -253,3 +275,16 @@ export declare function buildRequestContextResult(execId: number, requestContext
253
275
  * provider's virtual MCP servers and their tools are available.
254
276
  */
255
277
  export declare function buildMcpStateResult(execId: number, args: Record<string, unknown>, requestContext: Record<string, unknown>): Uint8Array;
278
+ /**
279
+ * Total fallback for Cursor's native MCP-resource exec channel (agent.v1
280
+ * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B
281
+ * `list_mcp_resources`/`read_mcp_resource` execute through the ordinary
282
+ * field-11 MCP path via the alias rules above, so any 17/18 that still reaches
283
+ * this provider is Cursor emitting the native variant unsolicited (its own
284
+ * client owns both executors unconditionally — there is no descriptor to
285
+ * un-advertise). Cursor's executors are total; mirror that here rather than
286
+ * guessing a generic success, which risks an endless heartbeat loop.
287
+ */
288
+ export declare function buildListMcpResourcesFallback(execId: number): Uint8Array;
289
+ /** See buildListMcpResourcesFallback. Always an `error`, even for `download_path`. */
290
+ export declare function buildReadMcpResourceFallback(execId: number, server: string, uri: string): Uint8Array;
@@ -15,6 +15,8 @@ import { BACKGROUND_SHELL_MARKER, buildBackgroundShellCommand, } from "../shell-
15
15
  export const REQUEST_CONTEXT_RESULT_FIELD = 10;
16
16
  export const CUSTOM_WEBSEARCH_TOOL = "custom_websearch";
17
17
  export const CUSTOM_WEBFETCH_TOOL = "custom_webfetch";
18
+ export const CUSTOM_LIST_MCP_RESOURCES_TOOL = "custom_list_mcp_resources";
19
+ export const CUSTOM_READ_MCP_RESOURCE_TOOL = "custom_read_mcp_resource";
18
20
  /** Match OpenCode's McpCatalog.sanitize for config server ids. */
19
21
  export function sanitizeMcpServerId(value) {
20
22
  return value.replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -57,12 +59,13 @@ export function resolveToolServerIdentity(opencodeName, defaultServer = "opencod
57
59
  export function toolsToDescriptors(tools, providerIdentifier = "opencode", knownMcpServers = []) {
58
60
  return tools.map((t) => {
59
61
  const id = resolveToolServerIdentity(t.sourceName ?? t.name, providerIdentifier, knownMcpServers);
60
- const collisionSafeWebAlias = t.name === CUSTOM_WEBSEARCH_TOOL || t.name === CUSTOM_WEBFETCH_TOOL;
62
+ const collisionSafeAlias = COLLISION_SAFE_ALIASES.has(t.name);
61
63
  return {
62
64
  // Keep the collision-safe public name exact. Prefixing it with the
63
65
  // synthetic default server weakens the distinction from Cursor-native
64
- // web tools in the model-visible catalog.
65
- name: collisionSafeWebAlias ? t.name : `${id.server}-${id.toolName}`,
66
+ // capabilities (web search/fetch, MCP resource list/read) in the
67
+ // model-visible catalog.
68
+ name: collisionSafeAlias ? t.name : `${id.server}-${id.toolName}`,
66
69
  description: t.description ?? "",
67
70
  input_schema: encodeJsonAsValue(normalizeInputSchema(t.inputSchema)),
68
71
  provider_identifier: id.server,
@@ -76,6 +79,10 @@ function normalizeInputSchema(schema) {
76
79
  }
77
80
  return { type: "object", properties: {} };
78
81
  }
82
+ // Despite the name, these rules cover any advertised OpenCode tool whose bare
83
+ // name collides with a Cursor-native capability, not only web search/fetch —
84
+ // see the MCP resource entries below and
85
+ // tasks/plans/fix-cursor-mcp-resource-exec.md Phase 0/2.
79
86
  const WEB_ALIAS_RULES = [
80
87
  {
81
88
  alias: CUSTOM_WEBSEARCH_TOOL,
@@ -87,7 +94,23 @@ const WEB_ALIAS_RULES = [
87
94
  exact: ["webfetch", "web_fetch"],
88
95
  suffixes: ["_web_fetch", "-web_fetch", "_webfetch", "-webfetch"],
89
96
  },
97
+ {
98
+ alias: CUSTOM_LIST_MCP_RESOURCES_TOOL,
99
+ exact: ["list_mcp_resources"],
100
+ suffixes: ["_list_mcp_resources", "-list_mcp_resources"],
101
+ },
102
+ {
103
+ alias: CUSTOM_READ_MCP_RESOURCE_TOOL,
104
+ exact: ["read_mcp_resource"],
105
+ suffixes: ["_read_mcp_resource", "-read_mcp_resource"],
106
+ },
90
107
  ];
108
+ const COLLISION_SAFE_ALIASES = new Set([
109
+ CUSTOM_WEBSEARCH_TOOL,
110
+ CUSTOM_WEBFETCH_TOOL,
111
+ CUSTOM_LIST_MCP_RESOURCES_TOOL,
112
+ CUSTOM_READ_MCP_RESOURCE_TOOL,
113
+ ]);
91
114
  /**
92
115
  * Give collision-prone web capabilities names Cursor will not confuse with its
93
116
  * native UI-bound WebSearch/WebFetch interactions. Exact host tools win; a
@@ -1100,11 +1123,33 @@ function findOneOfVariant(msg, candidates) {
1100
1123
  function untildify(input) {
1101
1124
  return input.replace(/^~(?=$|\/|\\)/, os.homedir());
1102
1125
  }
1126
+ /**
1127
+ * A read target addressed by URI scheme rather than by local filesystem path.
1128
+ *
1129
+ * Cursor's LocalReadExecutor only ever reads local files, so this provider
1130
+ * mirrors its pre-execution validation. Host read tools are not so limited:
1131
+ * oh-my-pi's `read` accepts "a path, URL, or internal URI", and mounts every
1132
+ * discoverable tool — including all MCP server tools — under `xd://<tool>`
1133
+ * device URLs that are read for their schema and written to for execution
1134
+ * (`tools.xdev`, on by default). Resolving those against the workspace root
1135
+ * mangles `xd://everything_echo` into `<root>/xd:/everything_echo`, which stats
1136
+ * as missing and gets refused as `file_not_found` — so the host's read tool is
1137
+ * never called and every xd://-mounted MCP tool is unreachable through this
1138
+ * provider. Such targets must be forwarded untouched and left to the host.
1139
+ *
1140
+ * Requires a scheme of two or more characters followed by `://`, so Windows
1141
+ * drive letters (`C:\src`, `C:/src`) stay local paths.
1142
+ */
1143
+ export function isUriReadTarget(requested) {
1144
+ return /^[a-zA-Z][a-zA-Z0-9+.-]+:\/\//.test(requested);
1145
+ }
1103
1146
  /**
1104
1147
  * Resolve a read target the way Cursor's LocalReadExecutor does before it
1105
1148
  * stats the file: untildify, then resolve a relative path against the workspace
1106
1149
  * root (`env.workspace_paths[0]`), else resolve as-is. Kept in lockstep with
1107
1150
  * agent utils `resolvePath(path, workspaceRoot)`.
1151
+ *
1152
+ * Only meaningful for local paths; check {@link isUriReadTarget} first.
1108
1153
  */
1109
1154
  export function resolveReadTargetPath(requested, workspaceRoot) {
1110
1155
  const expanded = untildify(requested);
@@ -1916,6 +1961,35 @@ function mcpStateToolDefinition(serverIdentifier, descriptor, flatTools) {
1916
1961
  tool_name: toolName,
1917
1962
  };
1918
1963
  }
1964
+ /**
1965
+ * Total fallback for Cursor's native MCP-resource exec channel (agent.v1
1966
+ * fields #17/#18, tasks/plans/fix-cursor-mcp-resource-exec.md). Under Option B
1967
+ * `list_mcp_resources`/`read_mcp_resource` execute through the ordinary
1968
+ * field-11 MCP path via the alias rules above, so any 17/18 that still reaches
1969
+ * this provider is Cursor emitting the native variant unsolicited (its own
1970
+ * client owns both executors unconditionally — there is no descriptor to
1971
+ * un-advertise). Cursor's executors are total; mirror that here rather than
1972
+ * guessing a generic success, which risks an endless heartbeat loop.
1973
+ */
1974
+ export function buildListMcpResourcesFallback(execId) {
1975
+ return encodeMessage("AgentClientMessage", {
1976
+ exec_client_message: {
1977
+ id: execId,
1978
+ list_mcp_resources_exec_result: { success: { resources: [] } },
1979
+ },
1980
+ });
1981
+ }
1982
+ /** See buildListMcpResourcesFallback. Always an `error`, even for `download_path`. */
1983
+ export function buildReadMcpResourceFallback(execId, server, uri) {
1984
+ return encodeMessage("AgentClientMessage", {
1985
+ exec_client_message: {
1986
+ id: execId,
1987
+ read_mcp_resource_exec_result: {
1988
+ error: { uri, error: `Server "${server}" not found` },
1989
+ },
1990
+ },
1991
+ });
1992
+ }
1919
1993
  function recordValue(value) {
1920
1994
  return value && typeof value === "object" && !Array.isArray(value)
1921
1995
  ? value
package/dist/usage.d.ts CHANGED
@@ -19,6 +19,21 @@ export type CursorUsageEstimate = {
19
19
  cacheWrite: number;
20
20
  reasoningTokens: number;
21
21
  };
22
+ /**
23
+ * A single-step trust path for TurnEnded counters is only safe when the
24
+ * totals are close to the request-local budget. Cursor reports cumulative
25
+ * conversation/Run totals even on Runs that never crossed an OpenCode tool
26
+ * boundary: single-step follow-up turns and parallel no-tools side calls
27
+ * carry the whole conversation's `cache_read`. Treating those totals as
28
+ * request-local inflates OpenCode's context usage and triggers premature
29
+ * compaction (issue #20).
30
+ */
31
+ export declare const CUMULATIVE_TURN_ENDED_RATIO = 3;
32
+ export declare const MIN_TRUSTED_TURN_ENDED_TOTAL = 1024;
33
+ export declare function exceedsRequestLocalBudget(te: Record<string, unknown>, budget: {
34
+ inputTokens: number;
35
+ outputTokens: number;
36
+ }): boolean;
22
37
  /**
23
38
  * Request-local usage for a held Cursor Run.
24
39
  *
package/dist/usage.js CHANGED
@@ -35,6 +35,30 @@ export function buildLanguageModelV3UsageFromTurnEnded(te) {
35
35
  },
36
36
  };
37
37
  }
38
+ /**
39
+ * A single-step trust path for TurnEnded counters is only safe when the
40
+ * totals are close to the request-local budget. Cursor reports cumulative
41
+ * conversation/Run totals even on Runs that never crossed an OpenCode tool
42
+ * boundary: single-step follow-up turns and parallel no-tools side calls
43
+ * carry the whole conversation's `cache_read`. Treating those totals as
44
+ * request-local inflates OpenCode's context usage and triggers premature
45
+ * compaction (issue #20).
46
+ */
47
+ export const CUMULATIVE_TURN_ENDED_RATIO = 3;
48
+ export const MIN_TRUSTED_TURN_ENDED_TOTAL = 1024;
49
+ export function exceedsRequestLocalBudget(te, budget) {
50
+ const total = turnEndedCounter(te, "input_tokens") +
51
+ turnEndedCounter(te, "cache_read") +
52
+ turnEndedCounter(te, "cache_write");
53
+ const requestLocal = budget.inputTokens + budget.outputTokens;
54
+ // Skip when the request-local seed is unknown; otherwise compare against
55
+ // max(ratio × request-local, absolute floor) so tiny prompts still reject
56
+ // multi-million-token conversation cache totals.
57
+ if (requestLocal <= 0)
58
+ return false;
59
+ const trustCeiling = Math.max(requestLocal * CUMULATIVE_TURN_ENDED_RATIO, MIN_TRUSTED_TURN_ENDED_TOTAL);
60
+ return total > trustCeiling;
61
+ }
38
62
  /**
39
63
  * Request-local usage for a held Cursor Run.
40
64
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -92,6 +92,7 @@
92
92
  "@opencode-ai/plugin": "^1.18.16",
93
93
  "@opencode-ai/plugin-next": "npm:@opencode-ai/plugin@0.0.0-next-17155",
94
94
  "@tsconfig/node22": "^22.0.5",
95
+ "@types/bun": "^1.3.0",
95
96
  "@types/node": "^22.20.1",
96
97
  "typescript": "^5.9.3"
97
98
  }