Tools, Resources & Prompts
The three primitives an MCP server can expose — Tools, Resources, and Prompts — and who controls each one.
An MCP server is a connector: it advertises capabilities to a host, validates inputs, and executes the real work. But it can only expose three kinds of things. Learn these three primitives and you understand almost everything a server can do.
The three things a server can expose
The most important distinction isn't what each returns — it's who controls when it fires:
- Tools are model-controlled — the LLM decides when to call them.
- Resources are app-controlled — the host app pulls them in as context.
- Prompts are user-controlled — a person triggers them (think slash-commands).
Which of these are MCP server 'primitives'?
#Tools — model-controlled actions
Tools are actions the model can invoke — run a query, send an email, create a file. Like POST endpoints with a schema. The agent decides when to call a tool; your code decides what it does. Every tool declares an input schema so the model knows how to call it correctly. Common examples: "query_sales", "send_slack_message".
// 2. Expose a TOOL. The agent decides *when* to call it;
// your code decides *what* it does. The agent never sees the DB directly.
server.registerTool(
"query_sales",
{
title: "Query sales",
description: "Aggregate revenue by customer or by month.",
inputSchema: {
metric: z.enum(["revenue", "recency"]),
groupBy: z.enum(["customer", "month"]).optional(),
limit: z.number().int().max(50).default(5),
},
},
async ({ groupBy, limit }) => {
const sql =
groupBy === "month"
? `SELECT strftime('%Y-%m', created_at) AS month,
SUM(amount) AS revenue
FROM orders GROUP BY month ORDER BY month LIMIT ?`
: `SELECT c.name AS customer, SUM(o.amount) AS total
FROM orders o JOIN customers c ON c.id = o.customer_id
GROUP BY c.name ORDER BY total DESC LIMIT ?`;
const rows = db.prepare(sql).all(limit);
return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
}
);The schema is the contract
The inputSchema (here written with Zod) is what the model reads to figure out how to call the tool. You declare the shape; the SDK handles discovery, validation, and transport. The function body is ordinary code — that's where your logic lives.
#Resources — app-controlled context
Resources are read-only data the model can pull in as context — a schema, a document, a config file. Like GET endpoints. They're app-controlled: the host application decides what context to attach, rather than the model triggering a side effect. Each resource is addressed by a URI. Common examples: "schema://shop", "file:///report.pdf".
// 3. Expose a RESOURCE — read-only context the agent can pull in.
server.registerResource(
"schema",
"schema://shop",
{ title: "Database schema", mimeType: "text/plain" },
async (uri) => ({
contents: [{ uri: uri.href, text: "orders(id, customer_id, amount, created_at)" }],
})
);Tools do, Resources describe
A quick gut-check: if it changes something or runs work, it's a Tool. If it's just read-only data you'd want as context, it's a Resource. The sales connector exposes both — a query_sales tool and a schema://shop resource that tells the model what columns exist to reason over.
#Prompts — user-controlled templates
Prompts are reusable, parameterized templates a user can trigger — a slash-command that pre-fills a rich interaction. They're user-controlled: a person invokes them intentionally, and they can take arguments to fill in the blanks. Common examples: "/summarize-sales", "/review-pr".
{
"prompts": [
{
"name": "summarize-sales",
"description": "Summarize sales for a time window",
"arguments": [
{ "name": "month", "description": "e.g. 2026-06", "required": true }
]
},
{
"name": "review-pr",
"description": "Review an open pull request",
"arguments": [
{ "name": "pr", "description": "PR number or URL", "required": true }
]
}
]
}A user types `/review-pr 482` in their chat app to kick off a code review. Which primitive is that?
"Prompt" here doesn't mean your chat message
In MCP, a Prompt is a named, reusable template the server exposes for the user to trigger — not the free-text message you type into the model. Different concept, same word. When in doubt, ask who controls it: model (Tool), app (Resource), or user (Prompt).
Key takeaways
- An MCP server exposes exactly three primitives: Tools, Resources, and Prompts.
- Tools are model-controlled actions (like POST) — the model decides when to call; your code decides what happens.
- Resources are app-controlled, read-only context (like GET), addressed by a URI such as `schema://shop`.
- Prompts are user-controlled, parameterized templates — slash-commands like `/review-pr` that a person triggers.
- The clearest way to tell them apart is to ask who controls when it fires: model, app, or user.
Actions the model can invoke — it decides when to call them.
query_sales · send_slack_messageThe model wants to run work: aggregate revenue by customer. Which primitive does it reach for, and how does that call travel on the wire?
// The model decides to aggregate revenue by customer.
// It has these three primitives available from the server:
// - Tool: query_sales
// - Resource: schema://shop
// - Prompt: /summarize-salesComplete the Node SDK call that exposes a read-only database schema as context the model can pull in.
server.( "schema", "schema://shop", { title: "Database schema", mimeType: "text/plain" }, async (uri) => ({ contents: [{ uri: uri.href, text: "orders(id, customer_id, amount, created_at)" }], }) );
This connector is supposed to let the model SEND a Slack message. Something about how it's exposed is wrong. What's the fix?
server.registerResource(
"send_slack_message",
"slack://send",
{ title: "Send a Slack message" },
async () => {
await slack.postMessage(channel, text);
return { contents: [{ uri: "slack://send", text: "sent" }] };
}
);Put the JSON-RPC exchange in the order it happens when a model uses the query_sales tool.
← Server runs the query and returns { "result": { "content": [...] } }→ Client asks what tools exist: { "method": "tools/list" }← Server advertises query_sales with its input schema
→ Model calls the tool: { "method": "tools/call", "params": { "name": "query_sales", "arguments": { ... } } }You're designing an MCP connector for a project-management tool (think Jira or Linear). Sketch one of each primitive for it:
- A Tool — name it and give it a one-line description plus 1-2 input fields.
- A Resource — give it a URI and say what read-only context it returns.
- A Prompt — name the slash-command and its argument(s).
For each, state who controls it (model / app / user) and one sentence justifying why it belongs in that category rather than the other two.
Try it yourself — a starting point to build on:
# Write your solution here