feat(FN-2703): support custom MCP tools in plan generation

- Remove ToolSearch prerequisite so custom MCP tools can be used during triage and plan generation flows
- Align built-in tool sets between provider wiring and prompt builder handling, including custom ls behavior
- Expand pi-claude-cli tests for event bridge, MCP config, prompt builder, and tool mapping regressions
- Add FN-2703 changeset and delivery documentation for the published @runfusion/fusion package
This commit is contained in:
Fusion
2026-04-27 09:51:49 -07:00
committed by gsxdsm
parent b67d409480
commit fed1d6975a
8 changed files with 141 additions and 16 deletions

View File

@@ -970,6 +970,46 @@ describe("createEventBridge", () => {
// Custom tool args pass through unchanged (no argument renames for MCP tools)
expect(event.toolCall.arguments).toEqual({ target: "prod" });
});
it("handles MCP-prefixed triage tool names", () => {
const bridge = createBridgeWithStart();
bridge.handleEvent({
type: "content_block_start",
index: 0,
content_block: {
type: "tool_use",
id: "toolu_triage1",
name: "mcp__custom-tools__fn_task_list",
},
});
bridge.handleEvent({
type: "content_block_stop",
index: 0,
});
bridge.handleEvent({
type: "content_block_start",
index: 1,
content_block: {
type: "tool_use",
id: "toolu_triage2",
name: "mcp__custom-tools__fn_review_spec",
},
});
bridge.handleEvent({
type: "content_block_stop",
index: 1,
});
const output = bridge.getOutput();
const calls = output.content.filter((c: any) => c.type === "toolCall") as any[];
expect(calls.map((call) => call.name)).toEqual([
"fn_task_list",
"fn_review_spec",
]);
expect(calls[0].arguments).toEqual({});
expect(calls[1].arguments).toEqual({});
});
});
describe("internal Claude Code tools are filtered", () => {

View File

@@ -16,7 +16,7 @@ vi.mock("node:os", () => ({
tmpdir: mocks.tmpdir,
}));
import { getCustomToolDefs, writeMcpConfig } from "../mcp-config";
import { getCustomToolDefs, writeMcpConfig, toolsFromContext } from "../mcp-config";
import type { McpToolDef } from "../mcp-config";
describe("getCustomToolDefs", () => {
@@ -171,6 +171,22 @@ describe("getCustomToolDefs", () => {
});
});
describe("toolsFromContext", () => {
it("keeps ls as a custom tool (not filtered as built-in)", () => {
const defs = toolsFromContext([
{ name: "read", description: "builtin", parameters: { type: "object" } },
{ name: "ls", description: "list files", parameters: { type: "object" } },
{
name: "fn_task_list",
description: "list tasks",
parameters: { type: "object", properties: {} },
},
]);
expect(defs.map((d) => d.name)).toEqual(["ls", "fn_task_list"]);
});
});
describe("writeMcpConfig", () => {
beforeEach(() => {
vi.clearAllMocks();

View File

@@ -949,6 +949,47 @@ describe("buildSystemPrompt", () => {
expect(result).toContain("mcp__custom-tools__fn_review_spec");
});
it("treats ls as custom and rewrites to mcp__custom-tools__ls", async () => {
vi.resetModules();
vi.doMock("node:fs", () => ({
existsSync: () => false,
readFileSync: () => "",
}));
const { buildSystemPrompt: bsp } = await import("../prompt-builder");
const context = {
systemPrompt: "List files with ls, then continue.",
messages: [],
tools: [
{ name: "ls", description: "list directory", parameters: {} },
{ name: "read", description: "builtin", parameters: {} },
],
} as unknown as any;
const result = bsp(context, "/some/project");
expect(result).toContain("mcp__custom-tools__ls");
expect(result).not.toContain("mcp__custom-tools__read");
});
it("custom tools addendum instructs direct MCP calls without ToolSearch prerequisite", async () => {
vi.resetModules();
vi.doMock("node:fs", () => ({
existsSync: () => false,
readFileSync: () => "",
}));
const { buildSystemPrompt: bsp } = await import("../prompt-builder");
const context = {
systemPrompt: "Use tools as needed.",
messages: [],
tools: [{ name: "fn_task_list", description: "list", parameters: {} }],
} as unknown as any;
const result = bsp(context, "/some/project");
expect(result).toContain("mcp__custom-tools__fn_task_list");
expect(result).not.toContain("ToolSearch");
});
it("does not rewrite identifier substrings that happen to overlap a tool name", async () => {
vi.resetModules();
vi.doMock("node:fs", () => ({

View File

@@ -228,6 +228,7 @@ describe("tool-mapping", () => {
it("returns true for custom tool names", () => {
expect(isCustomToolName("myTool")).toBe(true);
expect(isCustomToolName("deploy")).toBe(true);
expect(isCustomToolName("ls")).toBe(true);
});
it("returns false for all 6 built-in tool names", () => {