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:
@@ -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
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user