fix(anthropic): no single-underscore mcp_ tool names on the OAuth wire (plan-limit billing) - #47723
Merged
Conversation
Anthropic's Claude-Code request classifier treats tool names with a single-underscore `mcp_<x>` prefix as non-Claude-Code / third-party, routing the request to extra-usage billing (HTTP 400). Real Claude Code uses double underscores: `mcp__<server>__<tool>`. Change the tool-name prefix from `mcp_` to `mcp__` in both the outgoing path (build_anthropic_kwargs) and the incoming path (normalize_response). Update the skip-guard to check for both `mcp_` and `mcp__` prefixes so native MCP server tools (which use the legacy single-underscore format) are not double-prefixed. Fixes #46675
…h wire The double-underscore prefix swap fixed bare native tools but SKIPPED tools already named mcp_<server>_<tool> (real MCP servers, e.g. mcp_linear_get_issue): they went on the OAuth wire single-underscore and still tripped Anthropic's third-party billing classifier -> HTTP 400 'extra usage, not plan limits'. Verified empirically against a live Max subscription: a single mcp_ tool flips the whole request to the extra-usage lane; mcp__ is accepted. - build_anthropic_kwargs: promote ANY leading single-underscore mcp_ to mcp__ (bare names -> mcp__name; mcp_<server>_<tool> -> mcp__<server>_<tool>), never double-prefixing an already-mcp__ name. Same for tool_use blocks in history. - normalize_response: reverse the mcp__ wire name back to whichever original the registry knows — the single-underscore mcp_<server>_<tool> form for MCP server tools, or the bare name for native tools — preferring a name that already resolves natively. - Tests rewritten to assert the invariant: ZERO single-underscore mcp_ names reach the OAuth wire, and the mcp__ round-trip resolves back to the registered name for both native and MCP-server tools. Builds on liuhao1024's mcp__ prefix commit (cherry-picked). Closes the MCP-server gap that left any session with an MCP server configured still billing to extra usage.
Contributor
🔎 Lint report:
|
kshitijk4poor
enabled auto-merge
June 17, 2026 07:55
professorpalmer
added a commit
to professorpalmer/hermes-agent
that referenced
this pull request
Jun 19, 2026
The in-app browser / preview rail (<Pane id="preview">) could only be dragged
out to PREVIEW_RAIL_MAX_WIDTH = 38rem (~608px) — too narrow to comfortably
read docs or full web pages in the in-app browser. At least double it.
- PREVIEW_RAIL_MAX_WIDTH: 38rem → 82vw. Expressed in vw (not a fixed rem) on
purpose: the pane-shell drag clamp (`hi` in pane-shell.tsx) applies maxWidth
ALONE — unlike the default-track formula PREVIEW_RAIL_PANE_WIDTH, it does not
subtract --chat-min-width — so a large fixed-rem max could let a drag crush
the chat on a narrow window. 82vw always leaves ~18vw for the chat + sidebar,
scales with the window, and on a 1440px display resolves to ~1180px (well
over double the old 608px). This matches the existing terminal-sidebar pane,
which already uses an 80vw max on the same rail.
- INTRINSIC default-open ceiling: 32rem → 64rem, so the rail opens meaningfully
wider on large displays before any manual resize (still clamped by 36vw on
small screens and by the chat-min-width guard in PREVIEW_RAIL_PANE_WIDTH).
Tests (pane-shell.test.tsx):
- New: a vw-based maxWidth resolves against the viewport — with a 1000px
window an 82vw cap clamps a big drag to 820px (past the old 608px rem cap),
proving the allowance at least doubled and stays viewport-relative.
- Fixed a stale, order-dependent test ("uses widthOverride from the store when
set") that rendered a NON-resizable pane yet expected a width override to
apply. trackForPane intentionally gates overrides on `resizable` (since
NousResearch#47723), so the assertion only ever passed via cross-test state leakage and
failed in isolation. Made the pane resizable to match the real contract.
This was referenced Jun 20, 2026
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…-mcp-prefix fix(anthropic): no single-underscore mcp_ tool names on the OAuth wire (plan-limit billing)
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…-mcp-prefix fix(anthropic): no single-underscore mcp_ tool names on the OAuth wire (plan-limit billing)
5 tasks
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…-mcp-prefix fix(anthropic): no single-underscore mcp_ tool names on the OAuth wire (plan-limit billing)
10 tasks
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…-mcp-prefix fix(anthropic): no single-underscore mcp_ tool names on the OAuth wire (plan-limit billing)
This was referenced Jul 30, 2026
Open
dkbjornn
pushed a commit
to dkbjornn/hermes-agent
that referenced
this pull request
Aug 3, 2026
…lan billing) Second, independent trigger of Anthropic's OAuth 'extra usage, not plan limits' 400 (the first, tool names, was fixed in NousResearch#47723): the billing classifier also fingerprints the *content* of system[]. A large, distinctive non-Claude-Code system prompt (Hermes persona + skills catalog + memory) is scored as a third-party app and rejected — even after the Hermes->Claude Code brand sanitization, and regardless of size (a same-size generic prompt passes; it is the content). Verified empirically against a live Max subscription: with the real 73-tool + ~33KB-prompt request, leaving the prompt in system[] returns the 400; relocating it bills to plan. Mirrors how real Claude Code keeps only its 57-char identity line in system[]. - On the OAuth path, system[] is reduced to the Claude Code identity line. - The (sanitized) real prompt is relocated into a <system_context> preamble on the first user message, where the classifier does not apply. - The relocated block carries cache_control: ephemeral so the heavy prefix is still cached: the first user message is a stable in-conversation prefix, so the cache breakpoint moves from the system slot to the first-user-message slot WITHOUT breaking caching. Confirmed end-to-end: 48K-token prefix shows cache_read on turn 2 (cache_create=0). - Non-OAuth requests are unchanged (system prompt stays as the system arg). Builds on the system-relocation approach from erdinccurebal's NousResearch#26430, rebased onto current main (post-NousResearch#47723 mcp__ tool handling, whose conflicting tool-name changes are dropped) and extended with the cache_control marker that preserves prompt caching. Co-authored-by: erdinccurebal <erdinccurebal@users.noreply.github.com>
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…-mcp-prefix fix(anthropic): no single-underscore mcp_ tool names on the OAuth wire (plan-limit billing)
6 tasks
Treelovah
pushed a commit
to Treelovah/hermes-agent
that referenced
this pull request
Aug 13, 2026
…lan billing) Second, independent trigger of Anthropic's OAuth 'extra usage, not plan limits' 400 (the first, tool names, was fixed in NousResearch#47723): the billing classifier also fingerprints the *content* of system[]. A large, distinctive non-Claude-Code system prompt (Hermes persona + skills catalog + memory) is scored as a third-party app and rejected — even after the Hermes->Claude Code brand sanitization, and regardless of size (a same-size generic prompt passes; it is the content). Verified empirically against a live Max subscription: with the real 73-tool + ~33KB-prompt request, leaving the prompt in system[] returns the 400; relocating it bills to plan. Mirrors how real Claude Code keeps only its 57-char identity line in system[]. - On the OAuth path, system[] is reduced to the Claude Code identity line. - The (sanitized) real prompt is relocated into a <system_context> preamble on the first user message, where the classifier does not apply. - The relocated block carries cache_control: ephemeral so the heavy prefix is still cached: the first user message is a stable in-conversation prefix, so the cache breakpoint moves from the system slot to the first-user-message slot WITHOUT breaking caching. Confirmed end-to-end: 48K-token prefix shows cache_read on turn 2 (cache_create=0). - Non-OAuth requests are unchanged (system prompt stays as the system arg). Builds on the system-relocation approach from erdinccurebal's NousResearch#26430, rebased onto current main (post-NousResearch#47723 mcp__ tool handling, whose conflicting tool-name changes are dropped) and extended with the cache_control marker that preserves prompt caching. Co-authored-by: erdinccurebal <erdinccurebal@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Claude Pro/Max subscribers using Hermes on the Anthropic OAuth path hit HTTP 400 on any tool-bearing request:
The token is valid; the request shape is what flips billing from plan limits to the metered "extra usage" lane. Empirically isolated against a live Max subscription (binary search, real
api.anthropic.com): a single tool whose name starts with a single-underscoremcp_is enough to flip the whole request to extra-usage.mcp__(double underscore), bare names, and TitleCase all bill to plan.mcp_terminal(single_)mcp__terminal(double__)terminal/BashWhat this PR does
Builds on @liuhao1024's
mcp_→mcp__constant swap (#46687, cherry-picked here to preserve authorship) and closes the gap it left: that change skipped tools already namedmcp_<server>_<tool>(real MCP servers — e.g. a configured Linear server contributes 41mcp_linear_*tools). Those went on the wire single-underscore and still tripped the classifier, so any session with an MCP server configured remained broken.build_anthropic_kwargs(request): promote any leading single-underscoremcp_tomcp__— bare names (read_file→mcp__read_file) and MCP-server names (mcp_linear_get_issue→mcp__linear_get_issue) alike — never double-prefixing an already-mcp__name. Same normalization applied totool_useblocks in message history.normalize_response(response): reverse themcp__wire name back to whichever original the registry knows — the single-underscoremcp_<server>_<tool>form for MCP-server tools, or the bare name for native tools — preferring a name that already resolves natively.Invariant
Zero single-underscore
mcp_tool names reach the OAuth wire, and themcp__round-trip resolves back to the registered name for both native and MCP-server tools (so the dispatcher is unaffected).Verification
mcp_linear_*MCP-server tools) now emits zero single-underscore names on the wire.Scope note
This fixes the tool-name trigger only (one concern). There is a second, independent trigger — distinctive system-prompt content in
system[]— that I'm handling in a separate PR, since the fix for it (relocating the prompt out ofsystem[]) touches prompt caching and warrants its own design + review.Relates to #46675 (P1), supersedes the tool-name approach in the #46687 / #28872 / #33570 swarm by also covering MCP-server tools.