Tools, Resources, Prompts

Article 01: What Is MCP? named the three things a server can expose: tools, resources, and prompts. Naming them is easy. Knowing which one to reach for is where new server authors stall.

Most people build every capability as a tool because that is the one they understand. That works, but it leaves the model doing extra calls to browse data it could have read directly. This article goes primitive by primitive, then gives you a rule for picking.

The three primitives, and who controls each

The primitives differ on one axis that matters: who decides when it runs.

flowchart TB
    subgraph server ["Your MCP server exposes"]
        T["Tools<br/>model-controlled"]
        R["Resources<br/>app-controlled"]
        P["Prompts<br/>user-controlled"]
    end
    MODEL["The model calls tools"]
    APP["The host reads resources"]
    USER["The user picks a prompt"]
    T --> MODEL
    R --> APP
    P --> USER
  • Tools are model-controlled. The model decides to call one, mid-conversation, when it judges the call will help.
  • Resources are application-controlled. The host app reads them, often to hand the model context, and the user or host decides what gets pulled in.
  • Prompts are user-controlled. The user picks one from the host UI, usually a menu or a slash command.

Keep that axis in your head. It is the whole reason three primitives exist instead of one.

Tools: functions the model calls

A tool is a function you expose to the model. In article 03: Your First MCP Server you built one and watched Claude invoke it. Under the hood a tool has three parts that matter: a name, a description, and an input schema.

The input schema is JSON Schema. It describes the arguments the tool accepts, their types, which are required, and any constraints. The client shows this schema to the model so it knows how to build a valid call. Get it wrong and the model sends arguments your handler cannot use.

Here is a tool defined with the Python SDK, with a real schema:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("orders")

@mcp.tool()
def refund_order(order_id: str, amount_cents: int, reason: str) -> str:
    """Issue a refund for an order.

    Use this when a customer asks for money back on a specific order.
    order_id is the ID from the orders list, e.g. "ord_12345".
    amount_cents is the refund amount in cents, and must not exceed
    the order total. reason is a short free-text note for the audit log.
    """
    result = billing.refund(order_id, amount_cents, reason)
    return f"Refunded {amount_cents} cents on {order_id}: {result.status}"

FastMCP reads the type hints (order_id: str, amount_cents: int) and generates the JSON Schema for you. The docstring becomes the description.

The part beginners underestimate: the name and description are not documentation, they are the routing logic. The model never sees your code. It sees the name, the description, and the schema, and from those alone it decides whether this tool fits the request. A tool named refund_order with a clear description gets picked when a user says “give them their money back.” A tool named process described as “processes the thing” gets ignored or misused, no matter how good the code behind it is.

Why does the description matter so much?

The model does tool selection by reading text and predicting what fits. It has no access to your implementation, only the name, description, and schema the client sends it.

So the description does two jobs. It tells the model when to call the tool (“use this when a customer asks for money back”) and how to fill the arguments (“amount_cents must not exceed the order total”). A vague description means the model guesses on both. Treat it like a prompt for a junior engineer who will only ever read that one paragraph.

What a tool returns. A tool returns content, usually a block of text, though images and other types are allowed. That content goes back into the conversation for the model to read, so return something it can act on: a confirmation, the data it asked for, or a clear error string. “Refunded 500 cents on ord_12345: success” is useful. A bare true is not.

Resources: data the model can read

A resource is a piece of data the server exposes for reading, addressed by a URI. Think file:///logs/app.log, db://customers/schema, or config://settings. The scheme is yours to design; the client treats it as an opaque address.

Resources split into two operations:

  • resources/list: the server returns the available resources, each with a URI, a name, and a description. This is browsing. Nothing is read yet.
  • resources/read: the client asks for one URI, the server returns its contents.
flowchart LR
    A["Client asks<br/>resources/list"] --> B["Server returns<br/>URIs + names"]
    B --> C["Host or model<br/>picks a URI"]
    C --> D["Client asks<br/>resources/read"]
    D --> E["Server returns<br/>the contents"]

That two-step shape is the point. The model sees what exists before committing to read any of it, the same way you ls a directory before you cat a file.

When resources beat tools. Reach for a resource when the model needs to browse a space of data before acting, and reading a given item has no side effects. A folder of documents, a set of database tables, the current config, a list of open tickets: all natural resources. The model lists them, decides which is relevant, and reads only that.

You could model the same thing as a read_document(id) tool, and plenty of servers do. The difference is that resources are application-controlled, so the host can surface them in its UI (a file picker, an @-mention menu) and let the user attach one directly, without the model spending a turn guessing IDs. A tool only runs when the model calls it.

If a tool can return data too, why bother with resources?

A tool can return data, and for one-shot lookups it is simpler. The split is about control and browsing.

Resources are addressable and listable, so the host can show the user everything available and let them pick before the model does anything. That turns “the model calls a tool, guesses an ID, maybe gets it wrong” into “the user attaches the exact file they mean.” Resources are also side-effect-free by convention: reading one never changes state. Tools carry no such promise, which is why refund_order must be a tool, not a resource.

Prompts: templates the user picks

A prompt is a reusable message template the server exposes for the user to select. It is not something the model calls, and not something the host reads automatically. The user chooses it, often from a slash-command menu, and it expands into a pre-filled message, sometimes with arguments they fill in. A code-review server might expose a review-diff prompt that drops in a structured “review this diff for bugs, style, and security” message so the user does not retype it every time.

Prompts are the least-used primitive, for a concrete reason: they only exist if the host chooses to show them. A host with no UI for prompts makes yours invisible. Tools and resources have broad host support; prompt support is spottier. So add a prompt when you know your target host surfaces them and you have a genuinely repeatable message. Otherwise it is effort no one will see.

How to pick

The guidance is opinionated on purpose:

  • Default to tools. Most servers are mostly tools. If the model needs to do something, or fetch something on demand, it is a tool.
  • Add resources when the model needs to browse before acting, and reading is side-effect-free. Documents, tables, configs, logs.
  • Add prompts only if your host surfaces them and you have a repeatable message worth saving the user from retyping.

Start with tools, ship, then add the others when a real need shows up. A server that is nothing but well-named tools is a completely good server.

Common beginner mistakes

  • Vague tool names and descriptions: process with “processes the thing” never gets called correctly. Name and describe for the model, not for yourself.
  • A loose or missing input schema: no types, nothing required, so the model sends garbage arguments. Constrain the schema.
  • Everything is a tool: modeling a browsable document set as one big tool when resources would let the user attach the exact item.
  • Side effects hidden in a resource read: reading a resource should never change state. If it does, it is a tool.
  • Returning unusable tool output: a bare true or an empty string. Return text the model can actually act on.
  • Shipping prompts blind: adding prompts for a host that has no UI to show them, so no one ever sees them.

Questions you will face in production

“My tool works when I call it by hand, but the model never picks it. Why?” Almost always the name or description. The model routes on that text alone; it cannot see your code. Rewrite the description to say plainly when the tool should be used and what each argument means, then try the same request again. Treat it like prompt engineering, because it is.

“Should this be a tool or a resource?” Ask two questions. Does it change state, or is it a pure read? Does the model need to browse a set before choosing? A pure read the user might want to browse and attach is a resource. Anything with side effects, or anything the model should invoke on its own judgment, is a tool.

“Are prompts worth implementing at all?” Only if your target host surfaces them and you have a message users would otherwise retype constantly. For most servers, skip prompts and put the effort into good tools and, where they help, resources.

What to remember

  • Three primitives, split by who controls them: tools (model), resources (host or user), prompts (user)
  • A tool is a function the model calls; its name, description, and JSON Schema decide whether the model picks it correctly
  • A tool returns content, usually text, so return something the model can act on
  • A resource is data addressed by a URI, with a list step and a read step, so the model can browse before reading
  • Resources beat tools when the model needs to browse side-effect-free data the user might attach directly
  • Default to tools; add resources for browsing; add prompts only if the host shows them

What to study next

You now know which primitive fits which job. The most common real task is turning an existing REST API into a set of well-shaped tools, which is the next step in article 05: Wrapping an API as an MCP Server. There the naming and schema advice here becomes the difference between a server the model uses and one it ignores.

Further reading

  • MCP spec: Tools. The exact shape of a tool, its input schema, and its content result.
  • MCP spec: Resources. URIs, listing, reading, and subscriptions, defined precisely.
  • MCP spec: Prompts. How prompt templates and their arguments are exposed to hosts.
  • JSON Schema. The schema language MCP tools use to describe their arguments.

Where this article comes from. This is a synthesis of the MCP specification and common practice as of 2026, not a citation of any single paper. The sources above are where the mechanics come from. If you find an error or have a better source for a claim, the article gets fixed within a day, send me a note.


Auto-marks when you reach the end. Click to toggle.

If this helped, buy me a coffee

Everything is free. Tips keep me writing the rest.

Buy me a coffee →