Notes

A tool list may gain, never lose

juudd never removes or renames a tool, never makes an optional argument required, and never changes what an argument means. Callers hold copies we cannot see.

The juudd tool list only grows. We never remove or rename a tool, never make an optional argument required, and never change what an argument means. A new capability that does not fit an existing tool becomes a new tool. The reason is one fact: every caller holds a copy of our interface that we cannot see.

That is the decision. The rest of this post is why a refresh does not make the copy safe to ignore, the five rules, and the incident behind each.

We cannot ask who is still calling

An assistant reads the tool list when its client connects and keeps what it read. Our endpoint is stateless: a fresh server per request, no session, no registry of who is connected. So there is no answer to "has everyone stopped calling this tool", and no way to tell a client that its copy has expired.

We did find a way to tell it to look again. hello sends the protocol's notifications/tools/list_changed before it answers, and a client we measured came back for the list in the same millisecond. Three tools that had been missing from one connector for days appeared inside a single call.

A refresh makes a stale copy recoverable. It does not make it impossible. Four gaps remain:

  • a client that was offline when a change landed and calls before its next hello
  • a call already in flight against the schema the caller held a moment ago
  • a client that ignores the notification, because only one client's behaviour was measured
  • a client that calls nothing at all, and so learns nothing

In each of those the caller is acting on an older version. That is the permanent condition the rules are written for.

Five rules, each earned by something that happened

Never remove or rename a tool. A removed tool is still in somebody's cached list and will still be called. A tool that must stop being useful stops doing anything harmful and says so in its result. It does not vanish.

Never make an optional argument required. The safe direction is the other one, and deploy_site is the worked example: its subdomain went from required to optional. A client holding the old schema keeps sending it and still works. Reverse that, and every client holding the old schema breaks at once with a validation error about its own message rather than about the change.

Never change what an argument means. This is worse than removing it. The caller sends the value the old meaning asked for, the server acts on the new meaning, and both sides report success. If the meaning must change, it is a new argument, and the old one keeps its behaviour or refuses loudly.

A new capability that does not fit an existing tool is a new tool. Adding a tool degrades cleanly: a client that cannot see it never calls it, and nobody is misled. That is the mildest failure available, so "add a tool" beats "add a mode to an existing one" whenever the two are close.

But shipping a capability is not the same as shipping it. The three database tools were absent from one connector's cached list, so on the path the product is sold on, the headline capability did not exist. And a new argument of a new shape is not merely hidden, it is unusable: sending functions against a cached schema that did not declare it was rejected by the server's own validation, with expected array, received string at functions.

Descriptions are cached; results are not. A description is read from a copy taken at connect time. A result is produced now. So anything a caller must know in order to act correctly today belongs in the result, and the description carries only what stays true. deploy_site's description once said that publishing to a host was a separate step and not configured yet. That sentence had been false for a day and six deploys, and it was read as fact the whole time.

A refusal has to stand on its own

The last rule has a corollary we lean on everywhere: a refusal explains itself in the reply, names what would make the call succeed, and does not assume the caller can see the current schema.

When deploy_site is given a subdomain that disagrees with the hostname the platform composes, it does not just say no. It says which hostname the site is served at, tells the caller to omit the argument, and says that serving it at a domain you own is what claim_domain is for. A caller on an old schema, or one that never read the description, can still act on that.

What this costs

The rules bind descriptions as much as schemas. Every tool declares its own blast radius, and the descriptions are the product, so an annotation that changes meaning breaks rule three in a different costume.

The count is a surface of its own. How many tools there are appears in the setup document, the READMEs, the landing page's navigation, footer and tools page, and the panel. Adding a tool edits all of them. Adding one is cheap in code and not cheap in prose.

What is not decided

  • A change that genuinely cannot be additive is not ruled out. It is not an ordinary edit, and it owes its own written record: who breaks, how they find out, and what they are told when they do. There has not been one yet.
  • Only one client's response to the refresh notification was measured. The rules assume nothing about the others.

The rule, once

A tool surface may gain, never lose, and never change meaning, because the callers hold copies we cannot enumerate, expire or ask. Anything a caller must know today goes in the result. The tools and their limits are listed in the docs.