Skip to content
Books

Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information. Each resource is uniquely identified by a URI.

Resources follow an application-driven model, where host applications determine how to incorporate context based on their needs. This contrasts with tools, which are model-controlled — the language model can discover and invoke tools automatically based on its contextual understanding and user prompts. Ultimately, users select which resources to include in the context through the application interface, giving them direct control over the available data.

Resource specification

The MCP server can support, based on the official specification, these 5 messages:

  • Resource listing: Is used to discover available resources by calling resources/list request. The operation supports pagination.
  • Resource reading: Allows the client to read content of a resource with resources/read request.
  • Resource templates: Same as resource reading, with the difference of allowing parameters to be passed to the resource
  • Resource change: Notify client about changes in resource listing by sending a notification to the client. The operation is only available when the resources capability listChanged is enabled.
  • Subscribe: Allows clients to subscribe to specific resource changes
Flow diagram

Create resources with FastMCP

With FastMCP, creating a resource is similar to creating a new tool. We use the annotations to register a resource. And FastMCP also takes care of the specification (resource listing, resource call, etc), you only need to define the logic for the resource, and the rest happens out of the box with FastMCP.

Let's change the main.py file and move the tools to tools.py. This would result in the main.py to look like this:

from fastmcp import FastMCP
from resources import register_resources
from tools import register_tools

# STDIO by default
mcp = FastMCP(name="Demo 🚀")

# Register all resources
register_resources(mcp)

# Register all tools
register_tools(mcp)

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

And now let's add 3 resources:

  1. greetingResource resource: Normal resource
  2. greetingResourceTemplate resource: The "same" resource as a template
  3. weatherResource resource: The resource calls an API

As you can see all resources are ready only. Please note that resources requires the URI tobe unique. All of the examples follow this to a T.

greetingResource

A simple, non-parameterized resource that returns a fixed greeting. Use this form when the response is static or deterministic and no input is needed from the caller.

  • Purpose: expose stable, read-only data via a single, well-known URI (hello://world).
  • Invocation: call resources/read with the exact URI; there are no template parameters to fill.
  • Behavior: read-only and idempotent — safe for repeated calls and easy to cache.
def register_resources(mcp: FastMCP):
    """Register all resources with the MCP instance"""
    
    @mcp.resource(
        uri="hello://world",
        name="greetingResource",
        title="Greeting Resource",
        description="A resource for getting greetings.",
        mime_type="text/plain",
        meta={"version": "1.0", "team": "infrastructure"},
        annotations=Annotations(readOnlyHint=True, idempotentHint=True),
    )
    def greetingResource() -> str:
    """Get a personalized greeting

    Note: The description in the resource decorator provides metadata about the resource already. Same as with tools.

    """

    return "Hello, World!"

greetingResourceTemplate

A compact, parameterized resource whose URI includes placeholders (for example {name}). Use this when callers need simple, read-only, personalized data.

  • Purpose: expose parameterized, read-only content via a URI template (e.g., greetings://{name}).
  • Invocation: call resources/read with the resolved URI (for example greetings://Alice) — the server extracts name and returns the greeting.
  • Behavior: idempotent and safe to cache; validate and sanitize parameters to avoid unexpected input.
  • Quick usage note: LLMs should supply the final URI when requesting the resource (no separate parameter payload required).
    @mcp.resource(
        uri="greetings://{name}",
        name="greetingResourceTemplate",
        title="Greeting Resource Template",
        description="A resource template for getting personalized greetings.",
        mime_type="text/plain",
        meta={"version": "1.0", "team": "infrastructure"},
        annotations=Annotations(readOnlyHint=True, idempotentHint=True),
    )
    def greetingResourceTemplate(name: str) -> str:
    """Get a personalized greeting

    Note: The description in the resource decorator provides metadata about the resource already. Same as with tools.

    """

    return f"Hello, {name}!"

weatherResource

A network-backed, parameterized resource that fetches current weather for a given city and country. It demonstrates calling external APIs, handling common errors, and returning structured JSON suitable for LLM consumption.

  • Purpose: provide current weather via a URI template (weather://{city}/{country}/current) with mime_type: application/json.
  • Invocation: call resources/read with a resolved URI (for example weather://Berlin/DE/current); the server geocodes the location, queries the weather API, and returns the result.
  • Behavior: read-only and idempotent; returns structured data (here represented by WeatherResponse) and maps timeouts, HTTP errors, and missing locations to clear validation errors.
  • Best practices: validate/sanitize inputs, add timeouts and retries, consider caching and rate-limiting, and avoid leaking API keys or sensitive data in responses.
@mcp.resource(
    uri="weather://{city}/{country}/current",
    name="weatherResource",
    title="Weather Resource",
    description="A resource for fetching current weather data.",
    mime_type="application/json",
    meta={"version": "1.0", "team": "infrastructure"},
    annotations=Annotations(readOnlyHint=True, idempotentHint=True),
)
def weatherResource(city: str, country: str) -> dict:
    """Fetch current weather data for a given city using a public API.

    Note: The description in the resource decorator provides metadata about the resource already. Same as with tools.

    """

    try:
        # Get coordinates for the city
        geocode_url = (
            f"https://geocoding-api.open-meteo.com/v1/search?name={city}&country={country}&count=1"
        )
        response = requests.get(geocode_url, timeout=10)
        response.raise_for_status()
        data = response.json()

        if not data.get("results"):
            raise ValueError("Location not found.")

        latitude = data["results"][0]["latitude"]
        longitude = data["results"][0]["longitude"]

        # Get weather data
        weather_url = (
            f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m"
        )
        weather_response = requests.get(weather_url, timeout=10)
        weather_response.raise_for_status()

        weather_data = weather_response.json()
        return WeatherResponse(**weather_data)

    except requests.exceptions.Timeout:
        raise ValueError("Request timed out. Please try again later.")
    except requests.exceptions.ConnectionError:
        raise ValueError("Unable to connect to weather service. Please check your internet connection.")
    except requests.exceptions.HTTPError as e:
        raise ValueError(f"HTTP error occurred: {e.response.status_code}")
    except requests.exceptions.RequestException as e:
        raise ValueError(f"An error occurred while fetching weather data: {str(e)}")
    except KeyError as e:
        raise ValueError(f"Unexpected response format from weather service: missing {str(e)}")
    except Exception as e:
        raise ValueError(f"An unexpected error occurred: {str(e)}")

Error handling

The same principles as with tools apply for resources: the error can be on the JSON RPC level or on resource execution.

JSON schema

The JSON schema also include resrouces in the JSON object and it contains metadata as well about the resource. Please note that resources and resource templates are divided. The JSON schema has these 4 JSON properties for an MCP server:

  • tools
  • prompts
  • resources
  • templates

Generated schema for resources

The schema for the 3 created resources is as follows:

greetingResource resource presented as JSON (click to expand)
{
  "key": "hello://world",
  "uri": "hello://world",
  "name": "greetingResource",
  "description": "A resource for getting greetings.",
  "mime_type": "text/plain",
  "annotations": {
    "audience": null,
    "priority": null,
    "readOnlyHint": true,
    "idempotentHint": true
  },
  "tags": null,
  "enabled": true,
  "title": "Greeting Resource",
  "icons": null,
  "meta": {
    "version": "1.0",
    "team": "infrastructure"
  }
}
greetingResourceTemplate resource template presented as JSON (click to expand)
{
  "key": "greetings://{name}",
  "uri_template": "greetings://{name}",
  "name": "greetingResourceTemplate",
  "description": "A resource template for getting personalized greetings.",
  "mime_type": "text/plain",
  "parameters": {
    "properties": {
      "name": {
        "title": "Name",
        "type": "string"
      }
    },
    "required": [
      "name"
    ],
    "type": "object"
  },
  "annotations": {
    "audience": null,
    "priority": null,
    "readOnlyHint": true,
    "idempotentHint": true
  },
  "tags": null,
  "enabled": true,
  "title": "Greeting Resource Template",
  "icons": null,
  "meta": {
    "version": "1.0",
    "team": "infrastructure"
  }
}
greetingResourceTemplate resource template presented as JSON (click to expand)
{
  "key": "weather://{city}/{country}/current",
  "uri_template": "weather://{city}/{country}/current",
  "name": "weatherResource",
  "description": "A resource for fetching current weather data.",
  "mime_type": "application/json",
  "parameters": {
    "properties": {
      "city": {
        "title": "City",
        "type": "string"
      },
      "country": {
        "title": "Country",
        "type": "string"
      }
    },
    "required": [
      "city",
      "country"
    ],
    "type": "object"
  },
  "annotations": {
    "audience": null,
    "priority": null,
    "readOnlyHint": true,
    "idempotentHint": true
  },
  "tags": null,
  "enabled": true,
  "title": "Weather Resource",
  "icons": null,
  "meta": {
    "version": "1.0",
    "team": "infrastructure"
  }
}

Conclusion

Resources give your MCP server a simple, predictable way to expose read-only context to LLMs and client applications. They let you surface stable data (fixed URIs) and parameterized data (URI templates) without introducing side effects, which keeps LLM interactions safe, cacheable, and easy to validate.

Key takeaways:

  • Use non-parameterized resources for static, deterministic data and template resources when callers need personalized or contextual values.
  • Always declare mime types and metadata, validate and sanitize template parameters, and annotate resources with readOnly/idempotent hints.
  • Network-backed resources (like weather) should include timeouts, clear error mapping, caching, and rate-limiting to avoid surprises.

What's next

In the next post I'll cover prompts in the MCP server: how prompts interact with resources and tools, recommended prompt templates, and patterns for letting the LLM decide when to call resources versus returning text directly.

How would you apply resources? Where do you see the benefit using them compared to tools?

Published by...

Image of the author

Jernej Klancic

Visit author page