Exposing Resources and Prompts, Not Just Tools, from an MCP Server
Tools are actions. Many agent workflows also need readable context and reusable templates.
MCP models those as resources (URI-addressable data) and prompts (named message templates with arguments).
Summary
Keep your tools, then declare resources for files or API snapshots agents should read, and prompts for multi-step task templates hosts can insert. List, read, and get each primitive through the SDK; document URIs and prompt args as carefully as tool schemas.
Recipe
- Inventory what is currently stuffed into tool return strings (configs, docs, large JSON).
- Promote stable, readable payloads to resources with clear URIs (
scheme://path). - Identify repeated user-facing task setups and encode them as prompts with typed args.
- Register list/read (resources) and list/get (prompts) handlers via FastMCP or the TS SDK.
- Advertise capabilities so clients know resources/prompts exist (SDKs usually set this when you register).
- Test: list resources → read one URI; list prompts → get one with sample args.
- Teach host users when to attach a resource vs call a tool (read vs side effect).
- Version URIs and prompt names; avoid silent renames that break saved workflows.
Working Example
Python FastMCP sketch
# verify decorator APIs at build for your mcp package version
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("catalog-server")
DOCS = {
"intro": "# Product intro\nWelcome to the catalog API.",
"limits": "# Rate limits\n100 req/min per key.",
}
@mcp.resource("docs://{slug}")
def read_doc(slug: str) -> str:
"""Return a markdown doc by slug."""
return DOCS.get(slug, f"Unknown doc: {slug}")
@mcp.prompt()
def review_change(service: str, risk: str = "medium") -> str:
"""Template for reviewing a service change."""
return (
f"Review the proposed change for `{service}`.\n"
f"Risk level: {risk}.\n"
"List regressions, rollout steps, and monitors to watch."
)
@mcp.tool()
def list_doc_slugs() -> str:
"""List documentation slugs available as resources."""
return ", ".join(sorted(DOCS))
if __name__ == "__main__":
mcp.run(transport="stdio")TypeScript sketch
// Patterns - verify registerResource / registerPrompt names at build
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "catalog-server", version: "1.0.0" });
server.registerTool(
"list_doc_slugs",
{ description: "List documentation slugs", inputSchema: {} },
async () => ({ content: [{ type: "text", text: "intro, limits" }] }),
);
// Resources and prompts: use the SDK helpers for your version, e.g.
// server.resource(...) / server.registerResource(...)
// server.prompt(...) / server.registerPrompt(...)
// Return URI, mimeType, and text/blob contents per schema.Prefer checking the current SDK reference for exact resource / prompt registration helpers - names evolve slightly across releases.
Deep Dive
Tools vs resources vs prompts
| Primitive | Primary question | Side effects | Typical consumer |
|---|---|---|---|
| Tool | "Do something" | Often yes | Model tool call |
| Resource | "Read this URI" | Should be read-only | Host/context attach |
| Prompt | "Start from this template" | No (template only) | Host slash-commands / UI |
Mixing them poorly creates confusion: a "read_file" tool that also mutates state, or a prompt that secretly calls APIs.
Resource design
- Use a URI scheme you own (
docs://,config://,ticket://). - Prefer idempotent reads.
- Large binaries may use blob content types; text/markdown is simplest for LLMs.
- Resource templates (parameters in the URI) scale better than listing millions of static entries.
Prompt design
- Arguments should be few and typed.
- Body should be the instructions you would paste into chat yourself.
- Do not embed secrets in prompt text.
- Prompts are not a substitute for server-side authorization.
Host behavior differs
Some hosts auto-fetch resources; others only list them until a user or agent requests a read.
Some hosts surface prompts as slash commands; others ignore them.
Document your primitives for the hosts you officially support.
Combining primitives
Pattern: prompt sets the task, resource supplies policy docs, tool performs the change.
That split keeps token use lower than stuffing everything into one mega-tool result.
Gotchas
- Read tools that mutate - violates user mental model; split write tools.
- Huge resource bodies - still burn context; consider summaries + spill files.
- Unlisted URIs - if hosts only show listed resources, undocumented URIs never appear.
- Prompt vs system prompt - MCP prompts do not replace your agent system policy.
- Capability flags wrong - client never lists resources if initialize capabilities omit them.
- URI encoding bugs - special characters in path segments break reads.
- Stale caches - hosts may cache lists; send list-changed notifications when supported.
Alternatives
| Approach | When it wins | When it loses |
|---|---|---|
| Tools only | Simple action APIs | Large context payloads |
| Tools + resources | Context on demand | Hosts without resource UI |
| Host-side RAG only | Centralized retrieval | Per-server data locality |
| Static files in repo | Offline coding agents | Live multi-tenant data |
FAQs
Must every server expose all three primitives?
No. Tools alone are common. Add resources and prompts when they remove friction for your hosts.
Can a tool return a resource link instead of full text?
Yes when the SDK and host support resource references in tool results. That is a good pattern for large outputs.
How do I secure resources?
Apply the same auth as tools on remote servers. A readable resource can leak customer data as easily as a tool.
Are resources cached by MCP itself?
The protocol does not give you a global cache. Hosts may cache; your server should treat each read as authoritative unless you document otherwise.
Should prompts include few-shot examples?
Short examples help. Long few-shots may belong in a resource the prompt points to.
How do I test prompts without a UI host?
Use an MCP client harness to call the prompts list/get methods and assert the rendered messages.
Can prompts accept free-form JSON?
Prefer structured args. Free-form blobs recreate the problem prompts were meant to solve.
What mime types should I use?
text/plain and text/markdown for LLM-facing docs; more specific types when hosts render non-text.
Related
Related: Anatomy of an MCP Server: Handlers, Schemas, and Transport
Related: Building Your First MCP Server in TypeScript or Python
Related: Handling Large Tool Outputs: Auto-Spill to Sandbox Files
Related: MCP's Three Primitives: Tools, Resources, and Prompts
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.