fix(coding-agent): preserved mixed-case plugin tool names on refresh

This commit is contained in:
Harsha Vardhan
2026-08-14 20:57:16 +10:00
committed by can1357
parent 0d929bf586
commit ba5885880e
4 changed files with 39 additions and 3 deletions
+4
View File
@@ -51,6 +51,10 @@
- Continued Cursor turns that died with `NGHTTP2_INTERNAL_ERROR` / `NGHTTP2_REFUSED_STREAM` after tool calls already had results, instead of leaving the agent idle until the user typed "continue". HTTP/2 stream resets now use the same preserve-and-continue path as idle stream stalls, without requiring the Cursor exec-resolved marker that MCP/todo blocks never carry.
### Fixed
- Stopped mixed-case plugin tool names from being lowercased during tool-set refresh, which unmounted them from `xd://` whenever MCP tools connected.
## [17.3.3] - 2026-08-14
### Fixed
@@ -41,10 +41,14 @@ const LEGACY_BUILTIN_TOOL_NAME_ALIASES: ReadonlyMap<string, BuiltinToolName> = n
["find", "glob"],
]);
/** Return the canonical tool name for current and legacy built-in tool IDs. */
const CANONICAL_TOOL_NAMES: Record<string, true> = Object.fromEntries(
[...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES].map(name => [name, true]),
);
/** Canonicalize built-in IDs and legacy aliases. Leave plugin names unchanged. */
export function normalizeToolName(name: string): string {
const normalized = name.toLowerCase();
return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(normalized) ?? normalized;
const lower = name.toLowerCase();
return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(lower) ?? (Object.hasOwn(CANONICAL_TOOL_NAMES, lower) ? lower : name);
}
/** Normalize and deduplicate tool names while preserving first-seen order. */
@@ -1289,4 +1289,21 @@ These tools became available:
expect(session.getToolByName(newTool.name)).toBeDefined();
expect(session.getActiveToolNames()).toContain(newTool.name);
});
it("keeps mixed-case plugin devices mounted when MCP tools refresh", async () => {
const xdevState = createTestXdevState();
const { session, toolRegistry } = newSession(async toolNames => `tools:${toolNames.join(",")}`, {
xdev: xdevState,
});
const pluginTool = { ...createBasicTool("CaseAdd", "Case Add"), loadMode: "discoverable" as const };
toolRegistry.set(pluginTool.name, pluginTool);
xdevState.mountedNames.add(pluginTool.name);
await session.refreshMCPTools([
createMcpCustomTool("mcp__nucleus_search", "nucleus", "search", "Search nucleus"),
]);
expect(session.getMountedXdevToolNames()).toContain("CaseAdd");
expect(session.getToolByName("CaseAdd")).toBeDefined();
});
});
@@ -0,0 +1,11 @@
import { describe, expect, it } from "bun:test";
import { normalizeToolName } from "../src/tools/builtin-names";
describe("normalizeToolName", () => {
it("maps legacy builtin aliases without touching plugin case", () => {
expect(normalizeToolName("search")).toBe("grep");
expect(normalizeToolName("Read")).toBe("read");
expect(normalizeToolName("CaseAdd")).toBe("CaseAdd");
expect(normalizeToolName("Constructor")).toBe("Constructor");
});
});