fix(cursor): forward write transport so xd:// previews resolve

Cursor forwards mounted xd:// devices (e.g. ast_edit) into its
request-context catalog but filtered the built-in write tool out via
CURSOR_NATIVE_TOOL_NAMES. ast_edit always stages a dry-run preview whose
resolution rides a write to xd://resolve / xd://reject, so with no
model-visible write the SoftToolRequirement('write') escalation aborted
the turn after three forced turns.

Re-include write in the forwarded catalog whenever pi-agent devices are
advertised; other native tools stay filtered.

Fixes #6536
This commit is contained in:
roboomp
2026-07-24 19:50:12 +00:00
parent a38cd95d7d
commit 5179b7e342
3 changed files with 62 additions and 2 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed Cursor sessions exposing `ast_edit` (and other staged-preview `xd://` devices) without a reachable resolver: the built-in `write` tool — which carries the `xd://resolve` / `xd://reject` transport that finalizes a staged preview — was filtered out of Cursor's forwarded catalog, so previews could never be resolved and the session aborted after three forced `write` turns. `write` is now re-included in the forwarded catalog whenever pi-agent devices are advertised ([#6536](https://github.com/can1357/oh-my-pi/issues/6536)).
## [17.1.2] - 2026-07-24
### Added
+11 -2
View File
@@ -2401,7 +2401,7 @@ function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array):
const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp", "todo"]);
function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
export function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
if (!tools || tools.length === 0) {
return [];
}
@@ -2411,7 +2411,16 @@ function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[]
return [];
}
return advertisedTools.map(tool => {
// The `write` tool doubles as the xd:// transport: forwarded devices such as
// `ast_edit` stage previews finalized only by writing a reason to xd://resolve
// or xd://reject. Cursor's native catalog may expose no write path, so
// re-include the built-in `write` (dropped as native above) whenever pi-agent
// devices are advertised — otherwise a staged preview can never be resolved
// and the SoftToolRequirement('write') escalation aborts the turn.
const writeTool = tools.find(tool => tool.name === "write");
const forwarded = writeTool ? [...advertisedTools, writeTool] : advertisedTools;
return forwarded.map(tool => {
const jsonSchema = toolWireSchema(tool);
const schemaValue: JsonValue =
jsonSchema && typeof jsonSchema === "object"
@@ -0,0 +1,47 @@
import { describe, expect, it } from "bun:test";
import { buildMcpToolDefinitions } from "@oh-my-pi/pi-ai/providers/cursor";
import type { Tool } from "@oh-my-pi/pi-ai/types";
const tool = (name: string): Tool => ({
name,
description: `${name} tool`,
parameters: { type: "object", properties: {} },
});
describe("cursor buildMcpToolDefinitions", () => {
it("forwards the write transport alongside preview-staging devices so xd:// resolution stays reachable", () => {
// A Cursor session with xdev on: ast_edit is a mounted device, write is the
// xd:// transport carried top-level. ast_edit always stages a preview whose
// resolution rides `write xd://resolve` / `write xd://reject`.
const defs = buildMcpToolDefinitions([
tool("read"),
tool("write"),
tool("bash"),
tool("todo"),
tool("ast_edit"),
tool("task"),
]);
const names = defs.map(def => def.name);
expect(names).toContain("ast_edit");
expect(names).toContain("task");
// `write` is the sole native-filtered tool re-included: without it the
// staged preview can never be resolved and the SoftToolRequirement('write')
// escalation aborts the turn.
expect(names).toContain("write");
expect(names).not.toContain("read");
expect(names).not.toContain("bash");
expect(names).not.toContain("todo");
// The forwarded write must be a routable pi-agent MCP tool, so Cursor
// dispatches it back through the coding-agent write tool's xd:// handler.
const writeDef = defs.find(def => def.name === "write");
expect(writeDef?.providerIdentifier).toBe("pi-agent");
expect(writeDef?.toolName).toBe("write");
});
it("keeps write out when only native tools are advertised (no pi-agent device needs resolution)", () => {
const names = buildMcpToolDefinitions([tool("read"), tool("write"), tool("bash")]).map(def => def.name);
expect(names).toEqual([]);
});
});