MCP Resources vs Tools: The Production Server Rule
Use resources for client-controlled context, tools for model-invoked actions, and prompts for reusable user-selected workflows.

Use MCP resources for context the client should choose, cache, and read; use MCP tools for actions the model may invoke; use MCP prompts for reusable workflows the user should deliberately select. The production mistake is exposing every read path as a tool, because it turns harmless context into model-controlled action surface.
Verdict: Resources carry context, tools take action
Resources are nouns, tools are verbs, and prompts are saved workflows. That rule is simple enough to remember, but the production line is sharper: a resource is context the host application controls, a tool is an operation the model may request, and a prompt is a user-selected template that packages a repeatable interaction.
The current MCP resources spec says resources let servers share data that provides context to language models, including files, database schemas, and application-specific information, and each resource is identified by a URI. Resources are also application-driven: the host decides how to select, search, cache, or insert that context.
The current MCP tools spec says tools let language models interact with external systems, including databases, APIs, and computations. Tools are model-controlled: the model can discover and invoke them based on the user prompt and conversation context. That makes them powerful, but it also makes them the part of an MCP server that needs approvals, validation, rate limits, and audit logs.
Prompts are the third primitive. The prompts spec describes them as structured messages and instructions exposed by the server and selected by the user. Use prompts when the workflow is recognizable enough to package as a command, not when you need to smuggle a hidden approval decision into model behavior.
The broader MCP vs function calling decision is about integration architecture. This decision is inside the MCP server: which primitive should represent each capability.


Production comparison
The control owner matters more than the API shape. A database read can be a resource or a tool, but the safer default is resource when the application can bound the URI, enforce permissions, and decide when to load it.
The common shortcut is to expose everything as a tool because early MCP clients have strong tool support. That works for demos and becomes expensive in production. Every extra tool adds schema surface, model choice surface, and security review surface. A read-only customer profile, repository file, schema, or run snapshot does not need to become a model-invoked operation just because the framework makes tools convenient.
Use this first-pass rule:
Use resources when the server is exposing nouns
Resources should represent context a client can browse, search, select, subscribe to, or cache. The resources capability can optionally advertise subscribe and listChanged, so a server can tell clients whether individual resources or the resource list can change over time.
The clean production resource has four properties:
- It has a stable URI scheme that matches your permission model.
- It is safe to read after authorization without a model deciding to "call" it.
- It has metadata the client can show before insertion.
- It can be cached or invalidated without changing business state.
A repository MCP server is a good example. The current service map, OpenAPI schema, CLAUDE.md, migration history, and last CI run are all resources. They are context. The model may need them, but the host application should control how they enter the context window.
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {
"cursor": "optional-cursor-value"
}
}Then the client reads the selected resource:
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "repo://payments-service/openapi"
}
}For parameterized context, use a resource template instead of a generic "fetch anything" tool:
{
"uriTemplate": "customer://{customer_id}/support-history",
"name": "customer_support_history",
"title": "Customer Support History",
"description": "Read the support timeline visible to the current user",
"mimeType": "application/json"
}That design gives the host a clear permission checkpoint. The server validates the URI, resolves the user, checks whether the user can read that customer record, returns bounded content, and logs the resource read. The model receives context, but it does not get a model-controlled function that can probe arbitrary customer IDs.
Define the URI scheme around permissions
Use schemes such as
repo://,customer://,incident://, orrun://only when each URI can be authorized directly. A resource URI is part of your access-control surface, not just a label.Expose templates for bounded lookup
Use
resources/templates/listfor safe parameterized reads. Keep the template narrow enough that a reviewer can tell which records it can expose.Log reads as data access
Record the resource URI, user identity, client identity, permission decision, cache status, and response metadata. For sensitive data, avoid logging raw content.
Use subscriptions only where freshness matters
Advertise
subscribefor resources such as incident status or run state. Do not add subscriptions to static docs just because the capability exists.
The resources spec is explicit about security: servers must validate all resource URIs, sensitive resources should have access controls, binary data must be encoded correctly, and resource permissions should be checked before operations. Those are not optional production niceties. They are the difference between a useful context server and a quiet data-exposure path.
Use tools when the model needs to do work
Tools should represent operations the model may request and the client can approve. The tools spec calls tools model-controlled, and it also says there should always be a human in the loop with the ability to deny tool invocations for trust, safety, and security.
That human-denial line is the practical difference. If the model asks to call create_invoice, merge_pull_request, disable_user, or run_data_export, the client should show the name, inputs, target system, and expected effect before execution. A production MCP client should treat the model request as a proposal, not as permission.
The minimum tool definition is not just a function name. A tool needs a clear name, a description, a valid JSON Schema input object, and usually an output shape the client can validate.
{
"name": "create_release_pr",
"title": "Create Release Pull Request",
"description": "Create a release pull request for a selected service and version.",
"inputSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"service": { "type": "string" },
"version": { "type": "string" },
"base_branch": { "type": "string" }
},
"required": ["service", "version", "base_branch"]
},
"outputSchema": {
"type": "object",
"properties": {
"pull_request_url": { "type": "string" },
"status": { "type": "string" }
},
"required": ["pull_request_url", "status"]
}
}The current tools spec says inputSchema must be a valid JSON Schema object, and it defaults to JSON Schema 2020-12 when no schema field is present. It also gives naming guidance: tool names should be between 1 and 128 characters, should be case-sensitive, should use ASCII letters, digits, underscore, hyphen, and dot, should not contain spaces or commas, and should be unique within a server.
Those constraints look mechanical, but they force a useful design habit. A production tool name should read like an operation you are willing to audit:
- Good:
github.pr.create,incident.status.update,rag.eval.run - Weak:
doThing,query,admin,run
The server-side gate should be boring and strict:
type ToolAuditEvent = {
tool: string;
clientId: string;
userId: string;
approvedBy: string | null;
argumentsHash: string;
target: string;
result: "success" | "denied" | "error" | "timeout";
};Do not trust annotations from arbitrary servers. The tools spec says clients must consider tool annotations untrusted unless they come from trusted servers. A malicious or sloppy server can mark a dangerous operation as harmless. Your client policy should come from the server trust tier, the user's grants, and the operation's actual effect.
Tool results should also be validated. Structured tool content is returned in structuredContent, and the spec says a tool returning structured content should also return serialized JSON in a text content block for backwards compatibility. Use that structure to keep downstream model turns predictable: return typed fields, clear errors, and resource links when the result creates new context the client can read.
MCP security best practices for production servers go deeper on approvals and tool boundaries. The short version inside this comparison is stricter: if an operation can mutate state, spend money, send data, create records, or trigger external work, make it a tool and put it behind validation, approval, rate limits, timeouts, and audit logs.
Use prompts for repeatable user-selected workflows
Prompts are not weaker tools. They are user-controlled workflow templates. The prompts spec says prompts let servers provide structured messages and instructions, and they are exposed with the intention that a user explicitly selects them.
That makes prompts a good fit for workflows such as:
- "Review this PR against the release checklist."
- "Summarize this incident for the status page owner."
- "Prepare a migration plan from these resources."
- "Generate a customer handoff using the selected account resources."
The protocol surface is intentionally simple. Clients list available prompts:
{
"jsonrpc": "2.0",
"id": 1,
"method": "prompts/list",
"params": {
"cursor": "optional-cursor-value"
}
}Then the client retrieves a selected prompt with arguments:
{
"jsonrpc": "2.0",
"id": 2,
"method": "prompts/get",
"params": {
"name": "release_review",
"arguments": {
"service": "payments-service"
}
}
}The production value is discoverability. A user can invoke a prompt as a known workflow instead of asking the model to improvise the process. The server can include required arguments, embed selected resources, and standardize the review format.
The safety risk is prompt injection and hidden access. The prompts spec says implementations must carefully validate all prompt inputs and outputs to prevent injection attacks or unauthorized access to resources. A prompt should not cause private resources to appear unless the client selected them or the server authorized them for that user. It also should not ask the model to call tools without the same approval policy those tools would require in a normal conversation.
Treat prompts as product surface. Version them, name them clearly, show the user what they include, and log which prompt was selected. When a prompt regularly calls the same resource and tool sequence, the sequence should still remain visible in the trace.
A production MCP server usually needs all three
A serious MCP server is not "a list of tools." It is a control layer where resources, tools, and prompts have separate jobs and separate audit paths.
Take a repository operations server for an engineering team:
The server can now support a useful agent loop without collapsing every capability into one risky namespace.
Model the context graph first
List the records, files, schemas, dashboards, eval runs, and incident timelines the model needs. If the item is a noun with a permission check, make it a resource or resource template.
Promote only real operations to tools
Create tools for state changes, queued work, external API calls, expensive computations, or model-chosen parameterized operations. Keep each tool small enough to validate and approve.
Package repeated paths as prompts
When users repeatedly ask for the same workflow, expose a prompt. The prompt should make the required resources and possible tool calls visible, not hide them.
Add the control plane before rollout
Require token audience checks, per-client authorization, tool approvals, timeouts, rate limits, and audit logs before the MCP server touches production systems.
The token boundary matters. The MCP security best practices page says token passthrough is forbidden and MCP servers must not accept tokens that were not explicitly issued for the MCP server. That rule applies whether the operation is a resource read or a tool call. If the server is a proxy to GitHub, Linear, Salesforce, Snowflake, or an internal API, it still needs to know which client and user are making the request and which scopes were granted.
For an internal MCP rollout, the trace should answer these questions without reading chat transcripts:
- Which client connected?
- Which user identity was resolved?
- Which resources were listed or read?
- Which prompts were selected and with what arguments?
- Which tools were requested, approved, denied, timed out, or failed?
- Which downstream system saw the request?
- Which result resource or artifact was produced?
That is the same production posture that separates an MCP server from a local demo. You need enough trace detail to debug bad retrieval, rejected approvals, slow tools, and accidental data exposure.
The decision rule that prevents tool sprawl
The durable rule is resource-first, tool-second, prompt-last. Start with resources because production systems mostly need governed context. Add tools only where the model needs to request an operation. Add prompts where the user should select a repeatable workflow.
Use this review before adding any new MCP capability:
The best MCP servers are smaller than teams expect. They expose the context that helps the model reason, the actions the model can request safely, and the workflows users actually repeat. Everything else stays behind ordinary application code until it has a clear control owner.
MCP authorization for production servers covers the consent and token side. The primitive choice is the design side. Get both right and the MCP server becomes a production integration layer instead of a pile of model-callable endpoints.
Are MCP resources just read-only tools?
No. Resources are application-driven context identified by URIs, discovered with resources/list, and read with resources/read. Tools are model-controlled operations discovered with tools/list and invoked with tools/call.
Should database reads be MCP resources or MCP tools?
Use resources for bounded, permissioned reads the client can select, cache, or subscribe to. Use tools when the model needs to choose arguments for a computation or operation that cannot be represented as a stable resource URI.
Where do MCP prompts fit with tools and resources?
Prompts package reusable user-selected workflows. They can include arguments and embedded resources, but they should not bypass resource permissions or tool approvals.
Can a tool return a resource?
Yes. Tool results may include resource links or embedded resources, but that does not change the safety model of the tool call. The original operation still needs validation, approval where sensitive, and audit logging.
Scope Your MCP Server
Design and ship a custom MCP server with clean resource, tool, prompt, authorization, approval, and observability boundaries.
Jun 10, 2026




