Writing

The Fix for a Big Tool Catalog Isn't Fewer Tools

The Fix for a Big Tool Catalog Isn't Fewer Tools - abstract illustration

There's a piece of MCP server design advice going around that I keep seeing stated as settled: don't map tools to system APIs, because you end up with fifteen file tools. Define ten or twelve areas instead, each with an action parameter, and let the model dive into an area when it needs one. The MCP server I'm building has 123 tools across 20 prefixes, flat, and I didn't nest them. Here's the reasoning, including the part of the advice that's correct.

The half that's right

Mechanically mapping a system API surface to tools does produce bad catalogs. You get near-duplicate tools whose descriptions don't distinguish them, an ordering that reflects your implementation rather than any task a model would perform, and no namespace at all, so list and get and search sit next to each other with no indication of what they operate on. That's a real failure mode, I've shipped it, and the diagnosis is fair.

Where I part company is the prescription. Collapsing to file(action: "delete") fixes the symptom by discarding two things that are load-bearing, and it fixes it at a layer where the problem had a cheaper solution.

The schema goes soft

A tool's input schema is the only contract the model gets. Fifteen file tools means fifteen tight schemas, each with exactly the parameters that operation takes, each parameter documented for that operation specifically. One file tool with fifteen actions means either a permissive params: object blob or a oneOf large enough that most clients render it poorly and some flatten it entirely.

Either way you've moved the disambiguation problem. Tool selection is a place where the client has real machinery: descriptions, ranking, search, allowlists, and in most harnesses a retry when the model picks something that doesn't exist. Argument construction has none of that. The model writes action: "rename" as free text into a field whose valid values it half-remembers, with a params object whose required keys depend on which action it just wrote. Structured output exists precisely because free-form fields are where models drift, which I've written about in make your AI return data, not prose. Building a tool interface that reintroduces the drift is going the wrong direction.

The file-operations example is also the weakest case anyone could pick for this. Read, write, list, move, and delete have genuinely different argument shapes and wildly different blast radii. That's the set you least want behind a single signature.

The permission layer keys on the tool name

This is the one that actually decided it for me.

My server has 16 grant domains: filesystem, email, calendar, contacts, reminders, storage, smtp, http, scheduler, remote command execution, and so on. An API key doesn't get "access to the server." It gets a domain, a set of operations within it drawn from a fixed vocabulary (list, read, write, delete, presign, send, execute), and a resource selector narrowing it further, one storage bucket or one mail folder. I built that layer before there was anything worth protecting, for reasons I laid out in build the authorization model before you have anything to protect.

Every one of those checks resolves from the tool name. Filesystem_readTextFile maps to filesystem plus read. Filesystem_deleteFile maps to filesystem plus delete. A key scoped to read-only filesystem access reaches the first and is refused the second, and the refusal happens in the dispatch path before any argument is parsed.

Now collapse both into file. What domain and operation does file map to? It doesn't have one. The answer is inside the arguments, which means authorization has to parse the payload and branch on a string the caller supplied, in a code path that previously couldn't be wrong because it was matching a name the server itself registered. Every scoped key becomes a key to a tool that can do anything the tool can do, unless the argument inspection is exhaustive and stays exhaustive as actions get added.

The same collapse takes out the annotations. MCP carries readOnlyHint and destructiveHint per tool, and clients use them to decide what runs without asking. A file tool is destructive if any action is, so every list call now prompts, or you mark it read-only and a delete slips through unprompted. My build has a table that requires an explicit annotation entry for every tool name, enforced by a test that fails the build when a new tool shows up without one. That table has nowhere to live in an action-dispatch design.

And it takes out the client-side controls that sit above my server entirely. Harness allowlists, audit logs, per-agent tool lists like the ones I described in building a production MCP server: all of them key on tool name. Collapsing the catalog makes every one of those coarser at the same time.

The context argument aged out

The strongest version of the nesting argument was always about context: 123 schemas is a lot of tokens to spend on a session that only needs two of them. That was true, and it's largely stopped being true.

Clients now load tool names eagerly and schemas on demand. The session I'm writing this in has roughly 250 tools available across a handful of servers, and what's actually in context is a name list. When I need one, the client fetches its schema and it becomes callable. That's the exact benefit the nesting advice promises, avoid the clutter of an area you aren't using, delivered at the client layer without softening a single schema or breaking a single permission check.

Which reframes the whole pattern. Action dispatch is a workaround for clients that load every schema up front. That was the universal case a year ago. It isn't now, and designing a server's permanent tool surface around a client limitation that's actively being removed is the wrong trade.

Prefixes give you the areas without the collapse

The useful part of "define ten or twelve areas" is the areas. You don't need to fold the schemas to get them.

Filesystem_, EmailStore_, Calendar_, Storage_, Remote_. Twenty prefixes over 123 tools, and the distribution is as lopsided as it always is: email carries 19, image generation carries 1. A model scanning that list sees the same hierarchy an action-dispatch design would give it. Tools sort into groups, an irrelevant group is visibly irrelevant from its prefix alone, and the prefix is a real namespace, so Storage_list and Filesystem_listFiles are never confusable. The grouping is free. It costs one naming convention and nothing else.

My actual failures are naming, not count

Here's where the catalog does hurt, and it isn't the number.

I have EmailStore_findAllMessageIdsByEmail, EmailStore_findAllMessageIdsByMultipleEmails, EmailStore_findAllCompatMessageIdsByMultipleEmails, and EmailStore_findAllEmailMetadatasByMultipleEmails. Four tools whose names differ by a pluralization, the word "Compat," and a return type. When a model picks the wrong one, that's why. Not because there were 123 options; because four of them were indistinguishable at a glance.

Nesting fixes none of that. Fold them under an email tool and you have four near-identical actions in an enum instead of four near-identical names in a list, with less schema to tell them apart. The operation set is the bug. Those four exist because they accreted against a Java service over two years, and the fix is to merge them behind one well-named tool with an optional parameter, which is a design decision about that specific tool, not an architectural pattern applied to the whole catalog.

The general lesson is smaller than "the single biggest architecture requirement for any MCP server." Name tools for the task, not the endpoint. Namespace them. Make sibling descriptions state what distinguishes them from each other, not just what each one does. Then let the count be whatever the work requires, and keep the layer that decides what a key is allowed to do reading a name it can trust.

Building something like this?

This is the kind of work I do for clients. Tell me what you're building and I'll give you a straight read on the approach.

Book a 30-minute call