●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Resolving Tool Name Collisions When Bundling Multiple MCP Servers in the Claude Agent SDK
When the GitHub MCP and Linear MCP both expose create_issue, Sonnet 4.6 cannot tell them apart. This article walks through the structure of MCP tool name collisions, a TypeScript reconciler implementation, and the production failure modes I hit running six sites at once.
The day I plugged the GitHub MCP and the Linear MCP into the same Claude Agent SDK app, Sonnet 4.6 received a user instruction to "open a ticket on Linear" and confidently created a GitHub issue instead. Both servers exposed a tool named create_issue, and the GitHub schema had silently overwritten the Linear one when the second server registered. I only found the cause after dumping the SDK's tool list to the debug log.
Running six sites on my own (Claude Lab, Gemini Lab, Antigravity Lab, Rork Lab, plus two more), I run into this kind of collision often. I have been shipping mobile apps as an indie developer since 2014 and have crossed 50 million cumulative downloads. The number of overlapping verbs across MCP servers feels roughly three times higher than what I dealt with in the pre-MCP API era. This article walks through the namespace design and a TypeScript reconciler I now use, including the failures I hit on the way.
How tool name collisions actually happen
The Claude Agent SDK hands off the tool list from each MCP server straight into the tools array of the Anthropic Messages API and leaves collision detection to the caller. The API itself only allows alphanumerics, underscores, and hyphens in name. If the same name appears twice, the SDK implementations I've inspected push them through a Map that ends up keeping the last entry, which makes the earlier server's tool vanish silently.
From the model's perspective, a tool is a triple of name, description, and input_schema. Two tools with the same name get collapsed before the request leaves your process, so the model never sees the duplicate. If the descriptions differ, the model is choosing based on whichever description survived the merge. That mismatch is exactly what produced the GitHub-vs-Linear mistake above.
Typical collision-prone verbs include:
Verb
Likely MCP servers
create_issue
GitHub MCP / Linear MCP / Jira MCP
search
Notion MCP / Slack MCP / Web Search MCP
upload
S3 MCP / Cloudflare R2 MCP / Stripe Files MCP
send_message
Slack MCP / Discord MCP / SendGrid MCP
Across my Claude Lab, Gemini Lab, Antigravity Lab, Rork Lab, and the two non-Lab sites, having three or more of these servers alive in a single agent session is the default rather than the exception. A workflow that pulls AdMob revenue numbers into Slack, opens a fix PR on GitHub, and closes the related Linear ticket walks straight into this minefield.
Comparing three namespacing strategies
There are several ways to resolve collisions. After running a few in production I kept the "prefix with server identifier" approach. Here is how the three candidates compare.
The first candidate is dot notation. Names like github.create_issue read well to humans, but the Anthropic Messages API rejects . in name and you have to escape or substitute it. That forces you to maintain a bidirectional mapping back to the original name on every dispatch.
The second is slash notation, which runs into the same character restriction and the same escape cost.
The third is double underscore separation: github__create_issue, linear__create_issue. It uses only API-legal characters and the model parses it as "the GitHub side's create_issue" without prompting. I ran the same prompt 50 times each across Sonnet 4.6 and Haiku 4.5 with temperature: 0.0, comparing strategies. Double underscore had the lowest misfire rate and beat a 2-character prefix like gh_ by about 7 points. Short prefixes also get confused with other meanings ("gh" vs "ghp"), so I do not recommend them.
I also keep the following rules around prefixing:
Prefixes are lowercase ASCII, between 3 and 10 characters
Original tool names stay untouched; the prefix is additive
Prefixes are applied only when there is an actual collision; never blanket-prefix everything
Rule 3 matters. Renaming every tool unconditionally hides the "idiomatic" names the model already knows (web_search, for instance) and degrades selection accuracy. Limiting prefixes to the servers that actually collide is the most model-friendly setting.
It is worth saying a word about the benchmark setup. Fifty samples is just enough to see the differences, but only if you pin the temperature to zero and replay the exact same system prompt and user message. I also kept the working set close to production, with four AdMob-related MCP servers, two GitHub-side servers, and one Slack server in the bundle — about 60 tools in total. Toy benchmarks with five tools always look rosier than reality.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Observed misfire patterns when GitHub MCP and Linear MCP both ship create_issue, and whether the model relies more on name or description
✦A TypeScript Reconciler that prefixes only colliding tool names with double underscores and returns a bidirectional alias map
✦An MCPManager design for hot-swapping servers without invalidating prompt caches or stranding the model on stale tool names
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The reconciler takes server tool bundles, detects collisions, renames them, and returns a bidirectional alias map.
// reconciler.tsimport type { Tool } from "@anthropic-ai/sdk/resources/messages.mjs";export interface ServerToolBundle { serverId: string; // "github" | "linear" | ... tools: Tool[];}export interface ReconciledTools { tools: Tool[]; forward: Map<string, { serverId: string; original: string }>; reverse: Map<string, string>;}const PREFIX_SEP = "__";const NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;export function reconcile(bundles: ServerToolBundle[]): ReconciledTools { const seen = new Map<string, string[]>(); for (const b of bundles) { for (const t of b.tools) { const list = seen.get(t.name) ?? []; list.push(b.serverId); seen.set(t.name, list); } } const conflicted = new Set( [...seen.entries()].filter(([, ids]) => ids.length > 1).map(([n]) => n), ); const tools: Tool[] = []; const forward = new Map<string, { serverId: string; original: string }>(); const reverse = new Map<string, string>(); for (const b of bundles) { for (const t of b.tools) { const needsPrefix = conflicted.has(t.name); const renamed = needsPrefix ? `${b.serverId}${PREFIX_SEP}${t.name}` : t.name; if (!NAME_RE.test(renamed)) { throw new Error(`rename produced invalid tool name: ${renamed}`); } tools.push({ ...t, name: renamed }); forward.set(renamed, { serverId: b.serverId, original: t.name }); reverse.set(`${b.serverId}:${t.name}`, renamed); } } return { tools, forward, reverse };}
Three things to flag in the code. Only colliding tools get prefixed. The forward and reverse maps let you resolve github__create_issue back to "GitHub server, original name create_issue" in O(1). And the regex check raises during build if any rename produces a name that the Messages API would reject.
The dispatcher side that runs after the model returns a tool_use block looks like this.
// dispatcher.tsimport type { ToolUseBlock } from "@anthropic-ai/sdk/resources/messages.mjs";import { reconcile, type ServerToolBundle } from "./reconciler.js";export class MCPDispatcher { private forward: Map<string, { serverId: string; original: string }>; constructor(private servers: Map<string, MCPClient>, bundles: ServerToolBundle[]) { const r = reconcile(bundles); this.forward = r.forward; } async dispatch(block: ToolUseBlock): Promise<unknown> { const meta = this.forward.get(block.name); if (!meta) { return { isError: true, content: `unknown tool: ${block.name}. available: ${[...this.forward.keys()].join(", ")}` }; } const server = this.servers.get(meta.serverId); if (!server) { return { isError: true, content: `server ${meta.serverId} not registered` }; } return server.callTool(meta.original, block.input as Record<string, unknown>); }}
One word on error handling. Throwing on unknown tool will tear down your agent loop. I made that mistake in the first revision and lost a scheduled task slot to it. The current version returns an error tool_result to the model with the list of available tools, and Sonnet 4.6 almost always corrects itself on the next turn. The wording matters — including "these tools exist: github__create_issue, linear__create_issue, ..." gives the model a runway to land on the correct name.
Normalizing descriptions with provenance tags
Renaming alone is not enough. In my measurements, when two tools share an identical description after prefixing (both literally say "Create a new issue"), the model still treats the prefix as a weak signal and picks one consistently regardless of intent. To strengthen the signal I tag descriptions by origin.
Simple, but effective. The misfire rate in my setup dropped from 38% to 6% just by adding a bracketed tag. Square brackets matter — the model reads them as an auxiliary label rather than part of the prose. For tools with long descriptions, I keep a newline after the tag so the body stays readable.
I also learned that beefier descriptions help when names collide. [GitHub] Create a new issue is too terse; [GitHub] Create a new issue in a repository. Use this for code-related bug reports, feature requests, and pull-request discussions on the GitHub side. cuts misfires by an additional 3 points. Counter-intuitive perhaps — I usually trim descriptions — but for colliding names a thicker description pays for itself.
Rebuilding when MCP servers come and go
Real Agent SDK apps see servers come and go. The user connects Slack, disconnects Discord, swaps in a new Notion workspace. Every time the tool list changes, the Anthropic prompt cache invalidates the corresponding breakpoint and the cost picture shifts. On one AdMob automation that bundled four MCP servers, I quietly lost about 12% of my monthly cache benefit by failing to manage this transition.
The fix is to wrap server registration in an MCPManager that owns the rebuild.
Subscribe to onChange and you can re-plan your cache breakpoints, or stage the new tool list to take effect from the next turn of an in-flight agent session. The non-negotiable rule is to never swap the tool list mid-turn. If the model picked github__create_issue in the previous turn and the name has disappeared in the next, the API returns a 400 and the loop spins on retries. Switch at turn boundaries only.
Three production failure modes
After running this in production for a while, I want to share three specific failures.
Stale alias. GitHub MCP renamed create_issue to issues_create in an update. The reconciler picked up the new name fine, but the prompt cache still held the old system prompt with the old tool list. The model called the no-longer-existing github__create_issue, the API returned 400, and the agent retried itself into a rate limit. Fix: include a hash of the reconciler output in the cache key, and rotate breakpoints explicitly when the hash changes.
Description drift. Two tools sharing a name can have their descriptions evolve independently on each upstream side. Even after tagging by provenance, the prose underneath drifts and the model's preference flips one day for no obvious local reason. I now dump the forward map's descriptions weekly to a Slack channel for eyeball review, scheduled in Cowork alongside my AdMob monthly numbers.
Schema collision. Two tools with the same name can have different input_schema (GitHub create_issue requires repo, Linear requires team_id). If your reconciler merges by name and ignores the schema, the model picks the wrong server's schema and the call fails on the other side. Prefixing colliding names already prevents this, but I added a safety: hash the input_schema and force a prefix whenever two same-named tools have different schema hashes, even if you would not otherwise consider them collided.
function schemaHash(t: Tool): string { return JSON.stringify(t.input_schema, Object.keys(t.input_schema).sort());}
Two smaller things worth noting alongside the big three. The first-turn misfire rate is consistently higher than later turns; I add one sentence to the system prompt ("Use the full tool name as written; github__create_issue and linear__create_issue are distinct.") and that gap closes. The other is that description length nudges the model's choice noticeably more than I expected — when names collide, expand the description rather than trim it.
Operations notes from running six sites
I started programming in 1997 at age 16, picking up languages by trial and error on dial-up forums where the only feedback came from strangers around the world. The MCP world has the same texture — someone publishes a server, you fold it into your agent, and you read tool names to infer their intent. Designing for collisions with strangers' servers feels like a basic courtesy in that world.
In production I switched from "site-based" server IDs (claudelab, gemilab, rorklab) to "capability-based" ones (admob, slack, github). When four sites pull from the same AdMob account, write to the same Slack workspace, and commit to the same GitHub organization, capability-based IDs let you reuse one server registration across all four. The Reconciler output stays more stable too, which means a higher prompt cache hit rate.
A subtler observation: more servers means slower responses. Anthropic's API takes noticeably longer with 60 tools in the array compared to 10, and the prompt cache hit rate shifts with every change to the tool list. After reorganizing my AdMob pipeline around capability-based bundles, daily aggregation per site dropped by 14% in latency on average. Over a month, that adds up.
The takeaway is that adding more MCP servers is not free. For each agent task I now ask "which tools does this actually need", and I bundle accordingly. AdMob aggregation does not need the GitHub MCP; a PR review does not need the Slack MCP. Task-specific bundles keep the reconciler output deterministic, which keeps the cache hot.
Next steps
If you want to add a reconciler to your project, start by calling list_tools on every server you currently bundle and grouping by name to visualize the actual collisions. You'll probably find more than you expected. Next, run the reconciler above end to end with prefixing turned on for the conflicting pairs only. Description normalization comes after that.
MCP is still in the phase where multiple vendors are publishing servers with their own naming traditions, and the variance will widen before it narrows. A reconciler layer turns server churn from a code change into a configuration change, which keeps your agent code stable and your shipping pace steady.
I share more half-formed notes from this work over on my stand.fm channel when I have something worth saying. Thanks for reading this far.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.