Home Blog Contact
Home/Blog/How to Expose Resources From an MCP Server to…
How toLLM EngineeringMCPMCP resourcescontext engineering

How to Expose Resources From an MCP Server to Clients

9 min readBy Miloš Mitrović

Most MCP servers ship only tools, so every time a client needs a config file, a schema, or a document, someone writes a get_x tool for it. Resources exist precisely so you stop doing that: they are read-only data a server exposes by URI, and the client or model pulls them in as context. The one-line version is that you register a function against a URI (@mcp.resource("config://app") in Python, server.registerResource(...) in TypeScript) and the SDK answers the resources/list and resources/read calls for you. This guide walks the whole path, including URI templates and the errors that make resources silently not show up.

Key Takeaways

  • Short answer: decorate a function with @mcp.resource("uri") in Python, or call server.registerResource() in TypeScript; the SDK declares the resources capability and serves resources/list and resources/read automatically.
  • Resources are read-only context the host pulls in; tools are actions the model invokes. Use a resource for data, a tool for side effects.
  • There are two shapes: direct resources with a fixed URI, and resource templates using RFC 6570 URI templates like users://{userId}/profile.
  • Return text through the text field and binary through a base64 blob field, and always set mimeType so the client renders it correctly.
  • Host support varies. Some clients show resources in a picker, others surface only tools, so verify with the MCP Inspector before blaming your code.

What You Need Before You Start

A working MCP server and a runtime. For the Python path, Python 3.10 or newer and the official mcp SDK (the one that bundles FastMCP). For the TypeScript path, Node.js 18+ and @modelcontextprotocol/sdk. If you have not stood up a server at all yet, build the skeleton first with the FastMCP server guide, then come back to add resources to it.

You also want a way to inspect the server. The MCP Inspector is the fastest, and a scripted client works too if you already have one.

Expose a Resource From Your Server, Step by Step

The procedure below gets you from an empty server to a client reading both a static resource and a templated one. Finish it end to end and you have a working resources implementation.

  1. Install the SDK. Pull the official package for your language. Both implement the resources protocol described in the MCP resources specification.
    # Python
    pip install "mcp[cli]"
    
    # TypeScript
    npm install @modelcontextprotocol/sdk zod
  2. Declare a static resource with a fixed URI. The return value becomes the resource content, and the type hint plus mime_type tell the client how to read it. This is the whole thing in Python.
    from mcp.server.fastmcp import FastMCP
    
    mcp = FastMCP("config-server")
    
    @mcp.resource("config://app-version", mime_type="text/plain")
    def app_version() -> str:
        """The running application version."""
        return "1.4.2"
    
    if __name__ == "__main__":
        mcp.run()
  3. Add a resource template for parameterized URIs. Put a placeholder in the URI and take it as a function argument. FastMCP registers this as a template, so it answers resources/templates/list and matches reads against the pattern.
    import json
    
    @mcp.resource("users://{user_id}/profile", mime_type="application/json")
    def user_profile(user_id: str) -> str:
        profile = load_profile(user_id)  # your lookup
        return json.dumps(profile)
  4. Do the same in TypeScript if that is your stack. registerResource takes a name, a URI string or a ResourceTemplate, a metadata object, and the read callback. The callback returns a contents array.
    import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
    
    const server = new McpServer({ name: "config-server", version: "1.0.0" });
    
    server.registerResource(
      "app-version",
      "config://app-version",
      { title: "App Version", description: "Running app version", mimeType: "text/plain" },
      async (uri) => ({ contents: [{ uri: uri.href, text: "1.4.2" }] })
    );
    
    server.registerResource(
      "user-profile",
      new ResourceTemplate("users://{userId}/profile", { list: undefined }),
      { title: "User Profile", description: "Profile by user id" },
      async (uri, { userId }) => ({
        contents: [{ uri: uri.href, text: JSON.stringify(loadProfile(userId)) }],
      })
    );
  5. Return binary content the right way. Text goes in the text field; binary goes in a base64-encoded blob field, never in text. In FastMCP, return bytes and the SDK emits a blob; set the matching mime_type.
    @mcp.resource("assets://logo.png", mime_type="image/png")
    def logo() -> bytes:
        with open("logo.png", "rb") as f:
            return f.read()
  6. Run the server and read the resources back. Start it, then point the Inspector at it (or a client) and call resources/list followed by resources/read with a concrete URI such as users://42/profile.
    # stdio server, inspected locally
    npx @modelcontextprotocol/inspector python server.py
    If you want to script the reads instead of clicking, the Python MCP client guide covers the same session setup; swap the tool calls for session.list_resources() and session.read_resource(uri).

How Resources Differ From Tools, and When to Reach for Each

The split is about who is in control and whether anything changes. A tool is a function the model decides to call, and it can have side effects. A resource is data the application reads; the spec calls resources application-driven, meaning the host decides how and when to pull them into context, sometimes through a picker the user clicks.

So a database schema, a README, a config blob, or a rendered report belongs in a resource. "Create a ticket" or "send the email" belongs in a tool. If you find yourself writing a read-only tool whose only job is to return a document, that is the signal to make it a resource instead.

Direct Resources vs Resource Templates

A direct resource has one fixed URI and shows up in resources/list. A resource template carries a URI template following RFC 6570, so one registration covers a family of URIs like users://{userId}/profile. Templates are advertised through resources/templates/list, and clients can offer completion for the arguments.

The table below is the quick decision guide across the shapes and where each is discovered.

ShapeURI exampleDiscovery methodUse it for
Direct resourceconfig://app-versionresources/listA single known document or value
Resource templateusers://{userId}/profileresources/templates/listA parameterized family of resources
Web resourcehttps://example.com/report.pdfEither, client fetches directlyContent the client can load itself

One field is easy to miss: the metadata object supports name, title, description, mimeType, and size. Setting title and description is what makes your resources legible in a host UI, so fill them in even though they are optional. The Python SDK and the TypeScript SDK both expose these as decorator or method arguments, and FastMCP will infer a default mimeType when you omit it, per the FastMCP resources docs.

Capability Negotiation Happens for You

A server that supports resources must declare the resources capability during initialization, optionally with subscribe and listChanged flags. When you register at least one resource, FastMCP and the TypeScript McpServer emit that capability automatically. You only touch it by hand if you drop down to the low-level Server API, in which case you must declare the capability yourself or the client will never call resources/list.

Troubleshooting the Errors That Actually Happen

These are the failures that eat an afternoon, and the fix for each.

The Resources Never Show Up in the Host

First check whether the host renders resources at all. Several clients surface only tools today, which is a UI gap, not a bug in your server. Confirm the server side with the MCP Inspector: if resources/list returns your entries there, the server is correct and the host is the limitation. If the Inspector shows nothing either, you likely registered on the low-level Server without declaring the resources capability.

A Template Resource Is Missing From resources/list

This is by design. Template instances are enumerated through resources/templates/list, not resources/list. In the TypeScript SDK, new ResourceTemplate(uri, { list: undefined }) explicitly says "do not enumerate concrete instances." If you want the individual URIs to appear in the flat list, pass a list callback that returns them.

resources/read Returns -32602 (or -32002) Resource Not Found

The URI you asked for did not match any registered resource or template pattern exactly, including scheme and slashes. Read config://app-version, not config:/app-version. For templates, the read URI must fit the template, so users://42/profile matches users://{userId}/profile but users://42 does not. Note the spec now standardizes on -32602 for a missing resource while telling clients to still accept the older -32002.

Binary Content Renders as Garbage

Two usual causes. You put base64 text in the text field instead of the blob field, or the mimeType is wrong. Binary must be base64-encoded in blob with a matching type such as image/png. In FastMCP, returning bytes handles the encoding for you.

Path Traversal on file:// Resources

If a template maps a URL parameter onto a filesystem path, sanitize it. The spec is explicit that servers must validate resource URIs and prevent directory traversal, so reject or normalize .. segments before opening anything, and keep reads inside an allowed root.

What to Do Next

Once list and read work, add the notifications your data justifies. Declare listChanged and emit notifications/resources/list_changed when the set of resources changes, and declare subscribe if clients should get notifications/resources/updated for a specific URI. The resources spec details both flows and the message shapes.

From there, add reusable prompts alongside your resources, or wire argument completion for your templates through the completion API. If you are serving over HTTP rather than stdio, mind that resource URIs and any file access run in a shared process, so the access-control and sanitization notes above matter more, not less.

Sources

M
Miloš Mitrović
Email Marketing for Ecommerce

Have a question or a project?

Whether it is about this post or a system you want built, I'm happy to talk.

Get in touch

404

Post not found. It may have been moved or the link is incorrect.

← Back to the blog
Summarize with AI
ChatGPT, Perplexity, and Grok open with the prompt ready to run. Claude, Gemini, and Copilot open a chat with the prompt copied; press Ctrl+V (Cmd+V on Mac) to paste. The full text is included, so it works even without web access.