MCP tools vs. resources vs. prompts: when to use each
An MCP server can expose three kinds of capability, and they differ in who decides when they are used. Tools are model-controlled: the model invokes them to perform operations. Resources are application-driven: the host decides what context to pull in. Prompts are user-controlled: a person picks one, usually from a menu.
Pick by control model first. Most confusing MCP servers are servers that expressed everything as tools because tools were the primitive the author understood.
The three primitives at a glance
| Tools | Resources | Prompts | |
|---|---|---|---|
| Who invokes | The model | The host application | The user |
| Purpose | Perform an operation | Supply readable context | Supply a reusable message template |
| Identified by | A name | A URI | A name |
| Core methods | tools/list, tools/call |
resources/list, resources/read |
prompts/list, prompts/get |
| Side effects | Expected and allowed | Should be none | None |
| Typical UI | An approval prompt | A context picker or automatic inclusion | A slash command |
| Cost of getting it wrong | Unapproved actions | Context bloat | Feature nobody discovers |
If MCP itself is new, start with what an MCP server is and come back to this decision once the protocol shape is familiar.
Tools: model-controlled operations
A tool is a function the model can decide to call. That is the whole distinction, and everything else follows from it.
Because the model chooses, the description is the interface. tools/list returns each tool's name,
optional title, description, and JSON Schema for its input, and those fields are all the model has when
deciding. The specification also asks servers to return tools in a deterministic order, so clients can
cache the list reliably and prompt caching stays effective.
Because the model chooses, a human belongs in the loop. The specification is explicit that applications should make clear which tools are exposed, indicate when they are invoked, and offer confirmation so a person can deny an invocation.
Use a tool when the operation:
- changes something outside the conversation;
- needs arguments the model must reason about;
- is worth interrupting a person to approve.
// tools/call
{ "name": "create_invoice", "arguments": { "customer_id": "c_812", "amount_cents": 4900 } }
Two design habits pay for themselves. Annotate behaviour with readOnlyHint, destructiveHint, and
idempotentHint so hosts can auto-approve safe tools and gate dangerous ones. And return actionable
failures as tool results with isError: true rather than protocol errors, because clients pass those
to the model so it can correct itself — an out-of-range date, a malformed identifier, a business-rule
rejection.
Resources: application-driven context
A resource is data identified by a URI that a client can read. Files, database schemas, API responses, configuration, documentation.
Resources are application-driven: the host decides how to incorporate them. It might show a picker so the user selects them, let the user search and filter, or include them automatically by heuristic. The protocol does not mandate any of these.
Use a resource when the thing is:
- content to be read rather than an action to be taken;
- addressable by a stable identifier;
- something a person or an application should be able to choose deliberately.
// resources/read
{ "uri": "file:///project/src/main.rs" }
Three features are easy to miss and change what resources are good for:
Templates. resources/templates/list exposes parameterised resources using RFC 6570 URI
templates, so file:///{path} describes a family of resources rather than a fixed list. Arguments can
be auto-completed through the completion API.
Annotations. A resource can carry an audience (user, assistant, or both), a priority from
0.0 to 1.0, and a lastModified timestamp. Clients use these to filter, prioritise what enters
context, and sort by recency. A server that annotates well gives the host something to reason with
instead of a flat list.
Subscriptions. A server that declares the subscribe capability can deliver
notifications/resources/updated for watched URIs, so a client can re-read content when it changes
rather than polling.
Resources should not have side effects. A resources/read that mutates state is a tool wearing the
wrong label, and it will be read automatically by hosts that include context by heuristic.
Prompts: user-controlled templates
A prompt is a named, parameterised message template. prompts/get returns a list of messages with
roles and content, ready to seed a conversation.
Prompts are user-controlled. That phrase refers to who decides when the prompt is used, not who
authors it — the content is defined by the server. Hosts typically surface them as slash commands, so
people discover them by typing /.
Use a prompt when you are packaging:
- a repeatable way of asking for something, with slots to fill;
- an entry point a person should be able to find and trigger;
- a message sequence that includes reference material the model should see first.
// prompts/get
{ "name": "code_review", "arguments": { "code": "def hello():\n print('world')" } }
A prompt message can carry text, images, audio, a resource_link, or an embedded resource — so a
prompt can pull your documentation or a code sample into the conversation without the model needing a
separate read.
The decision, in order
Ask these in sequence and stop at the first yes.
- Does it change state, or reach an external system that can refuse? → Tool.
- Is it content addressable by a URI that a person or host should be able to select? → Resource.
- Is it a way of starting a task that a person should trigger by name? → Prompt.
If a capability answers yes to two of them, it is usually two capabilities. "Search the knowledge base and return the best article" is a tool. "The article, by URI" is a resource. Shipping both is fine and often right; shipping one that quietly does both is what makes a server hard to reason about.
Four common misuses
Everything is a tool. The most common shape, because tools are the primitive most tutorials start
with. The symptom is a get_document tool whose entire body is a file read. The cost is that hosts
lose the ability to let a user pick context deliberately, and every read passes through the model's
tool-calling budget.
Reference material as a tool result. A get_style_guide tool that returns 8,000 words puts the
whole document into context on a call the model made on a hunch. As a resource, the host decides. As a
reusable agent skill, it may not
need to be in an MCP server at all.
A resource that acts. A resources/read that provisions, sends, or bills. Hosts may read
resources automatically; anything with consequences must be a tool so it can be approved.
A prompt doing a tool's job. A prompt that instructs the model to call a sequence of tools is fine. A prompt that is the only place a required step is documented is a workflow with no enforcement.
Combining them well
The three primitives are strongest together, and the combinations are worth designing on purpose.
| Pattern | How it works |
|---|---|
Tool returns a resource_link |
The tool does the search; the client fetches full content only if needed. Keeps results small. |
Tool embeds a resource |
The result carries the content inline, with no resources/read round trip. Use when the content is small and always needed. |
| Prompt embeds a resource | A /review prompt that arrives with your style guide already attached. |
| Resource plus subscription | Long-lived context the client re-reads when the server says it changed. |
The resource_link pattern in particular is how you keep a search tool from becoming a context
firehose: return identifiers and titles, let the client decide what to open.
Security follows the control model
Each primitive has a different exposure, and the specification treats them differently for good reason.
Tools are the sharp edge. Servers must validate all inputs, implement access controls, rate-limit invocations, and sanitise outputs. Clients should prompt for confirmation on sensitive operations and show tool inputs to the user before the call, which is the defence against accidental or malicious data exfiltration through arguments.
Resources need URI validation and path sanitisation — the specification calls out directory
traversal explicitly for file:// resources — plus permission checks before any read.
Prompts need input and output validation like anything else that composes text a model will follow. A prompt that interpolates untrusted text is an injection vector.
One rule spans all three: the set of tools, resources, or prompts a server returns may vary by the authorization presented on the request, because credentials are per-request input. It must not vary per-connection or as a side effect of other requests. And filtering a list is never access control — enforce authorization in the handler that does the work.
A worksheet for your own server
For each capability you are planning, fill in one row:
| Capability | Who should decide when it runs | Does it change anything | Addressable by URI | Primitive |
|---|---|---|---|---|
| Look up an order | Model, mid-task | No | Yes, orders://{id} |
Tool for search, resource for the record |
| Refund an order | Model, with approval | Yes | No | Tool |
| The refund policy | Host or user | No | Yes | Resource |
| "Draft a refund email" | User | No | No | Prompt |
If the "who decides" column says "the model" for every row, re-read the list. Something in it is context the host should have been allowed to choose.
Next step: Browse MCP servers on LLM Mart and compare how their tools and resources match their stated purpose — then check your own server against the same question before you build the next tool into it.
Sources
Comments (0)
Sign in to join the conversation.
No comments yet.