Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
67 changes: 64 additions & 3 deletions src/transforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }],
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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",
Expand Down
52 changes: 34 additions & 18 deletions src/transforms.ts
Original file line number Diff line number Diff line change
@@ -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."

Expand Down Expand Up @@ -206,30 +204,44 @@ export function transformBody(
}
}

// Anthropic's OAuth billing validation rejects tool names with the
// mcp_ prefix when multiple tools are present. Skip prefixing and
// rename only the specific tool names that the validator blocks.
const BLOCKED_TOOL_NAMES: Record<string, string> = {
"todowrite": "TodoWrite",
"background_output": "backgroundOutput",
"background_cancel": "backgroundCancel",
}
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
}),
}
})
Expand All @@ -246,7 +258,11 @@ 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"')
.replace(/"name"\s*:\s*"backgroundOutput"/g, '"name": "background_output"')
.replace(/"name"\s*:\s*"backgroundCancel"/g, '"name": "background_cancel"')
}

export function transformResponseStream(response: Response): Response {
Expand Down