From a562d5367f194b8f6b2897c1588c270a7c2a1c20 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 2 Apr 2026 19:22:33 +0900 Subject: [PATCH] fix: strip mcp_ prefix from tool names before dispatch The model emits tool names like mcp_background_output but the runtime registry has them as background_output. While transformToolName already handles the prefix for display purposes, the tool dispatch path in tool-execute-before was not stripping it, causing 'unavailable tool' errors. This adds mcp_ prefix stripping at the earliest point in the tool execution pipeline, fixing background_output, background_cancel, and all nocturne-memory_* tools. Closes #2697 --- .../tool-execute-before-mcp-prefix.test.ts | 102 ++++++++++++++++++ src/plugin/tool-execute-before.ts | 13 +++ 2 files changed, 115 insertions(+) create mode 100644 src/plugin/tool-execute-before-mcp-prefix.test.ts diff --git a/src/plugin/tool-execute-before-mcp-prefix.test.ts b/src/plugin/tool-execute-before-mcp-prefix.test.ts new file mode 100644 index 000000000..61e0cf43b --- /dev/null +++ b/src/plugin/tool-execute-before-mcp-prefix.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test, mock, beforeEach } from "bun:test" +import { createToolExecuteBeforeHandler } from "./tool-execute-before" +import type { PluginContext } from "./types" +import type { CreatedHooks } from "../create-hooks" + +function createMockContext(): PluginContext { + return { + directory: "/tmp/test-dir", + sessionId: "test-session", + } as unknown as PluginContext +} + +function createMockHooks(): CreatedHooks { + return { + ralphLoop: null, + startWork: null, + autoSlashCommand: null, + } as unknown as CreatedHooks +} + +describe("tool-execute-before mcp_ prefix stripping", () => { + test("should strip mcp_ prefix from mcp_background_output", async () => { + // given + const handler = createToolExecuteBeforeHandler({ + ctx: createMockContext(), + hooks: createMockHooks(), + }) + const input = { tool: "mcp_background_output", sessionID: "ses_123", callID: "call_123" } + const output = { args: {} } + + // when + await handler(input, output) + + // then + expect(input.tool).toBe("background_output") + }) + + test("should strip mcp_ prefix from mcp_background_cancel", async () => { + // given + const handler = createToolExecuteBeforeHandler({ + ctx: createMockContext(), + hooks: createMockHooks(), + }) + const input = { tool: "mcp_background_cancel", sessionID: "ses_123", callID: "call_123" } + const output = { args: {} } + + // when + await handler(input, output) + + // then + expect(input.tool).toBe("background_cancel") + }) + + test("should strip mcp_ prefix from mcp_nocturne-memory_read_memory", async () => { + // given + const handler = createToolExecuteBeforeHandler({ + ctx: createMockContext(), + hooks: createMockHooks(), + }) + const input = { tool: "mcp_nocturne-memory_read_memory", sessionID: "ses_123", callID: "call_123" } + const output = { args: {} } + + // when + await handler(input, output) + + // then + expect(input.tool).toBe("nocturne-memory_read_memory") + }) + + test("should NOT strip mcp_ prefix from tools that already work (e.g., mcp_bash)", async () => { + // given — mcp_bash works fine because OpenCode handles it natively, + // but our prefix stripping should still normalize it + const handler = createToolExecuteBeforeHandler({ + ctx: createMockContext(), + hooks: createMockHooks(), + }) + const input = { tool: "mcp_bash", sessionID: "ses_123", callID: "call_123" } + const output = { args: { command: "echo hello" } } + + // when + await handler(input, output) + + // then — prefix stripped, bash handler still processes it + expect(input.tool).toBe("bash") + }) + + test("should not modify tool names without mcp_ prefix", async () => { + // given + const handler = createToolExecuteBeforeHandler({ + ctx: createMockContext(), + hooks: createMockHooks(), + }) + const input = { tool: "background_output", sessionID: "ses_123", callID: "call_123" } + const output = { args: {} } + + // when + await handler(input, output) + + // then + expect(input.tool).toBe("background_output") + }) +}) diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index 508f40dde..a0d0a22d2 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -52,6 +52,19 @@ export function createToolExecuteBeforeHandler(args: { } return async (input, output): Promise => { + // Strip mcp_ prefix from tool names — the model may emit mcp_background_output + // but the runtime registry has it as background_output (fixes #2697) + if (/^mcp_/i.test(input.tool)) { + const stripped = input.tool.replace(/^mcp_/i, "") + log("[tool-execute-before] Stripped mcp_ prefix from tool name", { + original: input.tool, + resolved: stripped, + sessionID: input.sessionID, + callID: input.callID, + }) + input.tool = stripped + } + if (input.tool.toLowerCase() === "bash" && typeof output.args.command === "string") { if (output.args.command.includes("\x00")) { replaceToolArgs(output, { command: output.args.command.replace(/\x00/g, "") })