Skip to content

Commit 9121ca4

Browse files
SeaL773claude
andauthored
fix: PascalCase tool names after mcp_ prefix to match Claude Code convention (#191)
## Summary - PascalCase tool names after `mcp_` prefix (e.g. `bash` → `mcp_Bash`, `read` → `mcp_Read`) to match Claude Code's actual naming convention - Lowercase `mcp_` names (e.g. `mcp_bash`) are flagged as non-Claude-Code clients by Anthropic's billing validator, causing spurious 400 "out of extra usage" errors - Reverse mapping in `stripToolPrefix` restores original tool names by lowercasing the first char after stripping `mcp_` - Add explicit `baseURL` to ensure correct API endpoint construction ## Context Anthropic's billing validator rejects requests with lowercase `mcp_`-prefixed tool names when multiple tools are present. Binary search testing with isolated curl requests confirmed specific tool names like `background_output` trigger 400 errors, while PascalCase equivalents (`mcp_Background_output`) pass. Approach aligned with ex-machina-co/opencode-anthropic-auth#81. Fixes #190 #188 ## Test plan - [x] All 39 tests pass - [x] Verify Claude Max users no longer receive spurious "out of extra usage" errors - [x] Verify all tools remain functional with PascalCase mcp_ prefix - [x] Verify responses correctly restore original tool names 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: SeaL773 <SeaL773@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 66968d0 commit 9121ca4

3 files changed

Lines changed: 74 additions & 9 deletions

File tree

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ const plugin: Plugin = async () => {
260260

261261
return {
262262
apiKey: "",
263+
baseURL: "https://api.anthropic.com/v1",
263264
async fetch(input: RequestInfo | URL, init?: RequestInit) {
264265
const latest = getCachedCredentials()
265266
if (!latest) {

src/transforms.test.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "./transforms.ts"
99

1010
describe("transforms", () => {
11-
it("transformBody moves non-core system text to user message and prefixes tool names", () => {
11+
it("transformBody moves non-core system text to user message and PascalCase-prefixes tool names", () => {
1212
const input = JSON.stringify({
1313
system: [{ type: "text", text: "OpenCode and opencode" }],
1414
tools: [{ name: "search" }],
@@ -36,8 +36,8 @@ describe("transforms", () => {
3636
// The original system text should now be prepended to the first user message
3737
assert.equal(parsed.messages[0].content[0].type, "text")
3838
assert.equal(parsed.messages[0].content[0].text, "OpenCode and opencode")
39-
assert.equal(parsed.tools[0].name, "mcp_search")
40-
assert.equal(parsed.messages[0].content[1].name, "mcp_lookup")
39+
assert.equal(parsed.tools[0].name, "mcp_Search")
40+
assert.equal(parsed.messages[0].content[1].name, "mcp_Lookup")
4141
})
4242

4343
it("transformBody relocates non-core system text to user message", () => {
@@ -449,6 +449,51 @@ describe("transforms", () => {
449449
assert.equal(parsed.thinking, undefined)
450450
})
451451

452+
it("transformBody PascalCase-prefixes tool names with mcp_", () => {
453+
const input = JSON.stringify({
454+
system: [],
455+
tools: [
456+
{ name: "bash" },
457+
{ name: "read" },
458+
{ name: "background_output" },
459+
],
460+
messages: [
461+
{
462+
role: "user",
463+
content: [
464+
{ type: "tool_use", name: "bash" },
465+
{ type: "tool_use", name: "background_output" },
466+
],
467+
},
468+
],
469+
})
470+
471+
const output = transformBody(input)
472+
const parsed = JSON.parse(output as string) as {
473+
tools: Array<{ name: string }>
474+
messages: Array<{
475+
content: Array<{ type: string; name?: string }>
476+
}>
477+
}
478+
479+
assert.equal(parsed.tools[0].name, "mcp_Bash")
480+
assert.equal(parsed.tools[1].name, "mcp_Read")
481+
assert.equal(parsed.tools[2].name, "mcp_Background_output")
482+
assert.equal(parsed.messages[0].content[0].name, "mcp_Bash")
483+
assert.equal(parsed.messages[0].content[1].name, "mcp_Background_output")
484+
})
485+
486+
it("stripToolPrefix reverses PascalCase mcp_ prefix", () => {
487+
assert.equal(
488+
stripToolPrefix('{"name": "mcp_Bash"}'),
489+
'{"name": "bash"}',
490+
)
491+
assert.equal(
492+
stripToolPrefix('{"name": "mcp_Background_output"}'),
493+
'{"name": "background_output"}',
494+
)
495+
})
496+
452497
it("stripToolPrefix removes mcp_ from response payload names", () => {
453498
const input = '{"name":"mcp_search","type":"tool_use"}'
454499
assert.equal(stripToolPrefix(input), '{"name": "search","type":"tool_use"}')

src/transforms.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ import { config, getModelOverride } from "./model-config.ts"
33

44
const TOOL_PREFIX = "mcp_"
55

6+
/**
7+
* Prefix a tool name with TOOL_PREFIX and uppercase the first character.
8+
* Claude Code uses PascalCase tool names (e.g. mcp_Bash, mcp_Read);
9+
* lowercase names (mcp_bash, mcp_read) are flagged as non-Claude-Code clients.
10+
*/
11+
function prefixName(name: string): string {
12+
return `${TOOL_PREFIX}${name.charAt(0).toUpperCase()}${name.slice(1)}`
13+
}
14+
15+
/**
16+
* Reverse prefixName: strip TOOL_PREFIX and restore the original leading case.
17+
*/
18+
function unprefixName(name: string): string {
19+
return `${name.charAt(0).toLowerCase()}${name.slice(1)}`
20+
}
21+
622
const SYSTEM_IDENTITY =
723
"You are Claude Code, Anthropic's official CLI for Claude."
824

@@ -206,10 +222,13 @@ export function transformBody(
206222
}
207223
}
208224

225+
// Anthropic's OAuth billing validation rejects lowercase tool names
226+
// when multiple tools are present. Claude Code uses PascalCase after
227+
// the mcp_ prefix (e.g. mcp_Bash, mcp_Read). Apply the same convention.
209228
if (Array.isArray(parsed.tools)) {
210229
parsed.tools = parsed.tools.map((tool) => ({
211230
...tool,
212-
name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name,
231+
name: tool.name ? prefixName(tool.name) : tool.name,
213232
}))
214233
}
215234

@@ -226,10 +245,7 @@ export function transformBody(
226245
return block
227246
}
228247

229-
return {
230-
...block,
231-
name: `${TOOL_PREFIX}${block.name}`,
232-
}
248+
return { ...block, name: prefixName(block.name) }
233249
}),
234250
}
235251
})
@@ -246,7 +262,10 @@ export function transformBody(
246262
}
247263

248264
export function stripToolPrefix(text: string): string {
249-
return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"')
265+
return text.replace(
266+
/"name"\s*:\s*"mcp_([^"]+)"/g,
267+
(_match, name: string) => `"name": "${unprefixName(name)}"`,
268+
)
250269
}
251270

252271
export function transformResponseStream(response: Response): Response {

0 commit comments

Comments
 (0)