---
title: "MCP Resources: Exposing Read-Only Data Sources to AI Agents"
description: "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."
canonical: https://callsphere.ai/blog/mcp-resources-exposing-read-only-data-sources-ai-agents
category: "Learn Agentic AI"
tags: ["MCP", "Resources", "AI Agents", "Data Sources", "Agentic AI"]
author: "CallSphere Team"
published: 2026-03-17T00:00:00.000Z
updated: 2026-05-07T05:56:23.113Z
---

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

```mermaid
flowchart LR
    HOST(["MCP host
Claude Desktop or IDE"])
    CLIENT["MCP client"]
    subgraph SERVERS["MCP Servers"]
        S1["Filesystem server"]
        S2["GitHub server"]
        S3["Postgres server"]
        SX["Custom tool server"]
    end
    LLM["LLM session"]
    OUT(["Grounded action"])
    HOST  CLIENT
    CLIENT |stdio or HTTP+SSE| S1
    CLIENT  S2
    CLIENT  S3
    CLIENT  SX
    CLIENT --> LLM --> OUT
    style HOST fill:#f1f5f9,stroke:#64748b,color:#0f172a
    style CLIENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style OUT fill:#059669,stroke:#047857,color:#fff
```

```python
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:

```python
@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.

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

```python
@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:

```python
@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

---

Source: https://callsphere.ai/blog/mcp-resources-exposing-read-only-data-sources-ai-agents
