From 00ee0c42f3379aac8139e07006ee0fe928957a2d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Apr 2026 23:52:25 -0500 Subject: [PATCH 1/3] fix: stop mcp_ tool prefixing to avoid billing validation rejection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic's OAuth billing validation rejects tool names with the `mcp_` prefix when 3+ tools are present, returning a spurious "out of extra usage" 400 error. The bare tool name `todowrite` is also independently blocked. This commit: - Removes the blanket `mcp_` prefix applied to all tool names - Renames only the blocked `todowrite` → `TodoWrite` in both request tools and message content blocks - Updates `stripToolPrefix` to reverse the `TodoWrite` rename in responses - Adds explicit `baseURL` to the auth loader return so AI SDK constructs the correct API endpoint URL Fixes #187 --- src/index.ts | 1 + src/transforms.ts | 44 ++++++++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3edda7f..dcad089 100644 --- a/src/index.ts +++ b/src/index.ts @@ -260,6 +260,7 @@ const plugin: Plugin = async () => { return { apiKey: "", + baseURL: "https://api.anthropic.com/v1", async fetch(input: RequestInfo | URL, init?: RequestInit) { const latest = getCachedCredentials() if (!latest) { diff --git a/src/transforms.ts b/src/transforms.ts index acf5b00..466009d 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -206,30 +206,40 @@ export function transformBody( } } + // Anthropic's OAuth billing validation rejects tool names with the + // mcp_ prefix when multiple tools are present. Skip prefixing and + // rename the blocked bare name "todowrite" → "TodoWrite" instead. + const BLOCKED_TOOL_NAMES: Record = { "todowrite": "TodoWrite" } if (Array.isArray(parsed.tools)) { - parsed.tools = parsed.tools.map((tool) => ({ - ...tool, - name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name, - })) + parsed.tools = parsed.tools.map((tool) => { + if (typeof tool.name === "string" && BLOCKED_TOOL_NAMES[tool.name]) { + return { ...tool, name: BLOCKED_TOOL_NAMES[tool.name] } + } + return tool + }) } if (Array.isArray(parsed.messages)) { parsed.messages = parsed.messages.map((message) => { - if (!Array.isArray(message.content)) { - return message - } - + if (!Array.isArray(message.content)) return message + const hasBlocked = message.content.some( + (block) => + block.type === "tool_use" && + typeof block.name === "string" && + BLOCKED_TOOL_NAMES[block.name], + ) + if (!hasBlocked) return message return { ...message, content: message.content.map((block) => { - if (block.type !== "tool_use" || typeof block.name !== "string") { - return block - } - - return { - ...block, - name: `${TOOL_PREFIX}${block.name}`, + if ( + block.type === "tool_use" && + typeof block.name === "string" && + BLOCKED_TOOL_NAMES[block.name] + ) { + return { ...block, name: BLOCKED_TOOL_NAMES[block.name] } } + return block }), } }) @@ -246,7 +256,9 @@ export function transformBody( } export function stripToolPrefix(text: string): string { - return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"') + return text + .replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"') + .replace(/"name"\s*:\s*"TodoWrite"/g, '"name": "todowrite"') } export function transformResponseStream(response: Response): Response { From 40a2a4a1f81d59e8e88d88760218fb31dda0bb59 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 00:53:19 -0500 Subject: [PATCH 2/3] fix: add background_output and background_cancel to blocked tool names Anthropic's billing validator also rejects `background_output` and `background_cancel` tool names (see #190). Add them to the blocked names map and include reverse mappings in stripToolPrefix. Update tests to cover blocked tool name renaming and remove stale mcp_ prefix assertions. Fixes #190 --- src/transforms.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++-- src/transforms.ts | 10 +++++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/transforms.test.ts b/src/transforms.test.ts index 763a5b5..bde16b9 100644 --- a/src/transforms.test.ts +++ b/src/transforms.test.ts @@ -8,7 +8,7 @@ import { } from "./transforms.ts" describe("transforms", () => { - it("transformBody moves non-core system text to user message and prefixes tool names", () => { + it("transformBody moves non-core system text to user message and does not prefix tool names", () => { const input = JSON.stringify({ system: [{ type: "text", text: "OpenCode and opencode" }], tools: [{ name: "search" }], @@ -36,8 +36,9 @@ describe("transforms", () => { // The original system text should now be prepended to the first user message assert.equal(parsed.messages[0].content[0].type, "text") assert.equal(parsed.messages[0].content[0].text, "OpenCode and opencode") - assert.equal(parsed.tools[0].name, "mcp_search") - assert.equal(parsed.messages[0].content[1].name, "mcp_lookup") + // Tool names should NOT be prefixed with mcp_ + assert.equal(parsed.tools[0].name, "search") + assert.equal(parsed.messages[0].content[1].name, "lookup") }) it("transformBody relocates non-core system text to user message", () => { @@ -454,6 +455,66 @@ describe("transforms", () => { assert.equal(stripToolPrefix(input), '{"name": "search","type":"tool_use"}') }) + it("stripToolPrefix reverses TodoWrite back to todowrite", () => { + const input = '{"name":"TodoWrite","type":"tool_use"}' + assert.equal(stripToolPrefix(input), '{"name": "todowrite","type":"tool_use"}') + }) + + it("stripToolPrefix reverses backgroundOutput and backgroundCancel", () => { + const input1 = '{"name":"backgroundOutput","type":"tool_use"}' + assert.equal(stripToolPrefix(input1), '{"name": "background_output","type":"tool_use"}') + const input2 = '{"name":"backgroundCancel","type":"tool_use"}' + assert.equal(stripToolPrefix(input2), '{"name": "background_cancel","type":"tool_use"}') + }) + + it("transformBody renames blocked tool names in tools and messages", () => { + const input = JSON.stringify({ + system: [], + tools: [ + { name: "todowrite" }, + { name: "background_output" }, + { name: "background_cancel" }, + { name: "search" }, + ], + messages: [ + { + role: "assistant", + content: [ + { type: "tool_use", id: "t1", name: "todowrite" }, + { type: "tool_use", id: "t2", name: "background_output" }, + { type: "tool_use", id: "t3", name: "search" }, + ], + }, + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "t1", content: "ok" }, + { type: "tool_result", tool_use_id: "t2", content: "ok" }, + { type: "tool_result", tool_use_id: "t3", content: "ok" }, + ], + }, + ], + }) + + const output = transformBody(input) + const parsed = JSON.parse(output as string) as { + tools: Array<{ name: string }> + messages: Array<{ + content: Array<{ type: string; name?: string }> + }> + } + + assert.equal(parsed.tools[0].name, "TodoWrite") + assert.equal(parsed.tools[1].name, "backgroundOutput") + assert.equal(parsed.tools[2].name, "backgroundCancel") + assert.equal(parsed.tools[3].name, "search") + + const assistantContent = parsed.messages[0].content + assert.equal(assistantContent[0].name, "TodoWrite") + assert.equal(assistantContent[1].name, "backgroundOutput") + assert.equal(assistantContent[2].name, "search") + }) + it("transformResponseStream passes error responses through without SSE parsing", async () => { const errorBody = JSON.stringify({ type: "error", diff --git a/src/transforms.ts b/src/transforms.ts index 466009d..bd1b6b0 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -208,8 +208,12 @@ export function transformBody( // Anthropic's OAuth billing validation rejects tool names with the // mcp_ prefix when multiple tools are present. Skip prefixing and - // rename the blocked bare name "todowrite" → "TodoWrite" instead. - const BLOCKED_TOOL_NAMES: Record = { "todowrite": "TodoWrite" } + // rename only the specific tool names that the validator blocks. + const BLOCKED_TOOL_NAMES: Record = { + "todowrite": "TodoWrite", + "background_output": "backgroundOutput", + "background_cancel": "backgroundCancel", + } if (Array.isArray(parsed.tools)) { parsed.tools = parsed.tools.map((tool) => { if (typeof tool.name === "string" && BLOCKED_TOOL_NAMES[tool.name]) { @@ -259,6 +263,8 @@ export function stripToolPrefix(text: string): string { return text .replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"') .replace(/"name"\s*:\s*"TodoWrite"/g, '"name": "todowrite"') + .replace(/"name"\s*:\s*"backgroundOutput"/g, '"name": "background_output"') + .replace(/"name"\s*:\s*"backgroundCancel"/g, '"name": "background_cancel"') } export function transformResponseStream(response: Response): Response { From b69e0849c9f8996f6bc21f72e65a2406f05c0306 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Apr 2026 11:05:05 -0500 Subject: [PATCH 3/3] fix: remove unused TOOL_PREFIX constant to pass lint --- src/transforms.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/transforms.ts b/src/transforms.ts index bd1b6b0..91e9fad 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -1,8 +1,6 @@ import { buildBillingHeaderValue } from "./signing.ts" import { config, getModelOverride } from "./model-config.ts" -const TOOL_PREFIX = "mcp_" - const SYSTEM_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude."