fix(coding-agent/mcp): include customWireName in applied-tool signature

A tool's wire-visible name (`customWireName`) is rendered into the
system prompt body via `toolPromptNames`, but the applied-tool signature
only hashed name+label+description. A future tool whose wire name varied
without touching the other fields would silently produce a stale system
prompt that advertises the wrong callable name to the model — desyncing
prompt guidance from actual tool routing.

Today the only mutation path (edit-mode toggle) is also covered by a
description change and an explicit `refreshBaseSystemPrompt` from
`#syncEditToolModeAfterModelChange`, so this is a defensive fix rather
than a live bug. Including `customWireName` makes the signature a
self-consistent model of the prompt inputs.

- Extended `describeTool` in `#computeAppliedToolSignature` to include
  `tool.customWireName ?? ""`. Applies to both the active-tool segment
  and the (mcpDiscoveryEnabled) registry segment via the shared helper.
- Updated the docstring to call out wire-name coverage.
- Added a regression test that mutates `customWireName` between
  identical-metadata refreshes and asserts the rebuild fires.

Per Codex review on #890.
This commit is contained in:
Miroslav Drbal
2026-04-30 12:23:45 +02:00
parent b5ca55e79f
commit 08cb3f8555
2 changed files with 45 additions and 6 deletions
@@ -300,4 +300,38 @@ describe("AgentSession refreshMCPTools rebuild skipping", () => {
await session.refreshMCPTools([active, discoverableV2]);
expect(rebuildCount).toBe(baseline + 1);
});
it("rebuilds when an MCP tool's customWireName changes", async () => {
// `customWireName` overrides the model-facing tool name (e.g. `edit` exposes
// itself as `apply_patch` to GPT-5). The wire name is rendered into the prompt
// body via `toolPromptNames`, so a wire-name flip with the rest of the metadata
// constant would otherwise leave a stale system prompt that advertises the wrong
// callable name to the model. The signature must catch this.
let rebuildCount = 0;
const { session } = newSession(async toolNames => {
rebuildCount++;
return `tools:${toolNames.join(",")}`;
});
// Attach a custom wire name to the MCP tool. `applyToolProxy` forwards arbitrary
// properties from the underlying CustomTool to the wrapper, so the AgentTool the
// signature inspects exposes `customWireName` as if it were declared on the type.
const v1 = createMcpCustomTool("mcp__nucleus_search", "nucleus", "search", "Search");
const v1WithWire = { ...v1, customWireName: "wire_v1" } as typeof v1 & { customWireName: string };
await session.refreshMCPTools([v1WithWire]);
expect(rebuildCount).toBe(1);
// Same wire name: skip.
await session.refreshMCPTools([v1WithWire]);
expect(rebuildCount).toBe(1);
// Wire name changes while name/label/description stay constant: must rebuild.
const v2WithWire = { ...v1, customWireName: "wire_v2" } as typeof v1 & { customWireName: string };
await session.refreshMCPTools([v2WithWire]);
expect(rebuildCount).toBe(2);
// Drop wire name entirely: must rebuild (signature must differ from `wire_v2`).
await session.refreshMCPTools([v1]);
expect(rebuildCount).toBe(3);
});
});