Exposing Readable Data as MCP Resources
A resource is how an MCP server hands over data for a client to read, addressed by a URI rather than invoked like a function.
Files, database rows, configuration values, and API responses are all natural fits for resources once you think of them as things a client can fetch, not actions a model performs.
Summary
Resources in the Python MCP SDK are defined with the @mcp.resource() decorator, given a URI or a URI template as its argument.
A static resource has a fixed URI, like config://settings, while a templated resource uses placeholders in curly braces, like notes://{name}, that become function parameters.
Because a resource represents a read, it should never mutate state or trigger a side effect; that distinction is what separates a resource from a tool.
Returning a sensible MIME type alongside the content helps the client decide how to render or parse what it gets back, whether that is plain text, JSON, or an image.
Resources pair naturally with tools: a tool can create or update something, and a resource can expose the result for later reading.
Recipe
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("resource-recipes")
@mcp.resource("config://settings")
def get_settings() -> str:
"""Return the current server configuration as JSON."""
return '{"theme": "dark", "max_results": 20}'
@mcp.resource("notes://{name}")
def get_note(name: str) -> str:
"""Return the contents of a named note."""
return f"Contents of note '{name}'"When to reach for this:
- The data is meant to be read, not acted on, such as a file, a config value, or a database row.
- You want the client to be able to list and fetch the data without the model having to construct a tool call.
- The same piece of data might be attached as context directly by the application, not only fetched on the model's request.
- You want a stable, addressable identifier (a URI) for a specific piece of data that can be referenced consistently.
Working Example
# server.py
import json
import sqlite3
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("docs-resources")
DB_PATH = Path(__file__).parent / "articles.db"
DOCS_DIR = Path(__file__).parent / "docs"
def _get_db() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
@mcp.resource("articles://{article_id}")
def get_article(article_id: str) -> str:
"""Return a single article row as JSON, addressed by its id."""
conn = _get_db()
try:
row = conn.execute(
"SELECT id, title, body FROM articles WHERE id = ?", (article_id,)
).fetchone()
if row is None:
raise ValueError(f"No article found with id {article_id}")
return json.dumps(dict(row))
finally:
conn.close()
@mcp.resource("docs://{filename}")
def get_doc_file(filename: str) -> str:
"""Return the raw text of a markdown file from the docs directory."""
path = (DOCS_DIR / filename).resolve()
if DOCS_DIR.resolve() not in path.parents:
raise ValueError("filename must stay within the docs directory")
if not path.exists():
raise FileNotFoundError(f"No such file: {filename}")
return path.read_text(encoding="utf-8")
@mcp.resource("stats://article-count")
def get_article_count() -> str:
"""Return the total number of articles as a static summary resource."""
conn = _get_db()
try:
count = conn.execute("SELECT COUNT(*) FROM articles").fetchone()[0]
return json.dumps({"article_count": count})
finally:
conn.close()
if __name__ == "__main__":
mcp.run(transport="stdio")What this demonstrates:
- A templated resource (
articles://{article_id}) reading a single row from a real database connection. - A templated resource (
docs://{filename}) serving files from disk, with a path traversal guard before touching the filesystem. - A static resource (
stats://article-count) with no parameters, useful for summary or aggregate data. - Consistent use of
json.dumpsso structured data comes back in a predictable, parseable shape. - Raising exceptions for a missing row or missing file, exactly like a tool would, since resources can still fail.
Deep Dive
How It Works
@mcp.resource("scheme://path/{param}")registers a URI template; the SDK extracts{param}segments and passes them as function arguments when a matching URI is read.- A client discovers resources with
list_resources()(for concrete URIs) orlist_resource_templates()(for templated ones), then reads a specific URI withread_resource(uri). - Unlike a tool call, a resource read has no separate "arguments" object; every parameter comes from the URI itself, which keeps resources addressable and cacheable.
- The scheme portion of the URI (
config://,notes://,docs://) is arbitrary and chosen by you; it is not a real network protocol, just a namespace. - The SDK infers a MIME type from the return value where it can, but you can be explicit for anything other than plain text.
Choosing a URI Scheme
| Scheme Style | Example | Best For |
|---|---|---|
| Custom namespace | config://settings | Server-specific concepts with no filesystem or network equivalent |
file://-style | file:///reports/q1.csv | Data that genuinely maps to a real file path |
| Domain-style | articles://{id}, users://{id} | Rows or records from a database, one scheme per entity type |
| Aggregate/static | stats://summary | Fixed, parameter-free summaries or counts |
Python Notes
# Guard against path traversal whenever a resource reads from disk using a
# user-supplied path segment.
path = (DOCS_DIR / filename).resolve()
if DOCS_DIR.resolve() not in path.parents:
raise ValueError("filename must stay within the docs directory")Parameters & Return Values
| Element | Type | Description |
|---|---|---|
| URI template | str | Passed to @mcp.resource(); {param} segments become function parameters |
| function parameters | str (typically) | Extracted from the matching URI segments at read time |
| return value | str (or bytes for binary content) | The resource's content, returned to the client on read_resource |
Gotchas
- Treating a resource as an action. A resource named
send_email://{to}implies a side effect and misleads clients that assume resources are safe to read repeatedly. Fix: move anything with a side effect into a tool. - No path traversal guard on filesystem-backed resources. A URI like
docs://../../etc/passwdcan escape the intended directory if you build a path naively. Fix: resolve the path and confirm it stays inside the allowed root before reading. - Returning unstructured text for what is really structured data. Callers then have to parse ad hoc formats instead of JSON. Fix: return
json.dumps(...)for anything with more than one field. - Opening a database connection per resource without closing it. Under load this leaks connections and eventually exhausts the pool. Fix: use a
try/finally(as shown above) or a connection context manager. - Overloading one resource template to also accept optional query-style parameters. URI templates in MCP are path-based, not query-string based, so this gets awkward and inconsistent. Fix: use separate resources or a tool if the data-selection logic gets complex enough to need optional filters.
- Silently returning an empty string when a lookup misses. A caller cannot tell "no data" from "an error occurred." Fix: raise an exception with a clear message when the requested resource genuinely does not exist.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| MCP Tool | The operation performs an action or computation with a result | The data is a plain read with no side effects |
| MCP Prompt | You are packaging instructions, not data | The client needs to fetch or reference actual content |
| A tool that returns the same data | You want the model to explicitly request the data as part of a reasoning step | You want the client to attach or cache the data as ambient context |
| Direct database/API access outside MCP | Only one internal application will ever need this data | You want the same data addressable from Claude Code, Claude Desktop, and other clients |
FAQs
What is the difference between a resource and a tool that just returns data?
- A resource is addressed by a stable URI and is meant to be read, listed, and potentially cached.
- A tool call is initiated by the model as part of reasoning through a task and does not carry the same addressability guarantee.
Can a resource URI have more than one parameter?
Yes, a template like orgs://{org_id}/users/{user_id} extracts both segments as separate function parameters, as long as each is delimited by its own path segment.
Should resources ever have side effects?
No. Resources are meant to be safely readable, potentially more than once, without changing anything. Anything that creates, updates, or deletes data belongs in a tool instead.
How do I return binary content, like an image, from a resource?
Return bytes from the handler instead of str, and the SDK will encode it appropriately for the client, typically along with an explicit MIME type describing the binary format.
What happens if a client requests a resource URI with no matching handler?
The server returns an error indicating the resource does not exist. This is why exact URI scheme and template design matters, since a typo in the scheme means no handler matches.
Can I list all available resources without knowing their exact URIs?
Yes. list_resources() and list_resource_templates() let a client discover what a server offers, including templated resources with their {param} placeholders shown, before reading any specific URI.
Is the URI scheme (like config:// or articles://) a real protocol?
No, it is just a namespace you choose. MCP does not require these to be resolvable network URIs; they only need to be unique and meaningful within your server.
How should I handle a resource that fails to find its data?
Raise an exception with a specific message, exactly as you would in a tool handler, rather than returning an empty string or None that hides the failure.
Can a resource depend on database state that changes frequently?
Yes, resources can be as dynamic as any handler function; each read executes fresh, so a database-backed resource always reflects current data unless you add your own caching layer.
Should file-backed resources trust the filename parameter directly?
No. Always resolve the resulting path and verify it stays within the intended root directory before reading, to prevent path traversal into files outside the served directory.
What MIME type should I use for a JSON resource?
application/json is the conventional choice, and returning content via json.dumps(...) alongside that MIME type gives clients a predictable, parseable payload.
Can one server mix static and templated resources?
Yes, a single FastMCP server can register any number of both static (config://settings) and templated (articles://{id}) resources side by side.
Related
- Understanding MCP: Tools, Resources, and Prompts - the conceptual distinction between resources, tools, and prompts.
- MCP Core Concepts Basics - a minimal stdio server and client to build resources on top of.
- Defining Callable Tools in an MCP Server - the sibling primitive for actions and computations.
- MCP Tools vs Resources vs Prompts Comparison - deciding which primitive fits a given capability.
- MCP Core Concepts Best Practices - a checklist for resource URI design.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the current Model Context Protocol specification. Model names, SDK versions, and the MCP spec move quickly - verify current specifics at platform.claude.com/docs and modelcontextprotocol.io before relying on them.