Skip to content
Learn Agentic AI13 min read0 views

MCP Resources: Exposing Read-Only Data Sources to AI Agents

Learn how MCP resources differ from tools, how to define resource URIs and templates, expose read-only data to AI agents with proper content types, and implement pagination for large datasets.

Resources vs Tools: A Critical Distinction

MCP defines two primary ways to expose data to AI agents: tools and resources. Tools are functions the agent calls to perform actions — they take input, do something, and return output. Resources are read-only data sources the agent can access without executing any logic.

Think of it this way: a tool is like calling an API endpoint with parameters. A resource is like reading a file from a known path. Tools have side effects and dynamic behavior. Resources are static or semi-static data that the agent can pull into its context.

This distinction matters for agent design. When an agent needs to read a configuration file, fetch documentation, or retrieve system status, a resource is more appropriate than a tool. Resources are explicitly read-only, which means the agent runtime can prefetch them, cache them, and include them in the prompt without worrying about side effects.

Defining Resources in Python

In FastMCP, resources are defined with the @mcp_server.resource() decorator. Each resource has a URI that the agent uses to request it:

from mcp.server.fastmcp import FastMCP

mcp_server = FastMCP(name="DataServer")

@mcp_server.resource("config://app/settings")
async def get_app_settings() -> str:
    """Return the current application configuration as JSON."""
    import json
    settings = {
        "version": "2.1.0",
        "environment": "production",
        "max_connections": 100,
        "features": {
            "caching": True,
            "rate_limiting": True,
            "audit_logging": True,
        },
    }
    return json.dumps(settings, indent=2)


@mcp_server.resource("docs://api/endpoints")
async def get_api_docs() -> str:
    """Return API endpoint documentation."""
    return """
    GET  /users          - List all users (paginated)
    GET  /users/{id}     - Get user by ID
    POST /users          - Create a new user
    PUT  /users/{id}     - Update user
    DELETE /users/{id}   - Delete user
    GET  /health         - Health check endpoint
    """

The URI scheme is flexible — you can use config://, docs://, data://, or any custom scheme that makes semantic sense for your domain. The agent sees these URIs during resource discovery and can request any of them.

Resource Templates for Dynamic Data

Resource templates use URI patterns with placeholders, allowing agents to request data for specific entities:

@mcp_server.resource("users://{user_id}/profile")
async def get_user_profile(user_id: str) -> str:
    """Retrieve a user profile by ID."""
    import json
    import aiosqlite

    async with aiosqlite.connect("app.db") as db:
        db.row_factory = aiosqlite.Row
        cursor = await db.execute(
            "SELECT id, name, email, role, created_at "
            "FROM users WHERE id = ?",
            [user_id],
        )
        row = await cursor.fetchone()

        if not row:
            return json.dumps({"error": "User not found"})

        return json.dumps(dict(row), indent=2, default=str)


@mcp_server.resource("metrics://{service_name}/summary")
async def get_service_metrics(service_name: str) -> str:
    """Get current performance metrics for a service."""
    import json
    import random

    # In production, pull from Prometheus or your metrics store
    metrics = {
        "service": service_name,
        "uptime_seconds": 86400 * 7,
        "requests_per_minute": random.randint(100, 500),
        "error_rate_percent": round(random.uniform(0.1, 2.0), 2),
        "p99_latency_ms": random.randint(50, 200),
    }
    return json.dumps(metrics, indent=2)

When the agent calls resources/read with URI users://42/profile, FastMCP extracts 42 as the user_id parameter and invokes the function.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

Content Types and Binary Data

Resources can return different content types. Text resources are the most common, but MCP also supports binary content encoded as base64:

@mcp_server.resource(
    "reports://daily/summary",
    mime_type="application/json",
)
async def get_daily_report() -> str:
    """Return today's summary report as structured JSON."""
    import json
    from datetime import date

    report = {
        "date": str(date.today()),
        "total_orders": 1247,
        "revenue": 89450.00,
        "top_products": [
            {"name": "Widget A", "units": 342},
            {"name": "Widget B", "units": 218},
        ],
    }
    return json.dumps(report, indent=2)

The mime_type parameter tells the agent what kind of data to expect. For JSON data, use application/json. For plain text, use text/plain. For markdown documentation, use text/markdown.

Pagination for Large Resources

When a resource returns a large dataset, implement pagination using URI query parameters or template parameters:

@mcp_server.resource("logs://app/recent/{page}")
async def get_recent_logs(page: str) -> str:
    """Get recent application logs, paginated by page number."""
    import json
    import aiosqlite

    page_num = int(page)
    page_size = 50
    offset = (page_num - 1) * page_size

    async with aiosqlite.connect("app.db") as db:
        cursor = await db.execute(
            "SELECT timestamp, level, message FROM logs "
            "ORDER BY timestamp DESC LIMIT ? OFFSET ?",
            [page_size, offset],
        )
        rows = await cursor.fetchall()

        count_cursor = await db.execute("SELECT COUNT(*) FROM logs")
        total = (await count_cursor.fetchone())[0]

        return json.dumps({
            "page": page_num,
            "page_size": page_size,
            "total_records": total,
            "total_pages": (total + page_size - 1) // page_size,
            "logs": [
                {"timestamp": r[0], "level": r[1], "message": r[2]}
                for r in rows
            ],
        }, indent=2)

The agent can iterate through pages by incrementing the page parameter in the URI template. Include total counts and page metadata so the agent knows when it has retrieved everything.

FAQ

When should I use a resource instead of a tool?

Use a resource when the data is read-only and does not require complex input parameters. Resources are ideal for configuration, documentation, status information, and reference data. Use a tool when the operation requires multiple parameters, has side effects, or involves computation beyond simple data retrieval.

Can resources be updated or are they always static?

Resources are read-only from the agent's perspective — there is no resources/write method in MCP. However, the data backing a resource can change over time (like metrics or logs). MCP supports resource subscriptions via resources/subscribe, where the server notifies the client when a resource changes so the agent can re-read it.

How does the agent discover available resources?

The agent calls resources/list to get a list of all available resources with their URIs, names, descriptions, and MIME types. For resource templates, the agent calls resources/templates/list to discover parameterized URI patterns it can fill in with specific values.


#MCP #Resources #AIAgents #DataSources #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.