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:
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user