Some MCP clients only support tools. They cannot list or read resources directly because they lack resource protocol support. The ResourcesAsTools transform bridges this gap by generating tools that provide access to your server’s resources.
When you add ResourcesAsTools to a server, it creates two tools that clients can call instead of using the resource protocol:
list_resources returns JSON describing all available resources and templates
read_resource reads a specific resource by URI
This means any client that can call tools can now access resources, even if the client has no native resource support.
Basic Usage
Pass your FastMCP server to ResourcesAsTools when adding the transform. The generated tools route through the server at runtime, which means all server middleware — auth, visibility, rate limiting — applies to resource operations automatically, exactly as it would for direct resources/read calls.
ResourcesAsTools (and PromptsAsTools) should be applied to a FastMCP server instance, not a raw Provider. The generated tools call back into the server’s middleware chain at runtime, so they need a server to route through. If you want to expose only a subset of resources, create a dedicated FastMCP server for those resources and apply the transform there.
from fastmcp import FastMCP
from fastmcp.server.transforms import ResourcesAsTools
mcp = FastMCP("My Server")
@mcp.resource("config://app")
def app_config() -> str:
"""Application configuration."""
return '{"app_name": "My App", "version": "1.0.0"}'
@mcp.resource("user://{user_id}/profile")
def user_profile(user_id: str) -> str:
"""Get a user's profile by ID."""
return f'{{"user_id": "{user_id}", "name": "User {user_id}"}}'
# Add the transform - creates list_resources and read_resource tools
mcp.add_transform(ResourcesAsTools(mcp))
Clients now see three tools: whatever tools you defined directly, plus list_resources and read_resource.
Both generated tools are annotated with readOnlyHint=True, since they only read data. Clients that respect tool annotations (like Cursor) can use this to auto-confirm these tool calls without prompting the user.
Static Resources vs Templates
Resources come in two forms, and the list_resources tool distinguishes between them in its JSON output.
Static resources have fixed URIs. They represent concrete data that exists at a known location. In the listing output, static resources include a uri field containing the exact URI to request.
Resource templates have parameterized URIs with placeholders like {user_id}. They represent patterns for accessing dynamic data. In the listing output, templates include a uri_template field showing the pattern with its placeholders.
When a client calls list_resources, it receives JSON like this:
[
{
"uri": "config://app",
"name": "app_config",
"description": "Application configuration.",
"mime_type": "text/plain"
},
{
"uri_template": "user://{user_id}/profile",
"name": "user_profile",
"description": "Get a user's profile by ID."
}
]
The client can distinguish resource types by checking which field is present: uri for static resources, uri_template for templates.
Reading Resources
The read_resource tool accepts a single uri argument. For static resources, pass the exact URI. For templates, fill in the placeholders with actual values.
# Reading a static resource
result = await client.call_tool("read_resource", {"uri": "config://app"})
print(result.data) # '{"app_name": "My App", "version": "1.0.0"}'
# Reading a templated resource - fill in {user_id} with an actual ID
result = await client.call_tool("read_resource", {"uri": "user://42/profile"})
print(result.data) # '{"user_id": "42", "name": "User 42"}'
The transform handles template matching automatically. When you request user://42/profile, it matches against the user://{user_id}/profile template, extracts user_id=42, and calls your resource function with that parameter.
Binary Content
Resources that return binary data (like images or files) are automatically base64-encoded when read through the read_resource tool. This ensures binary content can be transmitted as a string in the tool response.
@mcp.resource("data://binary", mime_type="application/octet-stream")
def binary_data() -> bytes:
return b"\x00\x01\x02\x03"
# Client receives base64-encoded string
result = await client.call_tool("read_resource", {"uri": "data://binary"})
decoded = base64.b64decode(result.data) # b'\x00\x01\x02\x03'