What are MCP's primitives?
MCP has three server primitives: tools, which the model invokes; resources, which the application attaches to the context; and prompts, which the user picks on purpose. The specification calls the split a control hierarchy, and it does not sort the three by the shape of the data. It sorts them by who decides when the thing enters the context1.
That sentence settles almost every classification question. The same sales report can be a tool, a resource or a prompt, and the right answer depends on which actor pulls the trigger rather than on the content.
All three show up in the Model Context Protocol as what a server exposes. If you are only ever going to write tools, the text describing each one deserves its own attention, because it lands inside the model’s reasoning. And if the question is how any of this turns into a call to your model provider, the comparison lives in MCP or function calling.
Tools: what the model fires
Tools are executable functions. The client discovers them with tools/list and
invokes them with tools/call, passing arguments that must match the tool’s
inputSchema, a JSON Schema that defaults to draft 2020-12 when no $schema is
present2.
A tool may also declare an outputSchema. When it does, the server must return
structured results conforming to that schema in the structuredContent field, and
the client should validate them. For backwards compatibility, the same serialized
JSON usually also arrives as a text block in content2.
The detail most implementations get wrong is the error channel, because there are
two. Protocol errors cover malformed requests, unknown tools and server failures,
and come back as ordinary JSON-RPC errors. Execution errors come back inside the
result, with isError set to true and the failure text in content. The
specification spells out why: execution errors carry actionable feedback and
clients should hand them to the model so it can self-correct, while protocol
errors may be passed along but are less likely to result in successful
recovery2. A server returning -32603 because a date was formatted wrong robs
the model of the chance to retry with the right date.
Two small conventions with real effects. Tool names should run 1 to 128 characters, use only ASCII letters, digits, underscore, hyphen and dot, and be unique within the server. And servers should return tools in a deterministic order, because that lets the client cache the list and improves prompt cache hit rates when tools go into the model’s context2. Iterating a map with unstable ordering there costs actual money.
Tool or resource for the same data
Take a monthly sales report and expose it both ways.
As a tool, get_sales_report takes the month as an argument and the model calls it
mid-task whenever it decides it needs to. The tool declaration sits in the context
on every model call, used or not. The model picks the argument, which is good when
the right month depends on the conversation and bad when the model picks the wrong
month.
As a resource, the same report becomes sales://2026-07 with a stable URI and
enters the context when the application attaches it. It occupies nothing in the
prompt until then. In exchange, somebody has to choose: either the user, from a
picker, or the application, by some heuristic. The specification says resources are
application-driven, with the host determining how to incorporate context, and lists
the possible patterns, including automatic inclusion based on heuristics or on the
model’s own selection1.
The security difference falls out of that and usually goes unnoticed. The protocol
gives the model no channel for reading a resource: resources/read is a client
call. Text injected into a tool result can push the model into calling another
tool, but not into pulling a resource on its own — unless the application chose to
let the model select, which is one of the patterns the spec permits. If you are
designing a server over sensitive data, exposing it as a resource moves the
decision outside the model.
Resources: what the application attaches
Every resource is identified by a URI. The client lists them with resources/list
and reads them with resources/read, and a single read may return several
contents, which is what happens when the URI points at a directory1.
Parameterized resources exist as templates, in the RFC 6570 URI template format,
listed under resources/templates/list. Their arguments can be auto-completed
through the completion API, which is what lets an interface offer suggestions while
the user types a path.
The specification defines a handful of URI schemes and is opinionated about one of
them. Use https:// only when the client can fetch the resource straight off the
web without going through the MCP server; for anything else, prefer another scheme
or define your own, even when the server itself is the one downloading the content
over the internet1. There are also file:// and git://, and nothing stops you
from defining a scheme of your own.
Each resource can carry annotations: audience, whose values are user and
assistant, priority between 0.0 and 1.0, and lastModified in ISO 8601. They
are hints for the client to filter and prioritize what enters the context, not
rules the protocol enforces.
One error rule prevents a silent bug: a missing resource returns -32602, and the
server must not return an empty contents array for a resource that does not
exist, because that is ambiguous between “exists and is empty” and “does not
exist”1.
Prompts: what the user picks
Prompts are message templates the server exposes and the user invokes. The client
lists them with prompts/list and resolves them with prompts/get, passing the
declared arguments. What comes back is a list of messages with role user or
assistant and content that may be text, image, audio, a resource link or an
embedded resource.
The specification defines prompts as user-controlled and takes care to qualify the phrase: it refers to who decides when the prompt is used, not to who authors its content, which is defined by the server1. That distinction matters for anyone assessing risk. A prompt is not more trustworthy for being “the user’s”; it is server text the user agreed to paste into the conversation.
The typical case is the slash command, and the spec uses that very example. Arguments accept completion through the completion API, and the list can change at runtime, with the server notifying anyone holding an open subscription stream.
A concrete example makes it stick. A repository server exposes a code_review
prompt with one required argument named code. In the editor it shows up as a
command; the user selects a snippet, the client calls prompts/get with that
snippet in the arguments, and gets back an assembled user message carrying the
server’s review instruction with the code embedded. The model has seen none of this
yet: what reaches it is the conversation with the finished message inside,
indistinguishable from something the user typed.
How to decide
The question is who decides when the thing enters the context.
If the answer is the model, mid-task, with parameters it chooses on the spot, it is a tool. If it is the application or the user, before the task, over content with a stable identity, it is a resource. If it is the user deliberately starting a flow with a ready-made shape, it is a prompt.
A secondary test breaks the ties: side effects only exist in tools. If the operation writes somewhere, sends mail or charges someone, it is neither resource nor prompt, because neither one has execution semantics.
Where the split breaks down
Everything becomes a tool. This is the most common failure and the most expensive one. Servers expose reads as tools because tools work in every client and because the model can call them unaided. The cost lands in context: each tool declaration takes room on every call, and selection degrades as options multiply. A 2025 paper measured that degradation in an MCP stress test and reported tool selection accuracy of 43.13% against a 13.62% baseline, cutting prompt tokens by over 50% by retrieving only the relevant tool descriptions through semantic search3. Another paper that year attacked the same problem by organizing tools into a hierarchical taxonomy and filtering by query, with results pointing the same way4. Both numbers come from specific experimental setups rather than a law, but the direction is consistent across the two.
The other two depend on the host implementing them. A resource needs an interface to be picked from; a prompt needs somewhere for commands to appear. A server betting on resources may end up invisible in hosts that only built the tool path. Public evidence here is weak: the available studies measured configuration, SDK usage and oversight controls, not coverage by primitive5. Treat it as a portability risk, not a measured fact.
Descriptions remain untrusted input. True for all three primitives, but worst in tools, because the description is what steers the model’s choice. The 2025 study of 67,057 servers described exactly this path: attacker-controlled tool metadata shapes the model’s reasoning and induces operations the host executes without independent verification6.
“User-controlled” does not mean safe. The prompt’s content belongs to the server. The user controls the moment, and only that. A malicious prompt is injection by invitation.
The wrong error channel destroys recovery. A server that treats a business failure as a protocol error turns a problem the model would have solved into a dead stop. It is a two-line bug with a large effect on observed behaviour.
What each one costs
Order-of-magnitude figures, enough to see the shape of the bill.
A tool costs context permanently. Name, description and argument schema land in the hundreds of tokens per tool, resent on every model call. Thirty thin tools cost far more than five fat ones covering the same ground, and they make selection worse besides.
A resource costs almost nothing until it is read, and then costs the size of its content. It is the tool’s mirror image: expensive per use, cheap to exist. That is why a large catalogue of resources is workable and a large catalogue of tools is not.
A prompt costs the size of the messages it returns, and only when the user invokes
it. The prompts/list entry itself is small.
The practical consequence is a single one: whatever you expose as a tool, you pay for always.
Where to start
- Start with a handful of read-only tools. They work in every client and give the fastest signal on whether the design makes sense.
- Move anything with a natural URI to a resource. Files, documents, records with stable identifiers. That takes weight off the prompt and takes the decision away from the model.
- Write the tool description as if it were the only documentation. It is the one part of the declaration the model reads before deciding.
- Separate the two error channels on day one.
isErrorfor domain failures, JSON-RPC errors for invalid requests. - Return the tool list in the same order every time. It costs one line and improves prompt caching.
- Measure the size of your tool prefix before adding the next one. Once the list is past a dozen, your next problem is selection, not capability.
Step 2 is the one most servers skip, and it is why tool lists grow until the model starts choosing badly.
Footnotes
-
The server primitives page of revision
2026-07-28is the source for the control hierarchy, the application-driven model for resources, the URI schemes, the annotations and the definition of prompts as user-controlled. Verified on 30 July 2026. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
The tools page of the same revision is the source for
inputSchemaandoutputSchema, forstructuredContent, for the two error channels, for the naming rules and for the deterministic ordering recommendation. ↩ ↩2 ↩3 ↩4 -
Gan and Sun (2025) measured, in an MCP stress test, tool selection accuracy of 43.13% against a 13.62% baseline by retrieving only the relevant descriptions through semantic search, cutting prompt tokens by more than 50%. ↩
-
Antonioni et al. (2025) organized MCP tools into a hierarchical taxonomy and filtered by query, reducing prompt size without meaningfully compromising capability and improving selection as the tool count grows. ↩
-
Majeed et al. (2026) characterized 1,723 MCP-consuming applications by configuration, SDK usage and human-oversight mechanisms, without breaking the data down by primitive. ↩
-
Li and Gao (2025) described how attacker-controlled tool metadata shapes the model’s reasoning and induces operations the host executes without independent verification, based on 67,057 servers across six registries. ↩
Frequently asked questions
- What are MCP's primitives?
- Three, all exposed by the server: tools, which the model invokes; resources, which the application attaches; and prompts, which the user picks. The specification calls this a control hierarchy, and it is the key to the split: each primitive has a different actor deciding when it gets used.
- What is the difference between a tool and a resource in MCP?
- A tool is executable and the model invokes it on its own with tools/call, possibly with side effects. A resource is content identified by a URI, read through resources/read at the application's discretion. The practical difference is trigger and cost: a tool occupies context on every call, a resource only when attached.
- When should I use a resource instead of a tool?
- When the content is read-only, has a stable identity, and it makes sense for the user or the application to pick it before the task starts. A file, a database schema, a policy document. If the model needs to fetch it mid-reasoning with parameters it chooses on the spot, that is a tool.
- What is the prompt primitive in MCP?
- A message template the server exposes and the user deliberately invokes, typically as a slash command. The server returns a list of assembled messages from prompts/get with the arguments filled in. The user controls when the prompt is used; the content is still written by the server.
- Can a tool return structured data?
- Yes. The result can carry structuredContent, a JSON value conforming to the outputSchema the tool declared. Servers that declare an outputSchema must return conforming results, and clients should validate them. For backwards compatibility the same serialized JSON usually also arrives as a text block.
- Is a tool error a protocol error?
- No, and conflating the two is expensive. Business failures, input validation and API errors come back in the result with isError set to true, and the client should hand them to the model so it can self-correct. Protocol errors are for unknown tools and malformed requests, which models rarely fix.
References
- Agentic AI Foundation. Model Context Protocol specification, revision 2026-07-28 — Server primitives (2026)
- Agentic AI Foundation. Model Context Protocol specification, revision 2026-07-28 — Tools (2026)
- Gan, T., Sun, Q.. RAG-MCP: Mitigating Prompt Bloat in LLM Tool Selection via Retrieval-Augmented Generation (2025)arXiv:2505.03275
- Antonioni, E. et al.. JSPLIT: A Taxonomy-based Solution for Prompt Bloating in Model Context Protocol (2025)arXiv:2510.14537
- Li, X., Gao, X.. A First Look at the Security Issues in the Model Context Protocol Ecosystem (2025)arXiv:2510.16558
- Majeed, M. H. A., Mahmoud, M., Nadi, S.. An Empirical Study of Model Context Protocol Applications (2026)arXiv:2607.25635