Chapter 4: Exposing Resources


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.


What Are Resources?

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:

The 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.


Defining a Static Resource

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.


Dynamic Resources With URI Templates

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


MIME Types

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:


Binary Resources

For 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.


Listing Resources

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:
    ...

Dynamic Resource Lists

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()

Resources vs. Tools

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.


Example: File System Resource

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.


Key Takeaways


← Chapter 3: Defining Tools Table of Contents Chapter 5: Prompt Templates →