diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index e042190d2..1dafe3329 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -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 diff --git a/packages/ai/src/providers/cursor.ts b/packages/ai/src/providers/cursor.ts index b1070c609..ab16d994e 100644 --- a/packages/ai/src/providers/cursor.ts +++ b/packages/ai/src/providers/cursor.ts @@ -2401,7 +2401,7 @@ function readCursorBlob(blobStore: Map, 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" diff --git a/packages/ai/test/cursor-mcp-tool-catalog.test.ts b/packages/ai/test/cursor-mcp-tool-catalog.test.ts new file mode 100644 index 000000000..1c8dda9a2 --- /dev/null +++ b/packages/ai/test/cursor-mcp-tool-catalog.test.ts @@ -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([]); + }); +});