Resources represent data that the AI model can read. Where tools are about taking actions, resources are about exposing information. This chapter covers how to define static resources, dynamic resources with URI templates, and how to choose between resources and tools.
Resources are identified by URIs and return content when read. They are the MCP equivalent of a REST GET endpoint — they return data without side effects.
Examples of what you might expose as resources:
config://app/settingsdb://users/42file:///var/log/app/metrics://server/cpudocs://handbook/onboardingThe AI model (or the host application) can list available resources and read them by URI. Resources appear in the model’s context when the user or the host explicitly includes them.
Use @mcp.resource() with a fixed URI:
@mcp.resource("config://app/settings")
def get_settings() -> str:
"""Return the current application settings as JSON."""
import json
settings = {
"version": "2.1.0",
"environment": "production",
"max_connections": 100,
}
return json.dumps(settings, indent=2)
The URI is the identifier clients use to request this resource. The docstring describes what the resource contains. The function’s return value is the resource content.
Use URI templates to expose parameterized resources. Template parameters use {curly_brace} syntax:
@mcp.resource("db://users/{user_id}")
def get_user(user_id: str) -> str:
"""Return user profile data for the given user ID."""
user = database.get_user(user_id)
if user is None:
raise ValueError(f"User {user_id} not found")
return user.to_json()
When a client requests db://users/42, FastMCP matches the template and calls get_user(user_id="42").
Full example: code/03_resources_example.py
Specify a MIME type when the content is not plain text:
@mcp.resource("reports://monthly/{month}", mime_type="text/csv")
def get_monthly_report(month: str) -> str:
"""Return the monthly sales report as CSV."""
return generate_csv_report(month)
Common MIME types for MCP resources:
text/plain (default)text/markdowntext/csvapplication/jsontext/htmlFor binary content (images, PDFs), return bytes and specify the MIME type:
from mcp.types import BlobResourceContents
@mcp.resource("assets://logo", mime_type="image/png")
def get_logo() -> bytes:
"""Return the company logo as PNG."""
with open("assets/logo.png", "rb") as f:
return f.read()
FastMCP automatically base64-encodes binary content for transport.
When a client calls resources/list, FastMCP returns all registered resources. For template-based resources, only the template is listed (not every possible instantiation).
You can give resources a human-readable name for display in client UIs:
@mcp.resource(
"db://users/{user_id}",
name="User Profile",
description="Full profile data for a user",
)
def get_user(user_id: str) -> str:
...
Sometimes the set of available resources changes at runtime (e.g., the files in a directory). Use list_resources to control what appears in the listing:
from mcp.server.fastmcp import FastMCP
from mcp.types import Resource
@mcp.list_resources()
async def list_log_files() -> list[Resource]:
"""List all available log files."""
import os
logs = []
for filename in os.listdir("/var/log/app"):
if filename.endswith(".log"):
logs.append(Resource(
uri=f"file:///var/log/app/{filename}",
name=filename,
mimeType="text/plain",
))
return logs
@mcp.read_resource()
async def read_log_file(uri: str) -> str:
"""Read a log file by URI."""
path = uri.replace("file://", "")
with open(path) as f:
return f.read()
Choosing between a resource and a tool:
| Aspect | Resource | Tool |
|---|---|---|
| Side effects | None — read only | Allowed |
| Identified by | URI | Name |
| Invocation | Client reads by URI | AI model calls explicitly |
| Best for | Exposing data | Taking actions |
If the operation changes state, use a tool. If it only reads data, a resource is more appropriate.
There is one important nuance: the AI model calls tools autonomously during generation, but resources are typically included by the host or user explicitly. If you want the AI to automatically fetch data mid-conversation, a tool is often more practical.
A common pattern is exposing parts of a file system:
import os
@mcp.resource("file://{path}")
def read_file(path: str) -> str:
"""Read a file from the allowed directory."""
safe_root = "/home/user/workspace"
full_path = os.path.join(safe_root, path.lstrip("/"))
# Security: ensure path doesn't escape the root
if not os.path.abspath(full_path).startswith(safe_root):
raise ValueError("Access denied: path outside workspace")
with open(full_path) as f:
return f.read()
Always validate resource URIs before reading from the filesystem. The path traversal check shown above is essential.
@mcp.resource("uri://template/{param}") for dynamic resources@mcp.list_resources() and @mcp.read_resource() for runtime-dynamic resource sets| ← Chapter 3: Defining Tools | Table of Contents | Chapter 5: Prompt Templates → |