Tools get most of the attention in MCP because they let an agent do something: run a query, create an issue, fetch a forecast, send a message. Resources solve the quieter problem. They let an agent read the context it needs before it acts.

If you already have a basic MCP server, adding resources is usually the next useful step. A resource can expose a config file, a database schema, a project document, a customer record, or any other read-only data your agent should be able to inspect safely. Think of it as giving the agent a shelf of labeled reference material instead of forcing every read through a custom tool call.

This guide shows how to add resources to a Python FastMCP server, when to use static resources versus templates, and how to test the result locally.

Prerequisites

You should have:

  • Python 3.10 or later
  • uv installed
  • A working MCP server or a new empty project
  • Claude Desktop or the MCP inspector for testing

If you need the first-server walkthrough, start with How to Build Your First MCP Server in 30 Minutes (Python). If you want the concept first, read What Are MCP Resources?.

1. Create a small FastMCP server

Start with a simple project:

mkdir mcp-resource-demo && cd mcp-resource-demo
uv init
uv add "mcp[cli]"

Create server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("resource-demo")


@mcp.tool()
def summarize_project() -> str:
    """Return a short summary of the demo project."""
    return "This project exposes application context as MCP resources."


if __name__ == "__main__":
    mcp.run()

This server has one tool and no resources yet. The tool works, but an agent cannot browse the project context. It can only call the function you gave it.

2. Add a static resource

A static resource has a fixed URI. Use it when the client should be able to list and read a known piece of data, such as a config document or schema description.

Add this above the if __name__ == "__main__" block:

@mcp.resource("app://config")
def app_config() -> str:
    """Return the current application configuration."""
    return """
name: resource-demo
environment: local
features:
  resources: enabled
  tools: enabled
""".strip()

The URI is app://config. The function returns text. The docstring explains the resource to the MCP client.

Now an agent can read the config directly instead of asking a tool to fetch it. That matters because reads and actions should not be mixed unless they have to be. A tool is better for work that needs input, computation, or side effects. A resource is better for data that already exists.

3. Add a resource template

Static resources are useful, but most real systems have too much data to list every item one by one. Resource templates solve that by letting you put variables in the URI.

Add a small in-memory data set:

PROJECT_DOCS = {
    "overview": "The demo server exposes docs through MCP resources.",
    "runbook": "Use uv run mcp dev server.py to test resources locally.",
    "limits": "Resources should be read-only and safe to fetch repeatedly.",
}


@mcp.resource("docs://{doc_id}")
def project_doc(doc_id: str) -> str:
    """Return a project document by ID."""
    if doc_id not in PROJECT_DOCS:
        return f"Document not found: {doc_id}"
    return PROJECT_DOCS[doc_id]

The template URI is docs://{doc_id}. A client can read docs://overview, docs://runbook, or docs://limits without the server declaring each one as a separate function.

This pattern is a good fit for records, documents, table schemas, feature flags, product requirements, and other data where the identifier belongs in the URI.

4. Test resources locally

Run the MCP inspector:

uv run mcp dev server.py

Open the inspector URL it prints. You should see both the tool and the resources. Test these reads:

app://config
docs://overview
docs://runbook

If a resource does not appear, check the decorator URI first. Resource URIs should be stable, descriptive, and scoped to your server. Avoid generic names like data://item unless the server is truly tiny.

You can also run the server directly:

uv run python server.py

It will wait for MCP protocol messages on stdin. That is expected.

5. Decide what belongs in a resource

Use resources for read-only context. Good candidates include:

  • Application config
  • Database schemas
  • Documentation pages
  • Runbooks
  • File contents
  • Search indexes or catalog entries
  • Current status snapshots

Do not use resources for actions. If reading the URI sends an email, updates a record, charges a card, starts a deployment, or writes to disk, it should be a tool with an explicit name and input schema. Agents need clean permission boundaries. Humans do too.

A simple rule works: if you would describe the operation with a noun, it is probably a resource. If you would describe it with a verb, it is probably a tool.

6. Wire resources into Claude Desktop

Install the server into Claude Desktop:

uv run mcp install server.py --name "Resource Demo"

Restart Claude Desktop, then ask a question that requires the exposed context:

Read the resource demo runbook and tell me how to test the server locally.

The client can now pull context from the MCP server before calling any tool. That is the real value. Resources make the server easier for agents to inspect and safer for humans to supervise.

FAQ

Q: Should every MCP server expose resources? A: No. A server that only wraps actions may not need them. But if the server has reference data, schemas, files, docs, or state that helps an agent decide what to do, resources are worth adding.

Q: Can resources return JSON instead of plain text? A: Yes. Return JSON as text and set up your resource so clients can interpret it consistently. Use stable field names. Agents can work with messy text, but structured JSON is easier to inspect and less ambiguous.

Q: Are resources safer than tools? A: Usually, because resources should be read-only. The safety comes from how you implement them, not from the word “resource” itself. Keep reads side-effect free, and put writes behind tools with clear names and approval rules.

Q: How are resource templates different from tool arguments? A: A template identifies data by URI, such as docs://runbook. A tool argument asks the server to perform work, such as search_docs(query="deploy"). Use the template when the agent knows what it wants to read. Use the tool when the server needs to compute or search for the answer.