Most MCP servers ship only tools, so every time a client needs a config file, a schema, or a document, someone writes a get_x tool for it. Resources exist precisely so you stop doing that: they are read-only data a server exposes by URI, and the client or model pulls them in as context. The one-line version is that you register a function against a URI (@mcp.resource("config://app") in Python, server.registerResource(...) in TypeScript) and the SDK answers the resources/list and resources/read calls for you. This guide walks the whole path, including URI templates and the errors that make resources silently not show up.
Key Takeaways
- Short answer: decorate a function with
@mcp.resource("uri")in Python, or callserver.registerResource()in TypeScript; the SDK declares theresourcescapability and servesresources/listandresources/readautomatically. - Resources are read-only context the host pulls in; tools are actions the model invokes. Use a resource for data, a tool for side effects.
- There are two shapes: direct resources with a fixed URI, and resource templates using RFC 6570 URI templates like
users://{userId}/profile. - Return text through the
textfield and binary through a base64blobfield, and always setmimeTypeso the client renders it correctly. - Host support varies. Some clients show resources in a picker, others surface only tools, so verify with the MCP Inspector before blaming your code.
What You Need Before You Start
A working MCP server and a runtime. For the Python path, Python 3.10 or newer and the official mcp SDK (the one that bundles FastMCP). For the TypeScript path, Node.js 18+ and @modelcontextprotocol/sdk. If you have not stood up a server at all yet, build the skeleton first with the FastMCP server guide, then come back to add resources to it.
You also want a way to inspect the server. The MCP Inspector is the fastest, and a scripted client works too if you already have one.
Expose a Resource From Your Server, Step by Step
The procedure below gets you from an empty server to a client reading both a static resource and a templated one. Finish it end to end and you have a working resources implementation.
- Install the SDK. Pull the official package for your language. Both implement the resources protocol described in the MCP resources specification.
# Python pip install "mcp[cli]" # TypeScript npm install @modelcontextprotocol/sdk zod - Declare a static resource with a fixed URI. The return value becomes the resource content, and the type hint plus
mime_typetell the client how to read it. This is the whole thing in Python.from mcp.server.fastmcp import FastMCP mcp = FastMCP("config-server") @mcp.resource("config://app-version", mime_type="text/plain") def app_version() -> str: """The running application version.""" return "1.4.2" if __name__ == "__main__": mcp.run() - Add a resource template for parameterized URIs. Put a placeholder in the URI and take it as a function argument. FastMCP registers this as a template, so it answers
resources/templates/listand matches reads against the pattern.import json @mcp.resource("users://{user_id}/profile", mime_type="application/json") def user_profile(user_id: str) -> str: profile = load_profile(user_id) # your lookup return json.dumps(profile) - Do the same in TypeScript if that is your stack.
registerResourcetakes a name, a URI string or aResourceTemplate, a metadata object, and the read callback. The callback returns acontentsarray.import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; const server = new McpServer({ name: "config-server", version: "1.0.0" }); server.registerResource( "app-version", "config://app-version", { title: "App Version", description: "Running app version", mimeType: "text/plain" }, async (uri) => ({ contents: [{ uri: uri.href, text: "1.4.2" }] }) ); server.registerResource( "user-profile", new ResourceTemplate("users://{userId}/profile", { list: undefined }), { title: "User Profile", description: "Profile by user id" }, async (uri, { userId }) => ({ contents: [{ uri: uri.href, text: JSON.stringify(loadProfile(userId)) }], }) ); - Return binary content the right way. Text goes in the
textfield; binary goes in a base64-encodedblobfield, never intext. In FastMCP, returnbytesand the SDK emits ablob; set the matchingmime_type.@mcp.resource("assets://logo.png", mime_type="image/png") def logo() -> bytes: with open("logo.png", "rb") as f: return f.read() - Run the server and read the resources back. Start it, then point the Inspector at it (or a client) and call
resources/listfollowed byresources/readwith a concrete URI such asusers://42/profile.
If you want to script the reads instead of clicking, the Python MCP client guide covers the same session setup; swap the tool calls for# stdio server, inspected locally npx @modelcontextprotocol/inspector python server.pysession.list_resources()andsession.read_resource(uri).
How Resources Differ From Tools, and When to Reach for Each
The split is about who is in control and whether anything changes. A tool is a function the model decides to call, and it can have side effects. A resource is data the application reads; the spec calls resources application-driven, meaning the host decides how and when to pull them into context, sometimes through a picker the user clicks.
So a database schema, a README, a config blob, or a rendered report belongs in a resource. "Create a ticket" or "send the email" belongs in a tool. If you find yourself writing a read-only tool whose only job is to return a document, that is the signal to make it a resource instead.
Direct Resources vs Resource Templates
A direct resource has one fixed URI and shows up in resources/list. A resource template carries a URI template following RFC 6570, so one registration covers a family of URIs like users://{userId}/profile. Templates are advertised through resources/templates/list, and clients can offer completion for the arguments.
The table below is the quick decision guide across the shapes and where each is discovered.
| Shape | URI example | Discovery method | Use it for |
|---|---|---|---|
| Direct resource | config://app-version | resources/list | A single known document or value |
| Resource template | users://{userId}/profile | resources/templates/list | A parameterized family of resources |
| Web resource | https://example.com/report.pdf | Either, client fetches directly | Content the client can load itself |
One field is easy to miss: the metadata object supports name, title, description, mimeType, and size. Setting title and description is what makes your resources legible in a host UI, so fill them in even though they are optional. The Python SDK and the TypeScript SDK both expose these as decorator or method arguments, and FastMCP will infer a default mimeType when you omit it, per the FastMCP resources docs.
Capability Negotiation Happens for You
A server that supports resources must declare the resources capability during initialization, optionally with subscribe and listChanged flags. When you register at least one resource, FastMCP and the TypeScript McpServer emit that capability automatically. You only touch it by hand if you drop down to the low-level Server API, in which case you must declare the capability yourself or the client will never call resources/list.
Troubleshooting the Errors That Actually Happen
These are the failures that eat an afternoon, and the fix for each.
The Resources Never Show Up in the Host
First check whether the host renders resources at all. Several clients surface only tools today, which is a UI gap, not a bug in your server. Confirm the server side with the MCP Inspector: if resources/list returns your entries there, the server is correct and the host is the limitation. If the Inspector shows nothing either, you likely registered on the low-level Server without declaring the resources capability.
A Template Resource Is Missing From resources/list
This is by design. Template instances are enumerated through resources/templates/list, not resources/list. In the TypeScript SDK, new ResourceTemplate(uri, { list: undefined }) explicitly says "do not enumerate concrete instances." If you want the individual URIs to appear in the flat list, pass a list callback that returns them.
resources/read Returns -32602 (or -32002) Resource Not Found
The URI you asked for did not match any registered resource or template pattern exactly, including scheme and slashes. Read config://app-version, not config:/app-version. For templates, the read URI must fit the template, so users://42/profile matches users://{userId}/profile but users://42 does not. Note the spec now standardizes on -32602 for a missing resource while telling clients to still accept the older -32002.
Binary Content Renders as Garbage
Two usual causes. You put base64 text in the text field instead of the blob field, or the mimeType is wrong. Binary must be base64-encoded in blob with a matching type such as image/png. In FastMCP, returning bytes handles the encoding for you.
Path Traversal on file:// Resources
If a template maps a URL parameter onto a filesystem path, sanitize it. The spec is explicit that servers must validate resource URIs and prevent directory traversal, so reject or normalize .. segments before opening anything, and keep reads inside an allowed root.
What to Do Next
Once list and read work, add the notifications your data justifies. Declare listChanged and emit notifications/resources/list_changed when the set of resources changes, and declare subscribe if clients should get notifications/resources/updated for a specific URI. The resources spec details both flows and the message shapes.
From there, add reusable prompts alongside your resources, or wire argument completion for your templates through the completion API. If you are serving over HTTP rather than stdio, mind that resource URIs and any file access run in a shared process, so the access-control and sanitization notes above matter more, not less.