Ken Muse

Your MCP Data Is Not Guaranteed


Generative AI models are, by their nature, non-deterministic – they can produce different outputs despite identical inputs. It’s easy to assume that features like MCP can provide stronger guarantees. Unfortunately, that’s not always the case. For example, you have an MCP tool that returns a series of measurements as a table, but when the model generates the final report, the numbers are different. You’re then forced to debug to find out why the numbers have changed – is it rounding errors, some hidden calculation, or something else entirely?

There is another possibility that’s easy to overlook: perhaps the model changed the number when it generated its response. An MCP server can return exact, deterministic results, but it does not change the model’s core behavior.

The issue is similar to asking a model multiple times to replicate an image without any changes. Each regeneration creates another opportunity for the output to drift. Even with the original data in the context, a non-deterministic model can introduce deviations that alter the original data.

Image of me, degraded by replication
Figure 1. An image of me, degraded by 50 iterations of “exact” replication

That makes a lot of sense for images, but it may seem counterintuitive for a language model’s tools. After all, tools can run deterministic code. It’s natural to assume that would mean they avoid this problem.

An exact tool result produces an inexact answer

Model Context Protocol (MCP) is an open protocol for connecting large language model (LLM) applications with external data sources and tools. At its core, MCP uses JSON-RPC messages to invoke tools and return structured results. It provides a way to interact with a system to directly request and retrieve data. An MCP server can implement a deterministic tool that returns the same response for the same input. But that’s not the end of the process.

A host application (or harness) makes the request, receives the response, and then determines how to best manage and store the data. It then packages up that response and sends it as additional context – a tool call result – to the model. The model uses that additional context when it generates its response. It’s this last step that introduces the issue. Even if it receives the correct data, the model must still pay appropriate attention to it and use it accurately to build its response. The model is generating an output – not “thinking” in the classic sense. It may use the value correctly, interpret it, or replace it with a plausible alternative. In short, it can confidently state something different from what the tool provided.

For example, suppose you are receiving a unique identifier from one service for an account. The MCP server returns A1234. The model then needs to use that value to retrieve the next set of data from a different MCP tool. It now generates a tool call request, but it generates A12345 instead of A1234. The request is syntactically valid, but it is no longer the correct record. Suddenly, the system is retrieving unrelated data for a completely different account.

This can happen when the model orchestrates a complex process across numerous calls. With each step relying on the output of the previous one, a single misinterpretation or alteration can propagate errors throughout the entire workflow. Each dependent call creates another opportunity for an error, while a large context window, such as one that supports 1M tokens, can make the relevant value harder to retrieve reliably. Worst of all, the error may go unnoticed until it causes a significant issue.

Why a hook cannot guarantee it

This may sound like a job for a hook. Hooks are application-level features, not an MCP feature: a host such as Copilot executes them at defined points in an agent lifecycle to add context or enforce a local policy. As described in my earlier post about hooks, this makes a hook a particularly good way to run deterministic application logic.

Unfortunately, hooks can guarantee their own execution, but they cannot guarantee how the model later uses or preserves MCP data. A hook can inspect only the representation that its host and event expose. Depending on the implementation, that might include the full tool response or only limited metadata.

That becomes especially important for large results. A host might pass a small tool response directly between components. It might instead retain a large response outside the immediate context and use a URI, handle, summary, or another internal reference that it resolves later. This is a plausible implementation pattern, not a required MCP behavior. Unless the host documents the hook’s contract, a hook cannot assume that it will see the original content or the same representation that the model receives.

Even if you had that access, the real issue remains that the problem is not with the hook or the MCP data. The problem is what happens after the model receives that data. When the model decides the next step – or what to output to the user – it may change or replace any of those values.

In short, because the model is non-deterministic, it does not provide any guarantees about preserving or correctly using the data it receives. But as long as it works correctly most of the time, you might not even notice it.

Math creates a second chance to be wrong

Now imagine that the tool returns several durations:

400 ms, 375 ms, 425 ms, 390 ms

Then you ask the model for an average, a percentile, or a comparison with last week’s run. The model must first identify every input correctly and then perform the calculation correctly. Either step can fail. A response can be wrong because one measurement was changed in the model’s interpretation, because the model dropped a value, or because the arithmetic itself was incorrect.

A language model does not execute arithmetic like a deterministic calculator. It may generate the right answer from patterns in its training, but that does not provide the same guarantee as running code. Just like you rely on locally installed tools to interact with data or your file system, the model can rely on server-side tools to supplement its capabilities. It can use these tools to perform calculations and fetch data, but it can also ignore the tools and generate a result directly.

This provides an additional opportunity for mistakes to occur, even when the model correctly interprets the input values. Depending on the host, you may not know whether the model used a calculator or generated the answer directly. If you don’t realize that an error has occurred, you may end up making decisions based on incorrect calculations or analysis.

Put work where it belongs

Solving this is not about avoiding MCP or model calls. The answer is to carefully consider the reliability of each step and plan appropriately.

For deterministic operations, rely on code rather than the model. Let the code perform the analysis. If you’re not sure how to do that, use the model to help you write the scripts! Models often go through a lot of tuning to improve the quality of the code they write. This approach ensures the work remains reliable, verifiable, and deterministic. Instead of it hiding any math in the background, you make the calculations explicit and transparent.

If you need to interact with MCP data, keep the context focused and limited. Pass the data into deterministic layers for any processing you need. Collect only the necessary data so that extra noise does not fill the context. In some cases, it might even be valuable to have steps – or other models – perform intermediate validations to ensure that the data is not changing in unexpected ways. While model-based validation is not proof of correctness, it can help catch obvious errors before they propagate further.

For more complex scenarios, consider breaking the problem down into smaller, verifiable steps and having deterministic code persist the data to a known location. This can make it easier to verify, and it can enable you to perform analysis and calculations outside of the model’s context.

Defining a solution

As a concrete example, think about a report that has three tables. After each one you want the model to add its own analysis. You could provide the model with the data and request it to create the output, but this means that the data in the tables has some potential to be altered. A similar approach would be to provide it the markdown tables and have it return the complete analysis; however, this still relies on the model to recreate the tables accurately.

In both of these cases, you’re also round-tripping a lot of duplicate data, increasing the noise and the potential for errors. You send the data in your request, it then has to copy the data in its response. The context window is rapidly growing, with lots of duplicate values that consume attention. With each round trip, the chance for errors accumulates.

A more reliable approach would be to generate the tables, then ask the model to return structured data (such as YAML or JSON) with the name for each table and its analysis of the data. The model still has to interpret the data, but it is no longer tasked with re-creating it accurately in the response. Instead, it returns – or writes – just the analysis, leaving the original data intact and verifiable. You can then use a script to merge together the data and each analysis.

The main idea is to let code preserve and manage exact data and let the model handle interpretation.

Mastering the mayhem

MCP gives your assistant access to useful, current information. It does not guarantee the model will use those details or validate how accurately they are used. The LLM is responsible for what will ultimately be generated, and because it is non-deterministic, it makes no guarantees about what it does or how. As a result, you’ll want to carefully separate the responsibilities to try to minimize risk. That will give you more accurate and repeatable outcomes, while still giving you the full analytical strengths that AI models can provide.