How forgiving are your MCP tools?

Egor Kraev

The problem with tool design for LLMs

Designing an interface surface for your application is more an art than a science, especially when it’s meant to be used by LLMs. Too many tools, or too many arguments to a tool, and LLMs might get confused and mix them up (not to mention extra context being used up). Too few, and each tool and each arg end up carrying a lot of cognitive load, and the calling LLM better be smart enough to work with that.

One specific case I’ve been coming across a lot lately is agents having to pass structured inputs to tools, conforming to a pretty complex json schema. It could be a SLayer query, containing not just measures and dimensions, but query-time transforms, queries treating other queries as models, and so on.

Or it could be a chart, which contains both a query (that’s subject to certain constraints to make sure the returned data could be plotted) and a config to describe what the chart should look like.

Importantly, for such an input to be valid, it’s not enough for it to conform to a schema - for example, the model that we are trying to query must actually exist, maybe you want to impose a condition that the query actually returns at least one row, etc.

The natural way to build this from a Python backend is you design the input shape you want in Pydantic, then expose it as the in-arg schema in a tool; and provide other tools that allow the agent to query the valid model names, the dimensions and measures a model contains, etc.

Why returning an error is not enough

But then comes the fun part: the agent consumes the information, then calls the tool with an invalid input. Maybe it’s trying to reproduce patterns it’s seen in its training data that don’t really apply here, or maybe Anthropic is about to release a new model and so badly cripples the current one (too many people have commented on this on LinkedIn to blow this off as my overactive imagination).

How should your tool react to invalid inputs? The naive way is to return an error message back to the agent, hopefully with a detailed description of what went wrong. This may work, but has several drawbacks. Firstly, it pollutes the context of your main conversation with the back-and-forth around the error; secondly, it confuses the user who sees that something went wrong and may not be sure how serious the issue was; thirdly, your main agent will take a fair bit of time to re-process the error message (along with all the earlier conversation context); and most frustratingly, it may not have better luck next time, as whatever condition caused the first error likely still prevails. So after a few tries, burning tokens all the while, it will likely start trying to improvise alternative solutions, usually with strange results.

Making tools more forgiving

What are better ways? The first step is of course to make sure your schema is as simple and clear as possible - maybe it makes sense to use a simplified version of the full object schema rather than the full thing. Also, you need to make sure your tools broadcast the right schema to the MCP interface.

The next step may feel strange to someone used to API that (rightly) enforced a strict schema: make your tools more forgiving.

In SLayer, we tried to make our tools as forgiving as we can. That is, if the agent calls the query tool with an invalid config, but it’s one of the half a dozen failure modes where it’s unambiguously clear what the agent is trying to do (for example, it submits a valid dimension to the ‘fields’ slot instead of the ‘dimensions’ slot), we just silently change it to the valid counterpart (for example, move the dimension to the right slot in the query json).

A technical aspect worth mentioning is that the we broadcast the original (strict) schema for the MCP tool in-arg, but use a more lenient schema for the actual initial validation of the input, which is then deterministically coerced into the strict schema if the error is one of the common failure modes.

That’s made a big reduction in the number of queries intermittently failing due to the LLM’s flakiness.

The validation loop

A second layer of tolerance, if the above is not enough, is the good old validation loop. It’s really simple: use built-in LLM flags to enforce a given output schema, then run any other validation on the resulting object; return the result to the caller if successful, return the error to the LLM if not, and ask it to try again, up to a certain number of retries. A micro-agent if you like.

Compared to letting the main agent do this retry process, this has the advantage of being faster and cheaper (smaller context), as well as allowing you to inject any extra instructions into that loop’s context that are too detailed for the main one. Also, this allows you to choose an LLM that’s best at quick and cheap generation of a json with a given schema, independently of what your main agent uses, making it cheaper and faster still. In my experience, OpenAI models tend to be better at that than Anthropic models (they've also had “json mode” as an API flag for a lot longer).

This approach has the disadvantage of itself being stochastic and so prone to sometimes producing technically valid but unexpected results - but then your main agent can introspect the result and tweak the bits that are wrong, which is less hard than producing a complete valid config from scratch.

The moral of this story is that in my opinion, optimal behavior for an MCP tool, meant to be used by an agent, is often different from that of classic APIs. While the latter rightly impose a rigid contract on the output validity, MCP tools should often be more forgiving, and auto-correct agent sloppiness, at least in some cases.

Do you agree?