hermes - ✅(Solved) Fix OAuth Anthropic + Pro/Max plan: mcp_ tool-name prefix triggers "out of extra usage" 400 on every tool-bearing request [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#28849Fetched 2026-05-20 04:01:30
View on GitHub
Comments
1
Participants
2
Timeline
8
Reactions
0
Author
Participants
Timeline (top)
labeled ×5cross-referenced ×2commented ×1

Root Cause

build_anthropic_kwargs in agent/anthropic_adapter.py:2100-2116 unconditionally prefixes every tool name with mcp_ on the OAuth path:

# 3. Prefix tool names with mcp_ (Claude Code convention)
if anthropic_tools:
    for tool in anthropic_tools:
        if "name" in tool:
            tool["name"] = _MCP_TOOL_PREFIX + tool["name"]

The comment is misleading. The official Claude Code CLI only adds mcp_ to tools that come from user-installed MCP servers (claude mcp add ...) — its built-in tools (Read, Write, Bash, etc.) ship with bare names. Anthropic's overage gate uses the mcp_ prefix as a signal that a tool is a user-installed MCP extension and charges it against the overage bucket, not the included subscription quota. On a subscription with overage disabled (the default for Max), every tool-bearing request is pre-flight-rejected — even when 95%+ of the 5h quota remains.

Fix Action

Fix / Workaround

Quick patch I'm running locally (works on opus-4-7 / sonnet-4-5/6 / haiku-4-5 with a Max OAuth token):

PR fix notes

PR #28872: fix(anthropic): remove mcp_ tool prefix on OAuth path that triggers overage gate

Description (problem / solution / changelog)

Summary

Remove the mcp_ tool name prefix that was applied to all tools on the Anthropic OAuth path. This prefix triggered Anthropic's overage gate for Pro/Max subscribers, causing every tool-bearing request to fail with HTTP 400 "out of extra usage".

Fixes #28849

Problem

build_anthropic_kwargs() unconditionally prefixed every tool name with mcp_ when is_oauth=True:

# Before (removed)
if is_oauth:
    for tool in anthropic_tools:
        tool["name"] = "mcp_" + tool["name"]  # e.g. "mcp_terminal"

Anthropic treats mcp_-prefixed tools as MCP server extensions, billing them against a separate "extra usage" bucket. Pro/Max subscribers who have extra usage disabled get an immediate HTTP 400 rejection — even when 95%+ of their included quota remains unused.

Root Cause

The prefix was modeled after Claude Code CLI's behavior, but Claude Code only prefixes tools from user-installed MCP servers — not its built-in tools like Read, Write, Bash. Hermes was incorrectly prefixing all tools including built-in ones (terminal, read_file, web_search, etc.).

Fix

Removed the mcp_ prefix logic entirely (steps 3 and 4 in the OAuth block). Tool names are now passed through unchanged.

Testing

  • 4 new tests in tests/agent/test_oauth_mcp_prefix_removed.py:
    • Tool names not prefixed on OAuth path
    • Tool names not prefixed on non-OAuth path
    • Tool names in message history not prefixed
    • No crash when tools=None
  • 163 total tests passed (4 new + 159 existing anthropic adapter + oauth guard tests)

Changed files

  • agent/anthropic_adapter.py (modified, +12/-18)
  • tests/agent/test_oauth_mcp_prefix_removed.py (added, +103/-0)

Code Example

# 3. Prefix tool names with mcp_ (Claude Code convention)
if anthropic_tools:
    for tool in anthropic_tools:
        if "name" in tool:
            tool["name"] = _MCP_TOOL_PREFIX + tool["name"]

---

import os, anthropic
TOKEN = os.environ["CLAUDE_CODE_OAUTH_TOKEN"]  # sk-ant-oat01-... from a Max subscriber
client = anthropic.Anthropic(
    auth_token=TOKEN,
    default_headers={
        "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
        "user-agent": "claude-cli/2.1.74 (external, cli)",
        "x-app": "cli",
    },
)
system = [{"type": "text", "text": "You are Claude Code, Anthropic's official CLI for Claude."}]
msg = [{"role": "user", "content": "ok"}]

def call(tool_name):
    return client.messages.create(
        model="claude-haiku-4-5", max_tokens=2048, system=system, messages=msg,
        tools=[{"name": tool_name, "description": "shell",
                "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}}}],
    )

call("terminal")       # ✓ 200
call("mcp_terminal")   # ✗ 400 "out of extra usage"

---

-        # 3. Prefix tool names with mcp_ (Claude Code convention)
-        if anthropic_tools:
-            for tool in anthropic_tools:
-                if "name" in tool:
-                    tool["name"] = _MCP_TOOL_PREFIX + tool["name"]
-
-        # 4. Prefix tool names in message history (tool_use and tool_result blocks)
-        for msg in anthropic_messages:
-            content = msg.get("content")
-            if isinstance(content, list):
-                for block in content:
-                    if isinstance(block, dict):
-                        if block.get("type") == "tool_use" and "name" in block:
-                            if not block["name"].startswith(_MCP_TOOL_PREFIX):
-                                block["name"] = _MCP_TOOL_PREFIX + block["name"]
-                        elif block.get("type") == "tool_result" and "tool_use_id" in block:
-                            pass  # tool_result uses ID, not name
RAW_BUFFERClick to expand / collapse

Every tool-bearing request from hermes-agent to the Anthropic API fails with HTTP 400 "You're out of extra usage. Add more at claude.ai/settings/usage and keep going." when the user is authenticated with a Claude Code OAuth token tied to a Pro/Max subscription (no usage-based billing enabled). Tool-free requests, and requests sent with the same token through the official Claude Code CLI, work fine.

Reproduces against agent/anthropic_adapter.py HEAD with a Max 5x subscription (anthropic-ratelimit-unified-5h-utilization was 0.08-0.18 across all tests — cota base nowhere near limit) and anthropic-ratelimit-unified-overage-disabled-reason: org_level_disabled_until (the default state for any Max subscriber who hasn't opted into pay-per-use overage).

Root cause

build_anthropic_kwargs in agent/anthropic_adapter.py:2100-2116 unconditionally prefixes every tool name with mcp_ on the OAuth path:

# 3. Prefix tool names with mcp_ (Claude Code convention)
if anthropic_tools:
    for tool in anthropic_tools:
        if "name" in tool:
            tool["name"] = _MCP_TOOL_PREFIX + tool["name"]

The comment is misleading. The official Claude Code CLI only adds mcp_ to tools that come from user-installed MCP servers (claude mcp add ...) — its built-in tools (Read, Write, Bash, etc.) ship with bare names. Anthropic's overage gate uses the mcp_ prefix as a signal that a tool is a user-installed MCP extension and charges it against the overage bucket, not the included subscription quota. On a subscription with overage disabled (the default for Max), every tool-bearing request is pre-flight-rejected — even when 95%+ of the 5h quota remains.

Minimal reproduction

Same model, same max_tokens, same schema, same OAuth token — only the tool name changes:

import os, anthropic
TOKEN = os.environ["CLAUDE_CODE_OAUTH_TOKEN"]  # sk-ant-oat01-... from a Max subscriber
client = anthropic.Anthropic(
    auth_token=TOKEN,
    default_headers={
        "anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
        "user-agent": "claude-cli/2.1.74 (external, cli)",
        "x-app": "cli",
    },
)
system = [{"type": "text", "text": "You are Claude Code, Anthropic's official CLI for Claude."}]
msg = [{"role": "user", "content": "ok"}]

def call(tool_name):
    return client.messages.create(
        model="claude-haiku-4-5", max_tokens=2048, system=system, messages=msg,
        tools=[{"name": tool_name, "description": "shell",
                "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}}}],
    )

call("terminal")       # ✓ 200
call("mcp_terminal")   # ✗ 400 "out of extra usage"

Suggested fix

Drop the prefix loop entirely, or make it opt-in only for tools that came from a real MCP server (the Hub/MCP-discovered ones), not for the built-in toolsets that hermes assembles itself. Hermes' built-in terminal, file, memory, etc. are not MCP-sourced and shouldn't be tagged as such.

Quick patch I'm running locally (works on opus-4-7 / sonnet-4-5/6 / haiku-4-5 with a Max OAuth token):

-        # 3. Prefix tool names with mcp_ (Claude Code convention)
-        if anthropic_tools:
-            for tool in anthropic_tools:
-                if "name" in tool:
-                    tool["name"] = _MCP_TOOL_PREFIX + tool["name"]
-
-        # 4. Prefix tool names in message history (tool_use and tool_result blocks)
-        for msg in anthropic_messages:
-            content = msg.get("content")
-            if isinstance(content, list):
-                for block in content:
-                    if isinstance(block, dict):
-                        if block.get("type") == "tool_use" and "name" in block:
-                            if not block["name"].startswith(_MCP_TOOL_PREFIX):
-                                block["name"] = _MCP_TOOL_PREFIX + block["name"]
-                        elif block.get("type") == "tool_result" and "tool_use_id" in block:
-                            pass  # tool_result uses ID, not name

A version-conditional fix would be even better: keep the existing behavior when the request is going to a non-Anthropic Claude-compat endpoint (some of those do expect mcp_ namespacing), and skip the prefix only when base_url is api.anthropic.com and is_oauth is true.

Side note (probably worth a separate issue)

Even after removing the mcp_ prefix, enabling the skills or session_search toolsets brings the 400 back. agent/prompt_builder.py:1180-1214 injects an <available_skills> catalog of all installed skills (84 in my case) plus a ~600-token "skills (mandatory)" instruction block into the system prompt every turn. The aggregate input is large enough that Anthropic's per-message overage check rejects it on Max. Cutting the catalog to skill names only (no descriptions) or gating it behind a config flag would fix that path too, but it's a distinct issue from the mcp_ one.

Environment

  • hermes-agent v0.14.0 (upstream 12c39830)
  • anthropic SDK 0.87.0
  • Python 3.11.15
  • Token: Claude Code OAuth setup token (sk-ant-oat01-...) under a Max 5x subscription, overage disabled at org level (default)
  • Affected models: opus-4-7, opus-4-6, sonnet-4-6, sonnet-4-5, haiku-4-5 (any model)

Happy to PR the fix if useful.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING