A client talks to one server. A host juggles a shelf of them β and the moment you have two, you have a problem.
Project #2 connects to two servers, and both of them have a tool called
secret_code. The collision is deliberate: the cookie-jar server does a Caesar
shift, the toolbox server does Atbash.
The Claude API sees one flat list of tool names. So without help, one silently shadows the other and you get baffling results with no error anywhere.
Rewrite every name on the way out
Double underscore, because Claude tool names must match
^[a-zA-Z0-9_-]{1,128}$ β no dots, no slashes, no colons.
One sleeping server must not kill the host
The other thing that changes the moment there are two servers: one of them will be down.
const results = await Promise.allSettled(servers.map(connect));Promise.all gives you a toolbox that fails entirely if any single server is
unreachable. Promise.allSettled gives you a toolbox with fewer tools in it,
and a host that still works.
A host curates
Here is the part that matters more than either of the above, and it is the mildest version of an idea the next three projects sharpen into a thesis.
The toolbox is not a dumb pipe. It does not forward whatever a server advertises.
Project #3 connects to its own durable cookie jar and project #1's live
server. Project #1 also has a cookie_jar β the forgetful one β and the host
hides it, because two identical-looking jars would mean the model guessing
which one you meant on every request, and sometimes guessing wrong.
A host curates its toolbox. The host, not the server, decides what the model can reach.
Note that the secret_code collision is deliberately kept, because the
namespacing lesson is still true and there is an eval measuring whether the
model can tell the two ciphers apart. Curating means choosing, not minimising.
Try it
The collision is live in the app:
Encode 'meet me at dawn' with BOTH secret_code tools and compare them.
Two tools, two ciphers, one flat namespace that would have made this impossible to ask for.