06 · Resources: Exposing Data to an LLM¶
Resources vs. tools: the real distinction¶
It's tempting to think "a resource is just a tool that returns data," but the distinction that actually matters is about control, not shape:
- A tool is called when the model decides, mid-conversation, that calling it will help answer the user. The model chooses the arguments.
- A resource is typically read because the application or user explicitly attached it — picked a file from a list, pasted a link, or the host proactively loaded it as context before the conversation even reaches the model.
A file's contents, a database record, a config value — things that are simply there to be read, with no side effects and no decision-making required — are resources. An action that changes something, searches with arbitrary model-chosen parameters, or has side effects belongs as a tool.
Exposing a resource¶
The @mcp.resource() decorator takes a URI and registers a function to
handle it. This lesson covers static URIs — fixed, exact strings.
Templated URIs with variable segments (notes://{note_id}, for example) are
a Level 2 topic, once you've got the fixed case solid.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo-server")
@mcp.resource("config://app-settings")
def get_app_settings() -> str:
"""The current application settings, as JSON."""
return '{"theme": "dark", "notifications": true}'
The URI's scheme (config:// here) is entirely up to you — MCP doesn't
mandate a fixed set of schemes. Pick something that describes what the
resource actually is: file://, config://, notes://, whatever reads
clearly for your server.
Multiple resources¶
Just like tools, a server can expose as many resources as it needs:
@mcp.resource("docs://readme")
def get_readme() -> str:
"""The project's README file."""
with open("README.md") as f:
return f.read()
@mcp.resource("docs://changelog")
def get_changelog() -> str:
"""The project's changelog."""
with open("CHANGELOG.md") as f:
return f.read()
A client calling resources/list gets back both URIs with their
descriptions, and can then resources/read whichever one it actually needs
— mirroring the tools/list → tools/call pattern from lesson 5, but for
reading instead of acting.
When you genuinely need both¶
Nothing stops a server from offering a resource and a tool that overlap in subject matter, as long as each is doing the job it's suited for:
@mcp.resource("orders://all")
def all_orders() -> list[dict]:
"""The full current order list, as structured records."""
return load_all_orders()
@mcp.tool()
def search_orders(status: str) -> list[dict]:
"""Search orders by status (e.g. 'shipped', 'pending')."""
return [o for o in load_all_orders() if o["status"] == status]
The resource is the whole dataset, available for a client to load as
context whenever it wants. The tool is a model-driven, parameterized query
against that same data — the model decides what status to search for
based on what the user actually asked.
Exercise¶
Add a resource stats://summary to a server of your own that returns a
short string summarizing some data you already have lying around (a word
count of a file, the number of lines in a log, anything). Confirm in the
Inspector that it shows up under resources/list and that reading it
returns the right content.