Chapter 1 of 6 · 17 min read
Application-controlled resources and model-controlled tools
On this page
These servers speak the Model Context Protocol (MCP), which gives a server a small set of
ways to offer something into a conversation, and this topic is about one of them. A tool
is a function the server advertises to the model — a name, a description, an argument schema
— that the model may decide to call part-way through a turn. A resource is a piece of
readable content the server publishes at a URI, such as crm://customer/4471/subscription
or file:///repo/architecture.png; the host application reads it and puts the contents into
the context window, and the model has no way to ask for one. Which of the two a piece of data
belongs to is the decision this chapter is about, and it turns on a property that is easy to
mistake for a different one.
The rule of thumb most teams reach for is that reads are resources and writes are tools. It
settles most cases and it cannot settle the ones that matter, because the interesting
arguments are all about data that is read-only under either design. A customer's current
subscription record — plan name, seat count, renewal date, billing status, about 700 bytes of
JSON — is read-only whether it arrives as get_subscription(id) or as
crm://customer/{id}/subscription. Both designs deliver the same bytes to the same model.
What they disagree about is who fetches them and when.
Resources exist because the model is a minor contributor to what ends up in a context window. The system prompt, the transcript, the record on screen — the host puts all of that there on its own initiative, and a server had no typed way to offer content into that pipeline until resources gave it one. Everything a server held otherwise had to be wrapped in a function and asked for, which turns "put the customer's record in front of the model" into a judgement the model has to get right in the middle of reasoning about something else, and charges a round trip for the privilege. A resource carries a URI for the same reason a web page does: a stable name is something a host can store, render in a menu, and hand to a permission layer.
You have met resources from the user's side of the screen already, whether or not they
carried the name: the attachment picker in a desktop assistant, the @-mention that pulls a
document or a ticket into a chat, the editor that ships the current file with every request.
From the server author's side they are entries in a listing the host browses.
Two traces, one question
Put the two designs against the same user turn and read the trace. A support agent has ACME's account open in a host application connected to an internal CRM server, and types "is ACME still on the annual plan?"
Under the tool design, the host sends the conversation and the server's six tool definitions
to the provider, gets back a tool_use block naming get_subscription with id: "4471",
dispatches tools/call to the CRM server, receives the 700 bytes, appends them as a tool
result and calls the provider again.
On the team's staging environment the first provider call takes 1.4 s to the stop reason, the CRM lookup takes 38 ms, and the second provider call takes 2.1 s. The agent waits about 3.6 s and the model made one decision along the way: whether to call the tool at all.
Two provider calls rather than one is not overhead that a faster implementation removes; it
follows from what a tool is. The host cannot know which tool the model will pick, because that
choice is an output of the model rather than an input to it. Producing that output means
sending the whole conversation and getting a completion back. The completion is where the
tool_use block appears, so by the time the host knows which record to fetch, the first
inference has already been paid for and finished. Whatever the CRM then returns has to travel
in a second request, because there is no way to insert it into a request that has already been
answered. One decision delegated to the model therefore costs exactly one additional
inference, and on this staging environment that inference is the 1.4 s.
Under the resource design the host already knows which customer is on screen, so before it
composes the turn it issues resources/read for crm://customer/4471/subscription and puts
the returned document into the context as an attachment.
The read is 38 ms, the single provider call is 2.1 s, and the agent waits about 2.2 s. There was no decision point, because nothing in the loop had to choose whether the record was relevant — the host settled that when it decided that the open account is part of the turn's context.
The two totals are worth adding up by hand, because the arithmetic says something the figures alone do not. Under the tool design the intervals are 1.4 s, 38 ms and 2.1 s, which sum to a little over three and a half seconds against the 3.6 s quoted; under the resource design they are 38 ms and 2.1 s, a little over two seconds against the 2.2 s quoted. Each quoted total therefore carries a fraction of a second the trace does not itemise — host work, transport, whatever sits between the arrows in the diagram — and neither figure is a pure sum of its parts. What survives the rounding is the gap between the two totals, because the same unitemised overhead sits inside both of them and cancels when you subtract. The 38 ms and the 2.1 s appear on both sides as well, so subtracting leaves exactly one term standing: the first provider call.
The 1.4 s saved is the visible part. The part that matters more is that the tool design
introduced a decision the host did not need to delegate, and that decision has a failure
rate. On the same staging traffic, get_subscription fired on 71% of the turns where the
record was relevant; the other 29% produced answers reasoning from whatever the transcript
had said about the account earlier. A resource attached by the host enters the context on
100% of turns, because attaching it is not a judgement call.
That 71% is the kind of number that moves when nothing about your server changes. It depends on the wording of the tool description, on how many other tools are competing for attention in the same listing, on the model version, and on how far back in the transcript the account was last mentioned. The team measured it by replaying 240 stored support turns through both designs and comparing the answers against the record as it stood at replay time, which is a crude instrument and still enough to show that the variance exists and that it lives on the model's side of the boundary.
The two rates are worth checking against each other before either is used to argue anything. Both are computed over the same set — turns of the replay in which the subscription record was relevant — so the 71% that fired and the 29% that did not exhaust that set with nothing left over. There is no third bucket of turns where the tool fired late, or fired with the wrong identifier, or fired and was ignored; the replay's classification is binary and every relevant turn falls in one half of it. That is what makes the comparison with the resource design legitimate: the resource figure is over the same denominator, so the two designs are being scored on the same question rather than on two different populations that happen to be described with percentages.
The hundred per cent on the resource side needs one condition attached to it, and the condition is doing real work. It holds for turns in a host that has such a policy — an application that opens with an account selected and attaches that account's record — and it says nothing about a host with no attachment surface at all, where the same resource reaches the model on none of the turns. The determinism being claimed belongs to the host's policy rather than to the resource primitive, which is why the objection later in this chapter is half right rather than wrong. Later paragraphs shorten the 71% to a coin flip, and that shorthand is about the decision having a failure rate at all rather than about the rate being one in two.
The rule is control of invocation
The split those two traces draw has a name in the specification. MCP sorts its primitives by who is in charge of firing them: tools are model-controlled, resources are application-controlled — the host, or the user working through the host's interface, decides when one is read and how its contents enter the context — and prompts sit on a third line as user-controlled, surfaced as something a person picks out of a menu. Read-only correlates with the resource side because data you can safely fetch ahead of time is usually data with no side effects, and correlations are not rules.
Once you hold onto invocation control rather than side effects, the consequences line up.
Resource crm://customer/{id}/subscription | Tool get_subscription(id) | |
|---|---|---|
| Chooses the moment | host or user, before the turn is composed | model, mid-turn |
| Added latency | one server round trip, off the critical path | one extra provider round trip, 1.4 s here |
| Consent | host asks once when the user attaches, or by policy | host must decide per call, with no user in the loop |
| Authorization sees | the session and the URI | the session and the arguments the model wrote |
| Audit record | "host read URI X at time T for turn N" | "model requested X at time T; host allowed it" |
| Standing cost | none in the request body until attached | schema in every request of the session, ~120 tokens |
| Failure lands on | the host, which can retry or show an error | the model, which has to interpret an error string |
The authorization row is where the two designs differ most in the long run. When the model composes the arguments, the identifier being fetched is a value the model produced, so every authorization check has to treat it as untrusted input arriving from inside the conversation. When the host resolves the URI from the record already on screen, the identifier came from the application's own state. Neither arrangement removes the server's obligation to check, and they give the check very different provenance to work with.
The audit row is the same difference written down after the fact, and it is worth reading as a sentence rather than as a cell. "Host read URI X at time T for turn N" names an actor, an identifier and an occasion, and every term in it was fixed before the turn began. "Model requested X at time T; host allowed it" names an actor whose reasons are not recoverable from the log, because the reason lives in a completion the log does not contain. Six months later somebody will ask why a particular record was in a particular context window. The first line answers it from the line itself; the second answers it only if the transcript that produced the request was kept, and only to the extent that a transcript explains a model. What the two rows are comparing, then, is not how much is logged but how much of the question the log can close on its own.
Where read-only still has to be a tool
The CRM server also exposes search_tickets(query, status, since). It is
read-only in the strictest sense — it opens no transactions and mutates nothing
— and it must be a tool, because the query cannot exist before the turn starts.
The model reads "find out whether ACME complained about the invoicing change"
and composes query: "invoice OR billing", since: "2024-11-01", a string that
depends on reasoning the host has not done and cannot do. There is no URI the
host could have attached in advance that would have contained the answer, since
the space of possible queries is unbounded.
That is the general shape of a read that belongs in a tool: the parameters are a function of
the model's reasoning, and the host cannot enumerate the candidate documents ahead of time.
Contrast the style guide sitting on the same server as get_style_guide(), returning the
same 4 KB Markdown document on every call with no parameters at all. Nothing about that
document depends on the turn. Every invocation is the host paying 1.4 s and a decision point
to retrieve a constant, and paying it inconsistently, since the model calls it when it
remembers to. The document is the canonical resource: fixed identity, fixed contents between
edits, relevance decided by the user or by a host policy that says "engineering assistant
sessions always carry the style guide".
A team's current sprint board sits in the same category with one extra property. It is dynamic — it changes several times a day — and it is still a resource, because the decision to include it is one a person can make: they pick it in the host's attachment menu, or the host attaches it because the session was opened from the board view. Dynamism is a reason to think about freshness, which chapter 5 takes up as subscriptions and polling, and not a reason to hand invocation to the model.
The sprint board is where most people get this wrong, and the wrong belief is worth stating in the form it usually arrives in: this data changes, so the model needs to be able to pull it, so it has to be a tool. The reasoning is tempting because a tool call really does return the value as it stands at the moment of the call, and an attachment really does go stale. It is false because it welds together two independent questions. Whether the value is current is settled by when something last read it, and the host can read it as often as it likes — once per session, once per turn, or on a notification from the server. Whether the model can pull it is settled by who is permitted to invoke, and handing that to the model does not make the value fresher on the turns where the model does not think to ask.
The objection that a model cannot call a resource
The objection turns up in every review of this kind, usually phrased as: resources are useless, because the model cannot request one, so the data only reaches it if some host happens to surface it.
The half of that which holds is worth stating plainly. A resource is inert without a host
that renders it. If the host has no attachment interface, no @-mention of server resources,
and no policy layer that reads URIs on the user's behalf, then everything on your
resources/list is unreachable in that host, and it will look to the model exactly like data
you never exposed. This is a real deployment risk rather than a theoretical one, and it is
severe enough that chapter 6 is about what to do when a host you depend on behaves that way.
The objection is also right that anything whose relevance can only be established
mid-reasoning cannot be a resource, which is the search case above.
The half that does not hold is the word "useless", which assumes the model is the only thing that can put bytes into a context window. The host puts most of them there — the system prompt, the transcript, the open file, the selected records — and it does so deterministically at a moment it controls. Resources exist to give that side of the boundary a typed, addressable way to reach into a server, so that a host can enumerate and attach documents without the server having to invent a tool for each one. A colleague who says the model cannot call resources has described the design, and then drawn the wrong conclusion from it.
The practical answer for the 4 KB style guide is that it is a resource whose contents the host injects, and if the host in question refuses to surface resources, the fix is a host-side policy that reads the URI on every session start. A tool would re-delegate a decision whose correct answer was already fixed, and it would pay the standing cost of a schema in every request to get an answer the host already had.
The split is older than MCP and you have met it before under other names. An HTTP resource identified by a URL, fetched by whoever holds the reference, is the same arrangement as a remote procedure whose caller decides when to invoke it; REST's whole argument was that naming things and retrieving them by name gives intermediaries — caches, proxies, permission layers — something to reason about, while an opaque call gives them nothing. MCP is making that argument again in a setting where the intermediary is a host assembling a context window, and where the alternative caller is a language model whose invocation decisions are probabilistic.
That comparison is a comparison and not a claim of equivalence, and it stops in a specific place. REST's intermediaries are shared infrastructure sitting in the network — a cache in front of an origin serves every client that passes through it, and the rules it follows are carried in the protocol. A resource's intermediary is one host, in one session, applying its own attachment logic, and the protocol carries nothing it could act on generically. So the half of the argument that transfers is the half about naming: a stable identifier is something a permission layer or a menu can hold and reason about. The half that does not transfer is everything HTTP built on top of naming once the names existed.
Authorization, freshness and cost stay separate decisions
Choosing the resource side settles when the fetch happens and nothing else. It does not decide where authorization lives; the server still checks every read, and chapter 4 is about a cross-tenant bug that exists precisely because people assume otherwise. It does not decide freshness; a resource read at 09:00 is a snapshot of 09:00 and stays one unless something re-reads it. It does not reduce token cost, since 700 bytes of JSON in an attachment costs about what 700 bytes of JSON in a tool result costs. The saving is a provider round trip and a coin flip, not context budget.
It also does not give you a caching layer. A URI is a name for a document, and naming a document does not mean the client will hold it, revalidate it, or notice that it changed. Anything resembling cache semantics has to be built on top of subscriptions or on a re-read schedule.
What it does settle is worth restating now that the numbers are behind it. Invocation control is a property of the decision rather than of the data: the same 700 bytes are a resource when the application already knows they are relevant and a tool when only the model can know. Read-only tells you which side is likely; the reliable test is whether the parameters of the fetch exist before the turn starts. For the account on screen they do, so the record is a resource. For a ticket search composed from a sentence the user has just typed they do not, so it is a tool. For a style guide there are no parameters at all, and paying a provider round trip and a 29% miss rate to retrieve it is the clearest version of the mistake.
The subscription record now has a URI, a shape and a reason to exist. None of that helps if the host cannot find it, and finding it is not one lookup: concrete documents and parameterised families are advertised through two different methods, and a server that publishes a template on the wrong one is invisible to every client that connects.
Resources — read the current version at https://daviddominguez.dev/learn/en/resources/application-controlled-resources-and-model-controlled-tools