Building MCP Servers
Exposing Resources
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?
Make it a Tool
- The model can decide it needs this, unprompted
- Arguments let one tool cover many cases
- 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
Make it a Resource
- Safe to expose broadly — nothing can be damaged
- Cacheable, because a URI means the same thing each time
- 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:
| Scheme | Use Case | Example |
|---|---|---|
file:// | Local files | file:///home/user/doc.txt |
db:// | Database records | db://users/123 |
config:// | Configuration | config://app/settings |
api:// | External APIs | api://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. :::
Sign in to rate