I was building a task manager and wanted a warehouse supervisor to be able to chat with an agent and ask about tasks and assignments in plain language.
Picture a supervisor asking: “Show all overdue cycle counting tasks for warehouse aisle A12 and assign high priority ones to Rahul?” It sounds simple. But with hundreds of thousands of tasks in the system, a naive agent may have to fetch a large number of records, pass them through the model, filter them and total them up.
That is why we wanted to offload the work to an agent, and MCP felt like the natural way to connect it, expose the task system as tools and let the agent call them. Then I explored further and started questioning something I had taken for granted. Should the model really sit in the middle of all that data processing?
The question this post is built around, is MCP the problem, or is the way we use MCP the problem? That led me to a newer approach, where the agent writes code that calls the tools inside a sandbox and only the answer comes back to the model. Below is what I learned, including a side-by-side test on a stand-in for my task data.
The short answer
MCP is not a thing of the past, and I don’t think it was ever the real problem. Two different problems get tangled together:
Connectivity: How does an agent discover and call tools and data? This is what MCP standardizes, and it does the job well.
Orchestration: How does the agent decide what to call, how often, and how much data reaches the model? The classic loop answers with “show the model everything, call one tool at a time, and pass every raw result through its context”. That is where the cost lives.
Newer patterns keep MCP for the first problem and change the answer to the second. Tools become code the model calls from a sandbox, definitions load on demand, and results are filtered before they reach the model. Anthropic reports one workflow falling from about 150,000 tokens to 2,000, a 98.7% saving. In my own experiment, the model’s peak context fell from about 79,000 tokens to under 1,000 for the same task and the same answers.
Code mode and tool search change how the agent reaches MCP servers. Function calling skips MCP entirely, and skills sit alongside as instructions.
What MCP is, briefly
The Model Context Protocol, launched in November 2024, is an open standard for how agents discover and invoke external capabilities. You build one connection to a tool or data source and any compatible agent can use it. That is why it spread so quickly. Anthropic reported more than 10,000 active public servers and over 97 million monthly SDK downloads by December 2025, and MCP moved into the Linux Foundation’s Agentic AI Foundation on December 9, 2025.
It is also still being redesigned, which is not what a dying standard looks like. According to Cloudflare’s August 2026 write-up, the 2026-07-28 revision removes the initialize handshake and session ID so servers can run statelessly. It moves features such as MCP Apps and Tasks into extensions, and it deprecates Roots, Sampling, Logging and Dynamic Client Registration with at least 12 months’ notice.
Where the classic MCP loop hurts
The complaints about MCP are mostly not about the protocol. They are about the default way clients hand MCP tools to a model, every tool definition up front, every call as a model turn, every result through the model’s context. Three costs follow.
1. Tool definitions fill the context before any work starts
Most clients load all tool schemas into context at the start. Anthropic’s advanced tool use post says a five-server setup with 58 tools uses about 55,000 tokens before the conversation begins, and Jira alone takes about 17,000. Anthropic has seen definitions reach 134,000 tokens before optimization. Big catalogs also hurt accuracy. In Anthropic’s MCP evaluations, loading definitions on demand raised Opus 4 from 49% to 74%.
2. Every intermediate result passes through the model
In Anthropic’s code execution with MCP article, an agent downloads a meeting transcript from Google Drive and attaches it to a Salesforce lead. The full transcript goes through the model twice, once when read and once when written back out. For a two-hour meeting, the article estimates about 50,000 extra tokens. Very large documents can exceed the context limit and break the workflow, and copying big blobs between calls adds a risk of errors.
3. Every dependent call costs a model turn
A loop such as “check 20 employees, then filter” becomes 20 or more round trips if the model cannot batch. Each round trip re-sends the growing context, so the bill grows faster than the number of calls. Polling, retries and conditionals that plain code handles for free also go through the model.
For the warehouse assistant, the second and third costs were the worry. A broad question across thousands of tasks means fetching many records and then filtering them, which is work that code does better than a model reading raw rows. The next section puts numbers on all three costs for one realistic task.
My experiment - the same task, two ways
To find out, I built a small MCP server for a company expense system and ran one task through it two ways. The warehouse task manager is what started all this, but the stand-in has the same shape, many records, a fetch per record, then filter and aggregate. The server uses the official MCP Python SDK (version 2.3.0), and the client talks to it over the real protocol.
One catch first, Anthropic’s managed programmatic tool calling does not currently support tools from the MCP connector, so for the code-mode runs I bridge MCP to the sandbox myself. The managed version is covered later in this post.
The setup
The server has three tools:
list_employees()returns 20 employees.get_expenses(employee_id)returns about 60 line items per employee: date, category, merchant, amount, status and a free-text note.get_budget(employee_id)returns the employee’s quarterly travel budget.
The data is generated from a fixed seed and is deliberately messy, several quarters, rejected claims, non-travel categories. Together the raw tool results come to 275,002 characters per run, roughly 68,750 tokens by a chars-divided-by-four estimate.
The task: which employees exceeded their Q3 2026 travel budget, and by how much?
Travel spend means flights, lodging and ground transport, excluding rejected claims, between July 1 and September 30. Answering needs 41 MCP calls. One to list employees, then one expenses call and one budget call for each of the 20.
The four runs
A1, classic loop, one call per turn. Every tool call is its own model turn, and every raw result is appended to context.
A2, classic loop, parallel calls. Turn 1 lists employees, turn 2 fires all 40 remaining calls at once, turn 3 answers. This is the fair baseline for a model that batches well.
B1, code mode. The model sees the three tools as short function signatures, writes one script, and the script calls the real MCP server from a sandbox. Two model turns: write the script, read its output.
B2, code mode with a peek. Same as B1, but the model first runs a tiny script to inspect the shape of one employee’s data. Three model turns.
In the classic loop, all 275,002 characters of results flow back through the model. In code mode they stop in the sandbox, and only the printed list returns.
What is real and what is simulated
Real: the MCP server, the protocol traffic, every tool result, and the size of everything that would enter the model’s context.
Simulated: the model itself. Its decisions are scripted so the run is reproducible and needs no API key, and token counts are a rough estimate. Treat this as a measurement of data volume and round trips, not of live model behavior.
What the model sees in code mode
Instead of JSON schemas, the model gets signatures like this, generated from the server’s real tool list:
async def get_expenses(args: dict) # keys: employee_id: string
"""Get every expense line item for one employee. Returns a JSON array of
{line_id, date (YYYY-MM-DD), category, merchant, amount_usd, status
(approved|pending|rejected), notes}. Covers several quarters."""And this is the script it writes for the whole task:
import json, asyncio
from datetime import date
TRAVEL = {"Flights", "Lodging", "Ground Transport"}
START, END = date(2026, 7, 1), date(2026, 9, 30)
employees = json.loads(await list_employees({}))
async def analyse(e):
items, budget = await asyncio.gather(
get_expenses({"employee_id": e["id"]}),
get_budget({"employee_id": e["id"]}),
)
items, budget = json.loads(items), json.loads(budget)["quarter_travel_budget_usd"]
spend = round(sum(
x["amount_usd"] for x in items
if x["category"] in TRAVEL and x["status"] != "rejected"
and START <= date.fromisoformat(x["date"]) <= END
), 2)
return {"id": e["id"], "name": e["name"], "spend": spend,
"budget": budget, "over_by": round(spend - budget, 2)}
results = await asyncio.gather(*[analyse(e) for e in employees])
over = sorted((r for r in results if r["over_by"] > 0),
key=lambda r: r["over_by"], reverse=True)
print(json.dumps(over))The script makes the same 41 real MCP calls. The difference is that 275,002 characters of results stay in the sandbox, and the model only reads the final printed list.
Results
All four runs produced the same answer as a ground truth computed directly from the data: 6 of 20 employees over budget, led by Aditya Kapoor at $3,275.82 over. Token figures are estimates.
Peak context is the largest input the model saw in any single turn, so it shows how much raw data sat in context at once.
Reading the results
Context size is the biggest gap. Every raw result sits in the classic loop’s context, so it peaks roughly 80 to 105 times higher than code mode’s.
Turns matter most when the model cannot batch. A1 needs 42 model turns, and because each turn re-sends the growing context its cumulative input reaches 1.68 million tokens. A2 shows parallel tool calls fix the turn count, but its cumulative input is still about 78 times B1’s.
The backend sees the same load. All runs make 41 MCP calls (43 for B2’s extra peek). Code mode does not reduce calls to your systems. It removes the model from the loop between them.
Nothing changed in the backend. The only thing that changed was whether the model sat in the middle of the data-processing loop.
This does not mean code mode is 100 times cheaper or faster. It means that in this workload, roughly 80 to 105 times less data had to enter the model’s context. Cost and latency also depend on caching, how good the generated script is, and how many turns the model needs. The next section spells out the limits.
What the experiment does and does not show
It shows how much data reaches the model and how many round trips each pattern needs. It does not show how a live model behaves, and some choices flatter one side or the other.
Where it flatters the classic loop
Python does the final arithmetic exactly. A real model adding up about 1,200 line items from raw JSON in its context can make mistakes. In code mode the sum is computed by code.
The classic runs get a clean tool-by-tool sequence with no wasted turns.
Where it flatters code mode
B1 assumes the generated script works first try. B2 adds one exploratory turn, but a real run might also need a retry after a bug.
The script is fixed and written by me. A model might structure it differently or pull more data than needed.
Limits of the measurement
Token counts are estimates (characters divided by four). Both approaches are measured the same way, so the ratios are more trustworthy than the absolute numbers.
Prompt caching would lower the classic loop’s billed cost, since cached input is cheaper. It would not shrink the context window or remove the accuracy risk, so “cumulative input” is a token count, not a dollar figure.
Latency was not measured. In practice A1’s 42 turns are 42 sequential model calls.
Only three tools. With dozens of tools, their schemas add fixed context to every classic turn. Code mode, or tool search, can load definitions on demand instead.
One task, one dataset. A task with a few small calls would favor the classic loop. Anthropic’s own docs show programmatic calling costing about 8% more on a sequential benchmark.
The sandbox is a shortcut. The demo runs the fixed script in-process, which is only acceptable because the script is mine. Real model-written code needs a real sandbox.
The newer approaches
No single replacement has emerged. There is a toolbox, and the right piece depends on the workload. The common thread is moving orchestration out of the model’s turn-by-turn loop and into code.
How each one works
Native function calling: tools are defined in the API request and your code runs them, with no server needed.
Skills: folders of instructions and scripts, loaded only when relevant.
Shell and code execution: the model writes and runs code or command-line tools.
Tool search: definitions are fetched on demand instead of loaded up front.
Programmatic tool calling: the model writes a script that calls your tools as functions inside a sandbox.
Code mode over MCP: MCP tools are exposed as a typed code API, and results stay in the sandbox.
Browser and computer use: the model operates a user interface.
What the published numbers say
Tool search. Anthropic reports that loading 50+ MCP tools the traditional way takes about 72,000 tokens, about 77,000 total before work begins. With its tool search tool the total is about 8,700 tokens, an 85% reduction. On MCP evaluations, accuracy rose from 49% to 74% for Opus 4 and from 79.5% to 88.1% for Opus 4.5. (Advanced tool use)
Programmatic tool calling. On complex research tasks, average usage fell from 43,588 to 27,297 tokens, a 37% reduction. In a budget-compliance example, about 200 KB of raw expense data shrank to about 1 KB reaching the model. (Advanced tool use)
Code execution with MCP. An example workflow dropped from about 150,000 tokens to 2,000, a 98.7% saving, by presenting MCP servers as code files the agent reads on demand. (Anthropic)
Cloudflare Code Mode. Cloudflare’s MCP server exposes its whole API, over 2,500 endpoints, through just two tools,
search()andexecute(), using about 1,000 tokens of context. It says an equivalent one-tool-per-endpoint server would need about 1.17 million input tokens. The generated code runs in a V8 isolate with no file system and with outbound fetches off by default. (Cloudflare)
It is not a free lunch
Anthropic’s own programmatic tool calling docs report about 38% fewer billed input tokens on a 75-tool project-management benchmark. On a benchmark of strictly sequential single calls, the same feature cost roughly 8% more. When each step needs the model to think about the previous result, a script cannot skip that round trip, and you still pay for the container and the generated code.
The managed version: programmatic tool calling on the Claude API
My experiment bridged MCP to a sandbox by hand. The Claude API offers a managed version of the same idea. The task: query sales for three regions and say which had the highest revenue, using one query_database tool. Done the classic way, that is a model round trip per region, with every result landing in context. Done programmatically, Claude writes one script and the API pauses it each time it needs a tool result.
Here is how a turn works:
You send a request with the code execution tool and your own tool, marked as callable from code.
Claude writes a Python script and runs it in a sandboxed container.
Each time the script calls your tool, execution pauses and the API returns a
tool_useblock whosecallerfield points at that script run.You run the real tool and send back only a
tool_result, along with the same tools and the container ID.The script resumes. When it finishes, Claude sees only what the script printed and writes the answer.
The tool definitions
The key line is allowed_callers. It is what makes your tool appear to Claude’s script as an async Python function.
TOOLS = [
{"type": "code_execution_20260120", "name": "code_execution"},
{
"name": "query_database",
"description": (
"Execute a SQL query against the sales database. Returns a JSON "
'array of rows, e.g. [{"order": 1, "revenue": 1200}, ...].'
),
"input_schema": {
"type": "object",
"properties": {"sql": {"type": "string"}},
"required": ["sql"],
},
"allowed_callers": ["code_execution_20260120"], # callable from the sandbox
},
]The description spells out the output format on purpose. Claude’s script will parse the result with json.loads, so the more precisely you describe it, the fewer mistakes it makes.
The client loop is short. Claude’s script pauses whenever it needs a tool result, you run the real tool and send back only a tool_result along with the container ID, and the script resumes. In the end Claude is sampled once to write the script and once to read what it printed, and the raw rows never enter its context. The full loop is in the docs.
Rules that will bite you
These come from the programmatic tool calling docs:
While calls are pending, your reply must contain only
tool_resultblocks, with no extra text.Return each result within about four minutes, or the script gets a
TimeoutError.Tool results must be strings or text blocks, not images or documents.
Tools from the MCP connector cannot be called programmatically, and
strict: truetools are not supported.allowed_callersguides Claude but is not a security boundary, so validate inputs in your own tool code.
I wrote this example from the docs and have not run it against the live API, so check model names and tool versions there before relying on it.
So is MCP a thing of the past?
No. What is aging is the classic loop, not the standard. The useful question is where MCP earns its place and where something lighter is enough.
Where MCP still earns its place
Many clients share one integration, such as IDEs, chat apps and agents.
You consume third-party or remote services that bring their own authorization.
Another team owns the tools, and you want a stable, typed contract.
You want a discoverable catalog the agent can search instead of hard-coded calls.
Where something lighter is enough
One app with a handful of tools you own: native function calling needs no server, transport or auth layer.
Repeatable procedures and house style: skills load instructions only when relevant.
Local file and system work: a shell and a script often beat a protocol.
The choice also depends on how fast the domain changes. In a LlamaIndex comparison, a documentation MCP server often gave a coding agent enough to write correct code, while skills were rarely invoked and were burdensome to keep current. Their conclusion: for fast-changing domains, a single source of truth such as an MCP server is the better option, and otherwise skills are a lightweight alternative.
For the warehouse assistant, this means the task system can still be exposed through MCP, especially if more than one client will use it. The decision that matters more is how the agent consumes those tools once there are thousands of tasks.
How I would build the warehouse assistant
I wouldn’t choose one mechanism. I’d use two, over the same MCP contract.
Direct tools for narrow lookups. A question like “what is assigned to a given worker today?” returns a short list from one filtered call. A direct tool call is the right fit.
Code mode for broad scans. Questions like “which operators have the most overdue tasks?” or “who is overloaded this shift?” touch thousands of records and end in a small summary. The agent writes a script, the sandbox does the filtering and totals, and the model reads the summary.
MCP stays the contract underneath both. One description of the task system, with a per-tool choice of how the agent calls it. Anthropic recommends choosing either direct or code-only calling for each tool rather than allowing both, because that gives the model clearer guidance.
These example questions are illustrative, not a real query log. The thresholds that would push me toward each piece come from Anthropic’s advanced tool use post. Tool search pays off once definitions exceed about 10,000 tokens. Programmatic calling suits workflows with three or more dependent calls, or large results that only need summarizing. It is a weak fit for single calls, small responses, and tasks where the model must reason over every intermediate result.
Building it yourself
The pattern is simple enough to build without any vendor feature, but the safe version takes work. The programmatic tool calling docs describe three ways to get it:
Client-side direct execution. Give the model a code tool and run its code where your functions live. This is the smallest change, but it runs untrusted code outside a sandbox and makes tool calls a vector for code injection.
Self-managed sandbox. Same idea, but the code runs in a locked-down container, for example with no network egress. It is safe and flexible, but you must build the channel for tool calls to leave the sandbox, and maintain it.
Anthropic-managed. Enabled with a tool definition and no infrastructure to run. It is available on the Claude API, Claude Platform on AWS and Microsoft Foundry, and the docs mark the feature as not eligible for zero data retention.
The provider-agnostic recipe
Expose your tools as typed functions in a module the sandbox can import.
Give the model one tool, “run this code.”
Execute the code in a sandbox whose only way out is calls to those functions.
Return only the script’s printed output to the model.
What to get right
Sandbox isolation. Deny network access except to your tool endpoints, and set CPU, memory and time limits. Cloudflare’s Code Mode runs generated code in a V8 isolate with no file system, no environment variables to leak, and outbound fetches off by default.
Permissions. The script can call any function you expose, in a loop. Scope credentials tightly and gate destructive operations behind a confirmation.
Untrusted results. Tool results are strings and can contain text that looks like instructions or code. Validate anything that comes from external sources, such as free-text notes on a task.
Observability. Calls no longer appear as separate model turns, so log every tool call the script makes along with its result.
Timeouts. In the managed version, a pending call times out after about four minutes, and idle containers are reclaimed after about five.
The MCP gotcha
Anthropic’s managed programmatic tool calling does not currently support tools from the MCP connector. To combine MCP with code mode you have three routes:
Bridge MCP yourself, as in the experiment above: connect an MCP client, and expose its tools as functions inside your sandbox.
Wrap the same backend as ordinary custom tools and mark them as callable from code.
Use a server that does code mode on its side, like Cloudflare’s Code Mode MCP server, whose agent writes code against a typed API and runs it in Cloudflare’s sandbox.
For a task system you control, the second route is usually the least moving parts. If several other clients also need the same tools, the first route keeps MCP as the single contract.
Conclusion
MCP isn’t going away. But I think we’re moving away from an assumption that shaped early agent architectures, that the model has to see every tool call. It doesn’t need to read every database row or orchestrate every loop. It needs the information required to make the next decision.
For the warehouse assistant, that means two paths over one contract: direct tools for narrow lookups, code for broad scans, and MCP underneath both. In my test, the same 41 backend calls ran with the model’s peak context at about 79,000 tokens in the classic loop and under 1,000 in code mode. Measure on your own traffic before you switch, because on strictly sequential single-call workloads Anthropic’s own benchmark shows code costing roughly 8% more.
MCP can remain the contract. Code can become the execution layer. And the model can stay focused on reasoning.





