Updated 11 September 2026. This article first ran on 23 January 2026. Three things in it did not survive the year, and the biggest one is the framing rather than any single fact.
The original set up a three-way choice between MCP, RAG, and tool calling. That is a category error. Two of those are positions on one axis and the third is the transport underneath them, so the comparison was never between like things. This version replaces the three-way choice with the Context Freeze Point, which is the part of the original argument that survived.
Two factual corrections follow from it. The original said to reach for Model Context Protocol (MCP) when capabilities need runtime discovery, and the 2026-07-28 specification made that discovery optional and client-driven. It also listed "authorization fragmentation" as an MCP pitfall, which was already wrong at the spec level, since MCP servers were classified as OAuth 2.0 resource servers in the 2025-06-18 revision. Operational fragmentation across identity providers was real in January and is still being worked on now.
What I got wrong about MCP
In January I wrote that you should choose MCP "when capabilities need runtime discovery." That sentence had a shelf life of six months. Going stateless in the 2026-07-28 specification removed the initialize handshake, removed the Mcp-Session-Id header, and demoted capability discovery to server/discover, an RPC that servers must implement and clients may ignore. Be precise about what died here, because an MCP maintainer will be. server/discover still exists and servers MUST implement it. What died is the guarantee: clients MAY call it and MAY ignore it, so no server can be designed on the assumption that a client will discover anything before using it. The negotiated moment is gone, not the remote procedure call. Capabilities now ride along in per-request _meta fields, and list results carry ttlMs and cacheScope so a client can cache them and stop asking.
If you need the migration mechanics, I wrote those up separately in MCP Went Stateless: Your 2026-07-28 Migration Guide. This article is about the decision the mechanics were serving, and that decision turns out to be older and more durable than any of the three names in the title.
The question the original article asked, and the better version of it
The January piece ended on a line I still believe: the choice is not "which is best?" but "which control flow matches my problem?" That was right. It was also unfinished, because it left control flow as an intuition rather than a measurable property.
Here is the sharper version.
Every system that feeds a language model reaches a moment when nothing further can be admitted to the context window. Before it, something can still add a document, call a tool, widen a search. After it, the model answers with what it has. I call that moment the Context Freeze Point.
Context Freeze Point: the last moment in a request at which new material can be admitted to the model's context window, together with the component holding authority to admit it.
Two things it is not. It is not the "frozen novice problem" from the agent memory literature, which describes model weights that never update across sessions. It is also not a code freeze. That phrase already means something specific to this audience, and I make a pun on it later that is a pun rather than an argument.
Admission is the load-bearing word, because a context window does not only grow. Compaction evicts and summarises mid-run, so what the model holds keeps changing after admissions have stopped. A spawned sub-agent opens its own window with its own freeze point nested inside its parent's. An agent that compacts twice and spawns three sub-agents does not have one freeze point, it has several, and a ledger worth keeping records each of them separately. The axis below describes a single request in a single window, which is the unit most teams get wrong before they get anywhere near the harder cases.
Be careful with the word axis, because the definition names two things and only one of them is ordered. The moment is the axis, and it runs from build time to generation time. The owner is a separate property recorded alongside it, because the same moment can belong to different components: a hardcoded retriever and a model rewriting its own query both freeze at request time, and they are not the same system to debug. Record the point and the owner, not a single coordinate.
Three positions exist on the moment axis. Freeze at build time, when you embed a corpus and store it. Freeze at request time, before a single token is generated. Freeze progressively during generation, with the model deciding when it has had enough.
MCP is not a fourth position, and that is the entire argument of this article. It holds no freeze point of its own, inherits whatever the host harness does, and sits underneath all three positions rather than beside them. RAG and agentic search are answers to "when does the window seal." MCP is an answer to "how does capability reach the model and who authorised it." You can serve agentic search through an MCP server, and thousands of people do.
flowchart LR
A["Build time<br/>embed and store"] --> B["Request time<br/>retrieve, then generate"]
B --> C["Generation time<br/>model asks as it goes"]
A --> A1["ceiling: the Synthesis Horizon<br/>stale index, permission sprawl"]
B --> B1["cost: one shot to be right<br/>bounded, pre-declared call budget"]
C --> C1["cost: 39x query tokens<br/>at the smallest corpus tier"]
style A fill:#98D8C8,color:#2C2C2A
style B fill:#4A90E2,color:#FFFFFF
style C fill:#7B68EE,color:#FFFFFF
style A1 fill:#FFD93D,color:#2C2C2A
style B1 fill:#6BCF7F,color:#2C2C2A
style C1 fill:#FFA07A,color:#2C2C2A
MCP is deliberately not in that diagram. Each of the three positions buys something and charges for it, and the rest of this article prices them one at a time before returning to where MCP actually belongs.
Freezing at build time: the index you have to keep honest
You embed the corpus, store the vectors, and the model queries a snapshot. The freeze happened whenever the indexing job last ran, which means every question is answered against a photograph of the world rather than the world.
This is the cheapest position per query and the most expensive position to operate, and the costs that make it expensive are not retrieval costs at all. They are operational ones, and the clearest public account of them comes from a team that abandoned the position outright.
Boris Cherny, describing how Claude Code was built, told The Pragmatic Engineer in March that the team tried local vector databases, recursive model-based indexing, and several other approaches. All of them had downsides he names specifically as stale indexes and permission complexity. What replaced them was plain glob and grep, driven by the model. His summary: agentic search "outperformed RAG."
Read the reason carefully, because it is not the reason most people repeat. Anthropic's team did not abandon the index because retrieval quality was bad. They abandoned it because an index is a second copy of your data that has to be kept fresh and kept inside the same permission boundary as the original. Those are operational properties of a build-time freeze, and they get worse as the corpus gets more sensitive.
There is a scale ceiling here too. I named it the Synthesis Horizon when writing about Karpathy's LLM wiki pattern: the corpus size at which build-time synthesis stops working, because the index can no longer be navigated in a single context pass and the ingest step cannot tell which pages a new source should update. Synthesis Horizon marks the build-time end of the freeze axis, and it is a ceiling rather than a preference. Cross it and you are forced rightward whether you planned for it or not.
Build-time freeze earns its place when the corpus is large, slow-moving, and uniformly readable by everyone who can query it. Product documentation qualifies. Your customers' support tickets almost certainly do not.
Freezing at request time: one-shot RAG and its fixed budget
Retrieve first, completely, then generate, so that the model never sees the query alone. That was the invariant the original article led with, and as a description of the mechanism it remains exactly right.
The property that makes request-time freeze valuable is one nobody talks about, because it is boring: the budget is fixed and knowable before you start. LlamaIndex counted the round trips in January and got four, one call to pick a filter, one to embed, one to search, one to answer. Four calls is a number you can put in a latency budget and a cost model. Reranking, query rewriting and multi-hop all change that number, but they change it before the request runs, which is the property that matters. The budget is declared in advance and bounded, rather than discovered afterwards.
That predictability wins at scale, and the measurements are less flattering to the fashionable option than the discourse suggests.
| Corpus size | Latency | Correctness | Relevance |
|---|---|---|---|
| 5 documents | RAG 7.36s, agent 11.17s | agent 8.4, RAG 6.4 | agent 9.6, RAG 8.0 |
| 100 documents | RAG substantially faster | RAG slightly better | equal |
| 1,000 documents | RAG faster | RAG slightly better | equal |
Read the first row honestly, because it does not favour the argument I am making. At five documents the agent wins on quality, by two points of correctness and 1.6 of relevance, and it only loses above that. This is a crossover rather than a verdict, and it is the same shape as the much larger crossover the BM25 scaling study found. LlamaIndex's own conclusion was that scaling is easier with RAG than with agentic file search, and the failure causes they name are all freeze-axis costs: latency accumulating across model round trips, context-window pressure from large files, filesystem traversal overhead.
One failure mode of a request-time freeze is specific enough to be worth naming carefully. When retrieval misses, the model does not know it missed. Context froze, the answer was not in it, and the model produced something anyway.
Anysphere shipped exactly that failure in April 2025. A support bot signing itself "Sam" told Cursor users their subscription was limited to one active session per account. There was no such policy. Users had been getting logged out because of a race condition on slow connections. The public account is consistent with a one-shot retrieval that missed and a model that filled the gap, though nobody confirmed the architecture, which is the next point. Michael Truell had to say publicly that the policy did not exist. Users cancelled subscriptions over a rule the company never had.
Nobody published a postmortem of the retrieval architecture, and that is a pattern rather than an accident. I went looking for production postmortems of one-shot retrieval failure and found none that name a company, a date, and a number. Retrieval failures do not page anyone, and they degrade answer quality quietly, in ways that look like the model simply being dumb, so they generate cancelled subscriptions instead of incident reports. Absence of postmortems is not evidence that the position is safe.
Freezing during generation: agentic search and what it actually costs
Give the model tools and it decides for itself when it has seen enough. This is what most people mean by tool calling, and what the agentic-search papers mean by letting the model drive its own retrieval. Context grows one deliberate step at a time, and the freeze happens when the model stops asking. This position won the coding-agent argument decisively, and the industry then over-generalised from one workload to every workload.
Measurements of where it stops working are unusually clear, and unusually unflattering to the consensus.
The strongest study is a scaling analysis published on 29 July 2026, "BM25 Wins at Scale," which ran 28 nested corpus tiers spanning roughly 450-fold in size. A file-system agent leads at the smallest scale. It costs 39 times more query tokens at that smallest tier to do it. Around 10 million corpus tokens, BM25 overtakes the agent and then leads at every larger tier, by a margin the paper puts at close to 20 points at full scale. The abstract does not say 20 points of which measure, so take the direction as the finding and the magnitude as approximate. The authors' own summary is the sentence to remember: agentic reasoning works best after ranked discovery rather than in place of it.
Look closely at the paper most often cited as proof that agentic search beat RAG, because it does not say that. "Keyword search is all you need," from Subramanian and colleagues, reports the agent reaching roughly 90% of the RAG baseline across six datasets.
| Metric | Agent as share of RAG baseline |
|---|---|
| Faithfulness | 94.52% |
| Context recall | 88.05% |
| Answer correctness | 91.48% |
Ninety percent of a baseline means losing on quality. It may still be winning on cost, which is the paper's actual claim, and refusing it here would contradict what I wrote two sections ago about Anthropic deleting its index. Reaching 91% of a vector database while deleting the vector database, the embedding pipeline and the freshness problem is a real engineering win. It is just not a retrieval win, and the headline sells it as one.
The detail underneath matters more than the average. The agent is worse than RAG on five of the six datasets, dropping to 70.56% of baseline on context recall for one. It wins on FinanceBench, where answer correctness goes from 24.24% to 32.71%, and that is the strongest reading for the agent rather than the weakest: FinanceBench is the dataset where the RAG baseline is nearly useless, which is exactly where a model that can go and look earns its round trips. Cite the paper, by all means, but cite what it actually measured.
Then there is the finding that does the most damage to the whole "pick one of three" framing. A May 2026 study asked whether grep is all you need, across 116 questions and several agent harnesses including Claude Code, Codex, and Gemini CLI. Grep generally beat vector retrieval, but performance depended strongly on which harness ran it and on whether tool output arrived inline or in a file. Change the harness, change the result, same retrieval method.
That is the closest published measurement to this thesis, with two caveats I would rather state than have a reader find. The study isolated harness and tool-calling style, which bundles prompt formatting, result truncation and context management together rather than separating them, so "harness" is not a synonym for "freeze point." And it is a code-search study across 116 questions, so stretching it to retrieval everywhere would be the same over-reach I accused the industry of one paragraph ago. With both said: retrieval technology was not the variable that decided the outcome. Whatever owned the freeze point was.
Where MCP freezes context: nowhere, which is why "MCP vs tool calling" is the wrong fight
MCP does not have a freeze point. After 2026-07-28 it does not even have a connect.
Look at what the current specification actually removed. Gone are the handshake, meaning initialize and notifications/initialized, and protocol-level sessions along with the Mcp-Session-Id header that carried them. Server-initiated requests are gone as a mechanism, replaced by multi-round-trip requests, where the server returns an input_required result and the client answers by re-issuing the original call under a fresh JSON-RPC id. The two best-known ones then diverge, which is easy to miss. Elicitation maps onto that new mechanism. Sampling was deprecated outright under the specification's new twelve-month feature lifecycle, with the suggested migration being to call your model provider directly. Both paths are in the migration guide. Server-sent-event resumability: gone, so a broken stream loses the in-flight request and the client must re-issue it.
Its replacement for session state is the most revealing part of the whole design. The specification's own words: "The protocol has no concept of a state handle; from the wire's perspective a handle is an ordinary string in a tool result and an ordinary argument to subsequent tool calls." A server that needs state mints an opaque handle, hands it to the model in a tool result, and waits for the model to hand it back.
Sit with that. MCP's answer to "who carries state across requests" is the model does. MCP pushed the freeze decision up into the harness and the model, and kept only transport and authorization for itself.
Which is why the InfoQ headline in August asked whether going stateless just makes MCP an API again, and why Cloudflare's Matt Carey welcomed the change in terms that make the point better than the critics did: the mandatory Mcp-Method and Mcp-Name headers let infrastructure read those headers and act on them, per method or per tool, using the same primitives it already applies to every other API. That is a compliment about transport rather than a claim about architecture.
What the tool catalogue costs before the first question
MCP has one context cost of its own, and it is large.
A server ships its whole tool catalogue on tools/list, which is one instance of the hidden token tax practitioners have been measuring all year. Anthropic published the arithmetic on a five-server setup in November 2025: GitHub's 35 tools cost about 26,000 tokens, Slack's 11 cost about 21,000, and the full 58 tools cost about 55,000 tokens before the user has typed anything. Internally they measured a peak of 134,000 tokens consumed by tool definitions alone. Two caveats on those numbers: they are vendor-measured, and they predate a tokenizer change that produces roughly 30% more tokens for the same text on newer models, so treat them as the right order of magnitude rather than a current price list.
Caching is the 2026-07-28 specification's entire answer to this. ttlMs and cacheScope are now required on the list endpoints, and servers should return tools in deterministic order to improve prompt-cache hit rates. Caching a 55,000-token catalogue makes it cheaper to fetch. It does not make it smaller in the window.
Both major providers solved it somewhere else. Anthropic shipped a tool-search tool that defers definitions until the model asks, reporting an 85% reduction in tool-definition tokens and accuracy on their own MCP evaluation moving from 79.5% to 88.1% on Opus 4.5. OpenAI's Responses API takes allowed_tools to import only some of a server's tools, and supports defer_loading so functions load when the model decides it needs them. Same idea, two vendors, neither of them the protocol.
The MCP maintainers conceded the gap on 22 August, in the roadmap, in plain language:
Connecting to a server with a hundred tools means the model pays for that entire surface before the user has asked a single question, and tool selection tends to get worse as the list grows. We're starting a progressive discovery effort so a server can offer a small entry point and reveal more of its catalog as the conversation narrows.
"We're starting" is doing a lot of work in that sentence. As of today the capability does not exist in a shipped revision.
Why "MCP vs RAG vs tools" is a category error
Put the three side by side and the mismatch is obvious.
RAG is a freeze position: request time. Agentic search is a freeze position: generation time. MCP is a way to distribute and authorize capability, and it is agnostic about both. Asking which to use is like asking whether to use PostgreSQL, an index, or TLS.
That versus framing survived as long as it did because in 2025 MCP genuinely did have architectural content of its own. It had a handshake, a session, and a negotiation, so choosing it genuinely changed your control flow. Removing all three in the 2026-07-28 revision left a well-specified HTTP convention with a serious authorization story attached to it.
There is a real counter-argument and it deserves better than a dismissal. MCP's value was always the governance envelope rather than the negotiation: one authorization model, one audit surface, and one place to attach policy across a fleet of agents that different teams built with different harnesses. The 2026-08-22 roadmap puts agent identity and enterprise-ready security among its five priorities, describing callers that are "agents running as cloud workloads with their own identity, acting on behalf of a user who isn't present, or delegating narrower authority to sub-agents." If that is your situation, MCP is answering a question you actually have.
Notice that the question is not a retrieval question. It is an identity and policy question. Which is the point.
The wrong way: a loop with no declared freeze point
Most production agent loops never decide where context freezes. They inherit it from whichever guard trips first, and often no guard trips at all.
# Wrong: nothing here decides when the context is finished.def answer(question: str, tools: list[Tool]) -> str: messages = [{"role": "user", "content": question}] while True: reply = llm.complete(messages, tools=tools) # full catalogue, every request messages.append(reply) if not reply.tool_calls: return reply.text for call in reply.tool_calls: messages.append(run_tool(call)) # admitted without accountingThree separate things are wrong, and only one of them is the missing loop bound.
The catalogue occupies the window on every single request. Be precise about the mechanism, because the sloppy version of this complaint is easy to refute: tool definitions are a request parameter rather than something appended to messages, so their cost does not compound turn over turn. With prompt caching you pay a cache write once and a cheaper read after that. What you never stop paying is the occupancy. Those 55,000 tokens are a floor under every request in the conversation, competing with the documents you actually wanted.
Context grows without accounting, so nobody can say afterwards how much was spent or on what. And when the loop stops, it stops because the model happened to stop asking, which means the stop reason is unrecorded and unreviewable.
A loop that ends for an unrecorded reason cannot be debugged when it ends for the wrong one.
The right way: make the freeze point explicit and auditable
Declare where context freezes, charge every admission against a budget, record why the freeze fired, and tell the model when something was refused. That last clause is the one most implementations skip, and skipping it rebuilds the exact failure this article condemned two sections ago: a model answering from a window that is quietly missing something, with no way to know.
from __future__ import annotationsfrom dataclasses import dataclass, fieldfrom enum import Enumclass FreezeStage(str, Enum): REQUEST = "request" GENERATION = "generation"@dataclass(frozen=True)class FreezePolicy: stage: FreezeStage max_context_tokens: int max_retrievals: int@dataclassclass FreezeLedger: policy: FreezePolicy context_tokens: int = 0 retrievals: int = 0 frozen_reason: str | None = None admitted: list[str] = field(default_factory=list) @property def frozen(self) -> bool: return self.frozen_reason is not None def begin_generation(self) -> None: """A request-time policy seals the window here. A generation-time one does not.""" if self.policy.stage is FreezeStage.REQUEST: self.freeze("request-time policy, sealed before generation") def admit(self, label: str, tokens: int) -> str | None: """Return None on success, or the refusal the model must be told about.""" if self.frozen: return f"refused {label}: {self.frozen_reason}" if self.retrievals + 1 > self.policy.max_retrievals: self.freeze(f"retrieval cap of {self.policy.max_retrievals} reached") return f"refused {label}: {self.frozen_reason}" if self.context_tokens + tokens > self.policy.max_context_tokens: self.freeze(f"token budget of {self.policy.max_context_tokens} reached") return f"refused {label}: {self.frozen_reason}" self.retrievals += 1 self.context_tokens += tokens self.admitted.append(label) return None def freeze(self, reason: str) -> None: if not self.frozen: self.frozen_reason = reasonTwo details carry the design. begin_generation() is where stage stops being a label and starts deciding something, because a request-time policy seals the window at that call and a generation-time policy does not. And admit() returns a refusal string rather than a bare False, because the caller has to put that string somewhere the model will read it.
Wired back into the loop from the previous section, the shape is barely different:
def answer(question: str, tools: list[Tool], policy: FreezePolicy) -> str: ledger = FreezeLedger(policy=policy) messages = [{"role": "user", "content": question}] ledger.begin_generation() while True: reply = llm.complete(messages, tools=tools) messages.append(reply) if not reply.tool_calls: return reply.text for call in reply.tool_calls: result = run_tool(call) refusal = ledger.admit(call.name, count_tokens(result)) if refusal is None: messages.append(result) else: messages.append({"role": "user", "content": refusal})One honest limitation: you have to fetch a result before you can count its tokens, so this budget governs what enters the window rather than what crosses the network. If your costs are dominated by the fetch rather than the context, you need a second limit in a different place.
A ledger that only logs is an audit artefact. A ledger whose refusals reach the model is a control, because the model can narrow its next request or decline to answer instead of confabulating over a gap it cannot see.
Does ranking before the agent actually help?
Drive a realistic sequence through it. Seven candidate documents, sizes drawn from a support corpus, and a relevance score per document, which is whatever your ranker produces.
CORPUS = [ ("billing_policy.md", 1800, 0.91), ("refund_matrix.md", 2400, 0.88), ("dunning_runbook.md", 3100, 0.42), ("legacy_billing_v1.md", 2900, 0.31), ("eu_tax_addendum.md", 2600, 0.77), ("support_macros.md", 1500, 0.19), ("crm_export_2026.csv", 4200, 0.08),]def rank(corpus, keep: int): """Request-time cut: pick the candidate set once, by score.""" return sorted(corpus, key=lambda row: row[2], reverse=True)[:keep]def retrieve(rows, policy: FreezePolicy) -> tuple[FreezeLedger, list[str]]: ledger = FreezeLedger(policy=policy) told: list[str] = [] for label, size, _score in rows: refusal = ledger.admit(label, size) if refusal is not None: told.append(refusal) break return ledger, tolddef report(title: str, ledger: FreezeLedger, told: list[str]) -> None: print(title) print(f" admitted : {', '.join(ledger.admitted)}") print(f" context : {ledger.context_tokens} tokens") print(f" feedback : {told[-1] if told else 'nothing refused'}")Three runs, same policy, same 8,000-token budget, same five-retrieval cap:
1. no ranking, storage order admitted : billing_policy.md, refund_matrix.md, dunning_runbook.md context : 7300 tokens feedback : refused legacy_billing_v1.md: token budget of 8000 reached2. ranked, ranker correct admitted : billing_policy.md, refund_matrix.md, eu_tax_addendum.md context : 6800 tokens feedback : nothing refused3. ranked, ranker wrong about one file admitted : crm_export_2026.csv, billing_policy.md context : 6000 tokens feedback : refused refund_matrix.md: token budget of 8000 reachedRuns one and two make the case for staging. Walking in storage order spends 3,100 tokens on a runbook scoring 0.42, hits the budget, and never reaches the tax addendum at 0.77. Ranking puts the addendum in the window and leaves the runbook out, using 500 fewer tokens. Ranking did not only reduce the spend, it changed which documents survived, which is the only property of a freeze point that affects the answer.
Run three is the case against it, and it is the more useful run. I changed exactly one number: the ranker now scores the 4,200-token customer-record export at 0.95 instead of 0.08. Ranking admits that file first, then the billing policy, then hits the budget and refuses the refund matrix, which scores 0.88. Two documents instead of three, and the best one missing.
Be clear about what the first two runs proved, which is less than it looks. A ranker defined to be correct beats an unranked walk by construction, because the same numbers serve as the ranker's output and as the reader's ground truth. The code demonstrates the mechanism. It does not measure the advantage, and it cannot, because the advantage lives entirely in whether your ranker is any good on your corpus. That is the whole reason I keep telling people to measure retrieval on their own corpus rather than pick a winner from someone else's benchmark.
Does the stage actually decide anything?
Same corpus, same budget, same ranked candidate set. The only difference is stage, and after retrieval finishes the model asks for one more document.
4. request-time freeze admitted : billing_policy.md, refund_matrix.md, eu_tax_addendum.md context : 6800 tokens feedback : refused late_discovery.md: request-time policy, sealed before generation5. generation-time freeze admitted : billing_policy.md, refund_matrix.md, eu_tax_addendum.md, late_discovery.md context : 7700 tokens feedback : admitted late_discovery.mdSame documents retrieved, same budget available, opposite outcomes for the late request. That is the axis, expressed as a branch in a program rather than a diagram. And in both cases the model is told which it got, so a late discovery that was refused becomes something the answer can acknowledge rather than something it silently omits.
The hybrid position: rank first, then let the agent reason
Buried in the conclusion of the BM25 scaling study is the recommendation most teams should implement, and it is more specific than the "use both" advice that usually stands in for thinking: agentic reasoning works best after ranked discovery rather than in place of it. In production, stage one is usually a cross-encoder reranker, which is the mature form of exactly this idea and long predates the agentic framing.
Runs one through three above are that argument in miniature, including its failure case. The request-time cut is cheap and bounded. The generation-time freeze inside the surviving set is where the model earns its round trips. Get the cut wrong and no amount of agentic reasoning recovers the document you never handed it.
Skip the staging when your corpus is genuinely small. Below the crossover the ranker is machinery you maintain for no measured gain, and an agent wandering a handful of files will do fine. Add it once a human can no longer hold the corpus in their head. I do not have a measured figure for where that sits, and the ten million tokens in the scaling paper is the only published one, so treat that sentence as my prior rather than a finding.
The degenerate case: load everything and skip retrieval
There is a fourth thing teams do that the axis has to account for, and the frontier-lab version of this audience will raise it immediately. With million-token context windows and cache reads priced far below fresh input, one legitimate answer is to freeze at request time with no selection step at all. Load the corpus, skip retrieval engineering entirely, let attention do the work.
This is request-time freeze in its degenerate form, and it fails differently. A retrieval miss is a document that never arrived. A long-context miss is a document that arrived and was not attended to, which is harder to detect because nothing in your logs looks wrong.
The published measurements are not reassuring, though they are getting old. NoLiMa, which tests retrieval when the needle shares little vocabulary with the question, found ten models dropping below half their short-context baseline at 32K tokens, and GPT-4o falling from 99.3% to 69.7% at 32K and 56% at 128K. The authors' "effective length," the point where a model still holds 85% of its base score, came in dramatically shorter than the advertised windows. Those are 2025-era models and the current generation may well do better, so read the shape rather than the numbers.
Cost is the other half, and it now differs by vendor in a way worth knowing. Anthropic charges no long-context premium, billing a 900,000-token request at the same per-token rate as a 9,000-token one. OpenAI prices a long-context tier at roughly double input and 1.5 times output. The same architecture decision therefore has a different cost curve depending on whose model is behind it.
The freeze point is also a trust boundary
A coined term that absorbs every incident explains none of them, so here are five, and only two of them are freeze-point failures. The other two are worth including precisely because they are not, and knowing where a term stops is how you find out whether it means anything.
A freeze-point failure. Asana's experimental MCP server, launched 1 May 2025, had a tenant-isolation flaw that let requests from one organisation receive cached results belonging to another. Project names, task descriptions, metadata. It was found on 4 June and the feature was disabled from 5 to 17 June. No attacker was involved, which is the uncomfortable part. This is the definition almost word for word: material admitted to a context window by something that lacked the authority to admit it.
A freeze-point failure. Tool-description poisoning, covered below, is the other one, and it is the one nobody has a mitigation for.
Not a freeze-point failure, and instructive anyway. In September 2025 a package called postmark-mcp built a reputation across 15 clean releases. Version 1.0.16 added one line, a blind carbon copy sending every email the server handled to an attacker-controlled domain, across a package that had attracted 1,643 downloads in total. What changed was code, not context-window contents, so this is a supply-chain compromise and calling it anything else would be stretching the term to fit. It still matters here, because the publisher legitimately controlled their namespace the whole time. MCP Registry namespace verification would not have helped: it verifies who published, never what they published. The registry's own documentation is candid about this, delegating security scanning to package registries and downstream aggregators, and describing its metadata as deliberately unopinionated. Seven months after I first wrote this article, it is still in preview.
Not a freeze-point failure at all. CVE-2026-33032, scored 9.8 on the Common Vulnerability Scoring System, put the /mcp_message endpoint of nginx-ui on the network without the authentication middleware its sibling endpoint had. Twelve destructive tools, no credentials, and full nginx takeover in two HTTP requests, actively exploited until it was fixed in v2.3.4. That is a missing authentication check. Every security framework ever written predicts it, which is another way of saying none of them earns credit for it.
A pun, and I am labelling it as one. In July 2025 a Replit agent deleted a production database during what Jason Lemkin had declared a code freeze. It then fabricated test results and told him a rollback would not work. The rollback worked. "Code freeze" is a different freeze from the one this article defines, and the overlap is linguistic. The real lesson survives the wordplay: a boundary asserted in English to a system with no mechanism to enforce it is not a boundary.
Now the gap that should worry you most. I read the current specification's security-best-practices page in full. It covers the confused deputy problem, token passthrough, server-side request forgery, state-handle hijacking, local server compromise, OAuth URL validation, stdio proxy escalation, mix-up attacks, localhost redirect impersonation, trust policies for client metadata documents, and scope minimisation.
Not one section addresses a model being persuaded by text it read.
Every threat the page models is an attack on the transport or the authorization flow. Tool-description poisoning, where the natural-language description of a tool carries instructions aimed at the model rather than the developer, appears nowhere. Nearest to it is a single line on the tools page saying clients must treat tool annotations as untrusted unless they come from trusted servers. That is a lot of weight on the word "trusted." I went deeper on this class of attack in Securing MCP Servers: Context Injection and Data Exfiltration.
A maintainer's answer to this writes itself, and it is partly right: a wire protocol cannot stop a model being persuaded by text, and guarding against that is the host application's job. The counter is narrower and harder. MCP defines the channel the prose arrives through, defines the field it arrives in, and defines the trust status of tool annotations in one sentence. It defines nothing about whether the description itself can be trusted, while being the reason that description reached the model at all.
What this page secures is the pipe. What travels through it is prose, aimed at a model, and that model is the thing deciding when your context freezes.
How to choose your freeze point
flowchart TD
Q1{"Does the corpus change<br/>faster than you can reindex?"}
Q1 -->|No| Q2{"Is every document readable<br/>by every user who can query?"}
Q1 -->|Yes| Q3
Q2 -->|Yes| BUILD["Freeze at build time<br/>embed and store"]
Q2 -->|No| Q3{"Is the corpus above ~10M tokens?<br/>(one unreplicated study)"}
Q3 -->|Yes| REQ["Freeze at request time<br/>lexical or hybrid retrieval"]
Q3 -->|No| Q4{"Can you afford unbounded<br/>round trips per query?"}
Q4 -->|Yes| GEN["Freeze during generation<br/>agentic search, with a ledger"]
Q4 -->|No| REQ
BUILD --> MCP{"Multiple teams, multiple<br/>harnesses, shared audit?"}
REQ --> MCP
GEN --> MCP
MCP -->|Yes| SERVE["Serve it over MCP<br/>for identity and policy"]
MCP -->|No| DIRECT["Call it directly<br/>MCP buys you nothing here"]
style Q1 fill:#7B68EE,color:#FFFFFF
style Q2 fill:#7B68EE,color:#FFFFFF
style Q3 fill:#7B68EE,color:#FFFFFF
style Q4 fill:#7B68EE,color:#FFFFFF
style MCP fill:#7B68EE,color:#FFFFFF
style BUILD fill:#98D8C8,color:#2C2C2A
style REQ fill:#4A90E2,color:#FFFFFF
style GEN fill:#FFA07A,color:#2C2C2A
style SERVE fill:#6BCF7F,color:#2C2C2A
style DIRECT fill:#FFD93D,color:#2C2C2A
Six checks, in review order, to run against the next agent design that crosses your desk.
- Name the freeze point out loud. If nobody on the team can say which of the four positions the system occupies, it occupies whichever one the framework defaulted to. That is the finding, and it is the common one.
- Charge every admission to a ledger. Token count, retrieval count, and the reason the freeze fired. An agent that cannot tell you why it stopped is an agent you cannot debug when it stops wrongly.
- Set the cap you will actually hit. In the run above, a five-retrieval cap never fired because an 8,000-token budget bound first. Measure which limit binds before you tune either.
- Price the catalogue before the query. Count tokens, not tools: in Anthropic's own figures Slack runs about 1,900 tokens per tool against GitHub's 740, so a tool count tells you almost nothing. If the total is above roughly 20,000 tokens, a threshold I picked from experience rather than a study, use deferred loading at the provider layer, because the protocol will not do it for you yet.
- Ask what your index would cost to keep honest, not what it costs to build. Staleness and permission drift are the two costs that made Anthropic abandon indexing for Claude Code, and neither shows up in a retrieval benchmark.
- Separate the freeze question from the MCP question. Decide where context freezes on retrieval grounds. Decide whether to serve it over MCP on identity, audit, and distribution grounds. Answering one with the other is the category error this whole article is about.
What to watch
Progressive discovery is the one to track. If it lands in a shipped MCP revision, catalogue cost stops being a provider-layer workaround and the protocol reclaims some architectural content. Its roadmap dates that effort to August 2026 with no shipping commitment attached, so treat any timeline you read anywhere as speculation.
Watch the 10-million-token crossover from the BM25 scaling study. It is a single paper on a single corpus family, and it is the most load-bearing number in this article. If it replicates on other corpora, generation-time freeze becomes a small-corpus technique with a hard ceiling. If it does not, I will be revisiting this section.
Watch whether anyone publishes a real RAG postmortem. Silence is currently doing argumentative work for people who want to claim the position is safe.
And watch the tokenizer, quietly, if you compare cost across model generations. Anthropic's newer models produce roughly 30% more tokens for the same text, which is enough to invert a per-document cost comparison that does not account for it.
No acronym was ever the decision
My decision tree from the original article was not wrong so much as under-specified. Here is the version I would defend now.
If your problem is answering questions from a large, slow, uniformly-readable corpus, freeze at build or request time and spend your effort on keeping the index honest. If your corpus is small and the questions are exploratory, freeze during generation and put a ledger on it. If your corpus is above roughly 10 million tokens, ranked retrieval will beat an agent wandering through it, and the agent should run after the ranking rather than instead of it.
RAG is not dead in 2026, which is the question everyone is actually asking. It wins at every corpus size the BM25 scaling study measured above roughly ten million tokens, and the naive version of it, one embedding pass and one shot at being right, is the part that deserved the obituary.
MCP is orthogonal to all of that. Serve your chosen position over MCP when you need one identity model and one audit surface across teams and harnesses. Skip it when you do not, and lose nothing architectural by skipping it.
The decision is when your window stops admitting anything new, and who holds the authority to admit it. Write that down before the framework decides it for you.
References
- Model Context Protocol. Key Changes - 2026-07-28. https://modelcontextprotocol.io/specification/2026-07-28/changelog
- Model Context Protocol. Security Best Practices - 2026-07-28. https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices
- Model Context Protocol. Tools - 2026-07-28. https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- Model Context Protocol Blog (2026, August 22). The New MCP Roadmap. https://blog.modelcontextprotocol.io/posts/mcp-roadmap/
- Model Context Protocol. The MCP Registry. https://modelcontextprotocol.io/registry/about
- Wang, P., Xu, B., Wang, S., Du, M., Zeng, X., Wu, H., Zhang, L., & Zhang, L. (2026, July 29). BM25 Wins at Scale: A Scaling Study of Retrieval-Augmented Generation Paradigms. arXiv:2607.26497. https://arxiv.org/abs/2607.26497
- Subramanian, S., et al. (2026). Keyword search is all you need: Achieving RAG-Level Performance without vector databases using agentic tool use. arXiv:2602.23368. https://arxiv.org/abs/2602.23368
- Sen, S., Kasturi, A., Lumer, E., Gulati, A., & Subbiah, V. K. (2026, May 14). Is Grep All You Need? How Agent Harnesses Reshape Agentic Search. arXiv:2605.15184. https://arxiv.org/abs/2605.15184
- Bertelli, C. A. (2026, January 13). Did filesystem tools kill vector search? LlamaIndex. https://www.llamaindex.ai/blog/did-filesystem-tools-kill-vector-search
- Orosz, G. (2026, March 4). Building Claude Code with Boris Cherny. The Pragmatic Engineer. https://newsletter.pragmaticengineer.com/p/building-claude-code-with-boris-cherny
- Anthropic (2025, November 24). Advanced tool use. https://anthropic.com/engineering/advanced-tool-use
- OpenAI. MCP and Connectors. https://developers.openai.com/api/docs/guides/tools-connectors-mcp
- Wiggers, S.-J. (2026, August 12). MCP Goes Stateless, and Developers Ask Whether That Just Makes it an API Again. InfoQ. https://www.infoq.com/news/2026/08/mcp-stateless-gateway/
- AI Incident Database. Incident 1152: Replit agent destructive commands during code freeze. https://incidentdatabase.ai/cite/1152/
- The Register (2025, April 18). Cursor AI's own support bot hallucinated its usage policy. https://www.theregister.com/2025/04/18/cursor_ai_support_bot_lies/
- BleepingComputer. Asana warns MCP AI feature exposed customer data to other orgs. https://www.bleepingcomputer.com/news/security/asana-warns-mcp-ai-feature-exposed-customer-data-to-other-orgs/
- Postmark (2025, September). Information regarding the malicious postmark-mcp package. https://postmarkapp.com/blog/information-regarding-malicious-postmark-mcp-package
- The Hacker News (2025, September). First malicious MCP server found in the wild. https://thehackernews.com/2025/09/first-malicious-mcp-server-found.html
- Rapid7. ETR: CVE-2026-33032 nginx-ui missing MCP authentication. https://www.rapid7.com/blog/post/etr-cve-2026-33032-nginx-ui-missing-mcp-authentication/
Related Articles
- MCP Went Stateless: Your 2026-07-28 Migration Guide
- Model Context Protocol (MCP): Architecture, Tradeoffs, and Production Realities



