My MCP Server Had 39 Tools. I Had Designed It for the Wrong Caller.

Every time SWCraft gained a model operation, I added an MCP tool.
That is how I would extend a conventional software library: one explicit function for one explicit operation. It was also the wrong way to design an interface whose caller is an LLM.
The result was 39 MCP tools. After separating the software API from the agent interface, the same system needed 13.
SWCraft is an experimental AUTOSAR software architecture environment I am building. Its model lives in version-controlled YAML. Deterministic tools validate it and generate ARXML, diagrams and documentation, while an LLM agent reads and changes the same model through MCP.
The mistake was not that the internal library had too many functions. The mistake was exposing those functions almost one-for-one as the agent’s vocabulary.
The Design Looked Reasonable
The SWCraft domain library already contained functions for concrete model operations:
create_swc
add_port
add_runnable
create_interface
create_cs_interface
create_connector
update_resource
When the model gained support for client-server interfaces, the natural library change was another explicit function: create_cs_interface.
The MCP server followed the same pattern. Each useful library operation received a corresponding tool: create_swc became add_swc, create_interface became add_interface, and the new create_cs_interface became add_cs_interface. The names were specific, the arguments were typed, and each tool was easy to test in isolation.
From a conventional API perspective, this was clean design. Adding add_cs_interface did not change add_interface. Existing callers could keep invoking the old function. Nothing became syntactically incompatible.
But the MCP server did not have a conventional caller.
A Library Function Is Selected by Code
A developer calling a library chooses the function explicitly:
add_interface(...)
The compiler or runtime does not reconsider that choice because a neighbouring function was added later. An IDE may help the developer discover available functions, but the call site still names one of them directly.
This makes a growing library API manageable. A new function expands what future code can call without changing what existing code already calls.
An LLM-facing tool surface behaves differently. The user does not normally say:
Call
add_interfacewith these arguments.
The user says:
Create an interface called IDiagnostics.
The model must translate that intent into a tool choice. Before add_cs_interface existed, only one tool looked plausible. Afterwards, two did. The old tool had not changed, but the decision the model had to make had changed.
That is why adding an MCP tool can be a breaking change. Compatibility is not only about whether an old call still works. It is also about whether the same request now presents the model with a different set of plausible actions.
I Had Turned the Model Schema Into a Tool List
Once I looked at the 39-tool surface from the model’s side, the deeper problem became visible.
Many of the tools were not genuinely different capabilities. They were the shape of the architecture model expressed as functions:
model contains SWCs → add_swc
SWCs contain ports → add_port
SWCs contain runnables → add_runnable
model contains interfaces → add_interface / add_cs_interface
model contains connectors → add_connector
The MCP catalog was a schema in disguise.
That created a structural growth rule:
New model concept → new library function → new MCP tool → new choice for the LLM
The first part of that chain was reasonable. A deterministic domain library benefits from explicit operations. The mistake was assuming that the MCP layer should mirror them.
I had designed the tool surface around the implementation beneath it, not around the kind of decisions the agent should make.
My First Fix Managed the Symptom
My initial proposal was task-scoped tool visibility. The server would keep all 39 tools, while an orchestration layer exposed only the subset relevant to the current task.
That would have reduced ambiguity at each decision point. It may still be the right approach for systems whose tools represent genuinely different services and actions.
But in SWCraft it would have managed a catalog that did not need to exist. The more useful question was not:
How should I filter these 39 tools?
It was:
Why does the agent-facing interface have one tool per model operation?
The Domain API and the Agent Interface Became Separate
The redesign kept the fine-grained domain functions available to deterministic Python callers and replaced their one-to-one MCP projection with three generic tools:
discoverdescribes what the model may contain: layers, element kinds, fields, types, enums, units and constraints;readreturns what the model contains now, addressed through that contract;change_setis the only tool that writes. It accepts a baseline, a set of operations and an optional intent, applies them to a shadow copy, validates the result, and presents the exact delta for review before committing anything.
Ten specialized tools remained for capabilities that are not generic model access: validation, build, diff, impact analysis, resource reporting, compatibility checks and component exchange.
The two interfaces now have different shapes:
| Caller | Interface it sees | How a mutation reaches the model |
|---|---|---|
| Deterministic Python code | Fine-grained functions such as create_swc, add_port and update_resource | Calls the operation-specific writer API directly |
| LLM agent | discover, read and change_set | Sends every mutation through the editing protocol |
The MCP mutation path is:
change_set
→ apply operations to a typed staged model
→ write touched elements with write_element / remove_element
→ validate the complete staged model
→ show the exact delta for review
→ commit everything or nothing
The library remains explicit because deterministic code benefits from explicit functions. The MCP path uses a registry-driven editing protocol because the agent does not need a separate top-level decision for every kind of model element.
change_set does not simply dispatch each operation back to the old narrow functions. That would reproduce the same coupling and would require still more functions for nested updates. Instead, its operations modify a typed in-memory snapshot, and a generic write_element / remove_element pair writes every touched element to the shadow model before validation and review.
Why Capability Did Not Disappear
Removing 29 MCP tools did not remove the operations behind them.
Creating an SWC, adding a port or updating a resource became an addressed operation inside change_set. The request is checked against the contract, applied to the typed model, materialized through the generic writer, validated as one transaction and either committed in full or not committed at all.
The change was therefore not:
many capabilities → fewer capabilities
It was:
domain operations exposed as tools → domain operations expressed through one protocol
This also changed how the surface grows. A new field or model element now changes what discover returns. It does not automatically add another candidate to the tool list.
The Ambiguity Moved to a Better Boundary
The redesign did not make the agent incapable of making mistakes.
It no longer had to choose between neighbouring mutation tools, but it still had to construct the correct address and operation inside change_set. In early rehearsals, the agent attempted a whole-list replacement where a member insertion was required and used a member step where a map required a key step.
Those mistakes were different from selecting the wrong top-level capability. The protocol rejected them with a concrete diagnosis, and the model could retry without changing the architecture. The contract already described the correct collection form; a short clarification in the agent profile prevented the same slips in later runs.
This is the trade: fewer ambiguous top-level choices, more responsibility inside one explicit and validated protocol. For SWCraft, that is a better boundary because protocol errors can be checked deterministically.
What the Smaller Surface Changed in Practice
The early evidence is limited, but it points in the intended direction:
- Across three macOS rehearsals and one Windows run, the agent consistently used
discover→read→ checks → onechange_set, with no wrong top-level tool selection. - One prompt produced one reviewed change set with 23 operations: five removals and eighteen creates. Under the old surface, the eighteen creates would have required eighteen separate mutating tool calls.
- The model contract continued to evolve while the MCP surface remained at 13 tools. Model growth changed the information returned by
discover, not the number of mutation tools presented to the agent.
Four rehearsals are not a benchmark, and they do not prove that 13 is an optimal number. They establish something narrower: after the redesign, the failures I observed were addressable protocol errors rather than choices between overlapping mutation tools.
When This Pattern Does Not Apply
Not every MCP catalog is a schema in disguise.
A server may expose genuinely different capabilities: search a file system, create a ticket, run a build, send a message, deploy a service. Those actions may not belong behind one generic mutation protocol. In that case, task-scoped visibility or tool search can still reduce the decision surface presented to the model.
The test is not whether the catalog is large. It is whether its tools represent different user-level capabilities or merely different records and fields in the same domain model.
If adding a model type repeatedly forces you to add another MCP tool, the tool layer may be mirroring the library beneath it too closely.
The Rule I Would Use Now
Design the domain library for deterministic callers. Design the MCP surface for an agent that must choose its next action from natural-language intent.
Do not automatically expose every useful function as a tool. First ask whether it is a distinct agent capability or one operation inside a broader protocol.
In SWCraft, separating those two interfaces reduced 39 MCP tools to 13 without removing capability. More importantly, the tool surface stopped growing with the model schema.