Building MCP Servers

Exposing Resources

5 min read

Tools let the model act. Resources let it read. Files, database rows, config — anything you're willing to hand over without letting it be changed.

Resource vs Tool

The line between them is not "is this data or an action." A tool can return pure data and still be a tool. The real question is who initiates, and whether anything can change as a result.

Which one should this be?

Model-initiated

Make it a Tool

Who starts itThe model, mid-conversation
Takes argumentsYes — a JSON Schema you define
Can change stateYes
Discoverytools/list
Pros
  • The model can decide it needs this, unprompted
  • Arguments let one tool cover many cases
Cons
  • Every tool is a new way for the model to be wrong at your expense
  • Needs a careful description to be called at the right time
User-attached

Make it a Resource

Who starts itThe user or host attaches it
Takes argumentsNo — the URI is the address
Can change stateNo, by contract
Discoveryresources/list
Pros
  • Safe to expose broadly — nothing can be damaged
  • Cacheable, because a URI means the same thing each time
Cons
  • The model cannot reach for it on its own
  • A large resource can swallow the context window

The practical test: if you'd want a human to approve it before it runs, it's a tool. If you'd be comfortable with it happening silently a hundred times, it's a resource.

Defining Resources

Resources use URIs to identify content:

from mcp.types import Resource

@server.list_resources()
async def list_resources():
    return [
        Resource(
            uri="config://app/settings",
            name="Application Settings",
            description="Current application configuration",
            mimeType="application/json"
        ),
        Resource(
            uri="file:///var/log/app.log",
            name="Application Logs",
            description="Recent application log entries",
            mimeType="text/plain"
        )
    ]

URI Schemes

You can use any URI scheme that makes sense for your data:

SchemeUse CaseExample
file://Local filesfile:///home/user/doc.txt
db://Database recordsdb://users/123
config://Configurationconfig://app/settings
api://External APIsapi://weather/london

Reading Resources

Implement the read handler to return content:

from mcp.types import TextContent, BlobContent

@server.read_resource()
async def read_resource(uri: str):
    if uri == "config://app/settings":
        settings = load_app_settings()
        return [TextContent(
            type="text",
            text=json.dumps(settings, indent=2)
        )]

    if uri.startswith("file://"):
        path = uri.replace("file://", "")
        content = read_file(path)
        return [TextContent(type="text", text=content)]

    raise ValueError(f"Unknown resource: {uri}")

Binary Resources

For binary data like images, use BlobContent:

import base64

@server.read_resource()
async def read_resource(uri: str):
    if uri.startswith("image://"):
        image_data = load_image(uri)
        return [BlobContent(
            type="blob",
            data=base64.b64encode(image_data).decode(),
            mimeType="image/png"
        )]

Dynamic Resources

Resources can be generated dynamically based on parameters:

@server.list_resources()
async def list_resources():
    # Generate resources from database
    users = await db.get_all_users()
    return [
        Resource(
            uri=f"user://{user.id}",
            name=f"User: {user.name}",
            description=f"Profile for {user.name}"
        )
        for user in users
    ]

Next: error handling, and why the model is an unusual audience for it. :::

Quiz

Module 2 Quiz: Building MCP Servers

Take Quiz
Was this lesson helpful?

Sign in to rate