mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Add ResourcesAsTools transform (#2943)
This commit is contained in:
parent
a036ad31e2
commit
d3327269d7
7 changed files with 641 additions and 0 deletions
|
|
@ -109,6 +109,7 @@
|
|||
"pages": [
|
||||
"servers/providers/overview",
|
||||
"servers/providers/transforms",
|
||||
"servers/providers/resources-as-tools",
|
||||
"servers/providers/local",
|
||||
"servers/providers/filesystem",
|
||||
"servers/providers/mounting",
|
||||
|
|
|
|||
104
docs/servers/providers/resources-as-tools.mdx
Normal file
104
docs/servers/providers/resources-as-tools.mdx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
title: Resources as Tools
|
||||
sidebarTitle: Resources as Tools
|
||||
description: Expose resources to tool-only clients
|
||||
icon: toolbox
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
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 server to `ResourcesAsTools` when adding the transform. The transform queries that server for resources whenever the generated tools are called.
|
||||
|
||||
```python
|
||||
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`.
|
||||
|
||||
## 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:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"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.
|
||||
|
||||
```python
|
||||
# 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.
|
||||
|
||||
```python
|
||||
@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'
|
||||
```
|
||||
|
||||
52
examples/resources_as_tools/client.py
Normal file
52
examples/resources_as_tools/client.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Example: Client using resources-as-tools.
|
||||
|
||||
This client demonstrates calling the list_resources and read_resource tools
|
||||
generated by the ResourcesAsTools transform.
|
||||
|
||||
Run with:
|
||||
uv run python examples/resources_as_tools/client.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from fastmcp.client import Client
|
||||
|
||||
|
||||
async def main():
|
||||
# Connect to the server
|
||||
async with Client("examples/resources_as_tools/server.py") as client:
|
||||
# List all available tools
|
||||
print("=== Available Tools ===")
|
||||
tools = await client.list_tools()
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
print()
|
||||
|
||||
# Use list_resources tool to see what's available
|
||||
print("=== Listing Resources ===")
|
||||
result = await client.call_tool("list_resources", {})
|
||||
resources = json.loads(result.data)
|
||||
for resource in resources:
|
||||
if "uri" in resource:
|
||||
print(f" Static: {resource['uri']}")
|
||||
else:
|
||||
print(f" Template: {resource['uri_template']}")
|
||||
print(f" Name: {resource['name']}")
|
||||
print(f" Description: {resource.get('description', 'N/A')}")
|
||||
print()
|
||||
|
||||
# Read a static resource
|
||||
print("=== Reading Static Resource ===")
|
||||
result = await client.call_tool("read_resource", {"uri": "config://app"})
|
||||
print(f"config://app content:\n{result.data}")
|
||||
print()
|
||||
|
||||
# Read a templated resource
|
||||
print("=== Reading Templated Resource ===")
|
||||
result = await client.call_tool("read_resource", {"uri": "user://42/profile"})
|
||||
print(f"user://42/profile content:\n{result.data}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
65
examples/resources_as_tools/server.py
Normal file
65
examples/resources_as_tools/server.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""Example: Expose resources as tools using ResourcesAsTools transform.
|
||||
|
||||
This example shows how to use ResourcesAsTools to make resources accessible
|
||||
to clients that only support tools (not the resources protocol).
|
||||
|
||||
Run with:
|
||||
uv run python examples/resources_as_tools/server.py
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.transforms import ResourcesAsTools
|
||||
|
||||
mcp = FastMCP("Resource Tools Demo")
|
||||
|
||||
|
||||
# Static resource - has a fixed URI
|
||||
@mcp.resource("config://app")
|
||||
def app_config() -> str:
|
||||
"""Application configuration."""
|
||||
return """
|
||||
{
|
||||
"app_name": "My App",
|
||||
"version": "1.0.0",
|
||||
"debug": false
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# Another static resource
|
||||
@mcp.resource("readme://main")
|
||||
def readme() -> str:
|
||||
"""Project README."""
|
||||
return """
|
||||
# My Project
|
||||
|
||||
This is an example project demonstrating ResourcesAsTools.
|
||||
"""
|
||||
|
||||
|
||||
# Resource template - URI has placeholders
|
||||
@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}",
|
||||
"email": "user{user_id}@example.com"
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
# Another template with multiple parameters
|
||||
@mcp.resource("file://{directory}/{filename}")
|
||||
def read_file(directory: str, filename: str) -> str:
|
||||
"""Read a file from a directory."""
|
||||
return f"Contents of {directory}/{filename}"
|
||||
|
||||
|
||||
# Add the transform - this creates list_resources and read_resource tools
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
|
|
@ -222,6 +222,7 @@ class Transform:
|
|||
# Re-export built-in transforms (must be after Transform class to avoid circular imports)
|
||||
from fastmcp.server.transforms.enabled import Enabled, is_enabled # noqa: E402
|
||||
from fastmcp.server.transforms.namespace import Namespace # noqa: E402
|
||||
from fastmcp.server.transforms.resources_as_tools import ResourcesAsTools # noqa: E402
|
||||
from fastmcp.server.transforms.tool_transform import ToolTransform # noqa: E402
|
||||
from fastmcp.server.transforms.version_filter import VersionFilter # noqa: E402
|
||||
|
||||
|
|
@ -232,6 +233,7 @@ __all__ = [
|
|||
"GetResourceTemplateNext",
|
||||
"GetToolNext",
|
||||
"Namespace",
|
||||
"ResourcesAsTools",
|
||||
"ToolTransform",
|
||||
"Transform",
|
||||
"VersionFilter",
|
||||
|
|
|
|||
190
src/fastmcp/server/transforms/resources_as_tools.py
Normal file
190
src/fastmcp/server/transforms/resources_as_tools.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
"""Transform that exposes resources as tools.
|
||||
|
||||
This transform generates tools for listing and reading resources, enabling
|
||||
clients that only support tools to access resource functionality.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.transforms import ResourcesAsTools
|
||||
|
||||
mcp = FastMCP("Server")
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
# Now has list_resources and read_resource tools
|
||||
```
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Annotated, Any
|
||||
|
||||
from fastmcp.server.transforms import GetToolNext, Transform
|
||||
from fastmcp.tools.tool import Tool
|
||||
from fastmcp.utilities.versions import VersionSpec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.server.providers.base import Provider
|
||||
|
||||
|
||||
class ResourcesAsTools(Transform):
|
||||
"""Transform that adds tools for listing and reading resources.
|
||||
|
||||
Generates two tools:
|
||||
- `list_resources`: Lists all resources and templates from the provider
|
||||
- `read_resource`: Reads a resource by URI
|
||||
|
||||
The transform captures a provider reference at construction and queries it
|
||||
for resources when the generated tools are called. When used with FastMCP,
|
||||
the provider's auth and visibility filtering is automatically applied.
|
||||
|
||||
Example:
|
||||
```python
|
||||
mcp = FastMCP("Server")
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
# Now has list_resources and read_resource tools
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, provider: Provider) -> None:
|
||||
"""Initialize the transform with a provider reference.
|
||||
|
||||
Args:
|
||||
provider: The provider to query for resources. Typically this is
|
||||
the same FastMCP server the transform is added to.
|
||||
"""
|
||||
self._provider = provider
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ResourcesAsTools({self._provider!r})"
|
||||
|
||||
async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
|
||||
"""Add resource tools to the tool list."""
|
||||
return [
|
||||
*tools,
|
||||
self._make_list_resources_tool(),
|
||||
self._make_read_resource_tool(),
|
||||
]
|
||||
|
||||
async def get_tool(
|
||||
self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
|
||||
) -> Tool | None:
|
||||
"""Get a tool by name, including generated resource tools."""
|
||||
# Check if it's one of our generated tools
|
||||
if name == "list_resources":
|
||||
return self._make_list_resources_tool()
|
||||
if name == "read_resource":
|
||||
return self._make_read_resource_tool()
|
||||
|
||||
# Otherwise delegate to downstream
|
||||
return await call_next(name, version=version)
|
||||
|
||||
def _make_list_resources_tool(self) -> Tool:
|
||||
"""Create the list_resources tool."""
|
||||
provider = self._provider
|
||||
|
||||
async def list_resources() -> str:
|
||||
"""List all available resources and resource templates.
|
||||
|
||||
Returns JSON with resource metadata. Static resources have a 'uri' field,
|
||||
while templates have a 'uri_template' field with placeholders like {name}.
|
||||
"""
|
||||
resources = await provider.list_resources()
|
||||
templates = await provider.list_resource_templates()
|
||||
|
||||
result: list[dict[str, Any]] = []
|
||||
|
||||
# Static resources
|
||||
for r in resources:
|
||||
result.append(
|
||||
{
|
||||
"uri": str(r.uri),
|
||||
"name": r.name,
|
||||
"description": r.description,
|
||||
"mime_type": r.mime_type,
|
||||
}
|
||||
)
|
||||
|
||||
# Resource templates (URI contains placeholders like {name})
|
||||
for t in templates:
|
||||
result.append(
|
||||
{
|
||||
"uri_template": t.uri_template,
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
}
|
||||
)
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
return Tool.from_function(fn=list_resources)
|
||||
|
||||
def _make_read_resource_tool(self) -> Tool:
|
||||
"""Create the read_resource tool."""
|
||||
provider = self._provider
|
||||
|
||||
async def read_resource(
|
||||
uri: Annotated[str, "The URI of the resource to read"],
|
||||
) -> str:
|
||||
"""Read a resource by its URI.
|
||||
|
||||
For static resources, provide the exact URI. For templated resources,
|
||||
provide the URI with template parameters filled in.
|
||||
|
||||
Returns the resource content as a string. Binary content is
|
||||
base64-encoded.
|
||||
"""
|
||||
from fastmcp import FastMCP
|
||||
|
||||
# Use FastMCP.read_resource() if available - runs middleware chain
|
||||
if isinstance(provider, FastMCP):
|
||||
result = await provider.read_resource(uri)
|
||||
return _format_result(result)
|
||||
|
||||
# Fallback for plain providers - no middleware
|
||||
resource = await provider.get_resource(uri)
|
||||
if resource is not None:
|
||||
result = await resource._read()
|
||||
return _format_result(result)
|
||||
|
||||
template = await provider.get_resource_template(uri)
|
||||
if template is not None:
|
||||
params = template.matches(uri)
|
||||
if params is not None:
|
||||
result = await template._read(uri, params)
|
||||
return _format_result(result)
|
||||
|
||||
raise ValueError(f"Resource not found: {uri}")
|
||||
|
||||
return Tool.from_function(fn=read_resource)
|
||||
|
||||
|
||||
def _format_result(result: Any) -> str:
|
||||
"""Format ResourceResult for tool output.
|
||||
|
||||
Single text content is returned as-is. Single binary content is base64-encoded.
|
||||
Multiple contents are JSON-encoded with each item containing content and mime_type.
|
||||
"""
|
||||
# result is a ResourceResult with .contents list
|
||||
if len(result.contents) == 1:
|
||||
content = result.contents[0].content
|
||||
if isinstance(content, bytes):
|
||||
return base64.b64encode(content).decode()
|
||||
return content
|
||||
|
||||
# Multiple contents - JSON encode
|
||||
return json.dumps(
|
||||
[
|
||||
{
|
||||
"content": (
|
||||
c.content
|
||||
if isinstance(c.content, str)
|
||||
else base64.b64encode(c.content).decode()
|
||||
),
|
||||
"mime_type": c.mime_type,
|
||||
}
|
||||
for c in result.contents
|
||||
]
|
||||
)
|
||||
227
tests/server/transforms/test_resources_as_tools.py
Normal file
227
tests/server/transforms/test_resources_as_tools.py
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
"""Tests for ResourcesAsTools transform."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.server.transforms import ResourcesAsTools
|
||||
|
||||
|
||||
class TestResourcesAsToolsBasic:
|
||||
"""Test basic ResourcesAsTools functionality."""
|
||||
|
||||
async def test_adds_list_resources_tool(self):
|
||||
"""Transform adds list_resources tool."""
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
tool_names = {t.name for t in tools}
|
||||
assert "list_resources" in tool_names
|
||||
|
||||
async def test_adds_read_resource_tool(self):
|
||||
"""Transform adds read_resource tool."""
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
tool_names = {t.name for t in tools}
|
||||
assert "read_resource" in tool_names
|
||||
|
||||
async def test_preserves_existing_tools(self):
|
||||
"""Transform preserves existing tools."""
|
||||
mcp = FastMCP("Test")
|
||||
|
||||
@mcp.tool
|
||||
def my_tool() -> str:
|
||||
return "result"
|
||||
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
tool_names = {t.name for t in tools}
|
||||
assert "my_tool" in tool_names
|
||||
assert "list_resources" in tool_names
|
||||
assert "read_resource" in tool_names
|
||||
|
||||
|
||||
class TestListResourcesTool:
|
||||
"""Test the list_resources tool."""
|
||||
|
||||
async def test_lists_static_resources(self):
|
||||
"""list_resources returns static resources with uri."""
|
||||
mcp = FastMCP("Test")
|
||||
|
||||
@mcp.resource("config://app")
|
||||
def app_config() -> str:
|
||||
return "config data"
|
||||
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("list_resources", {})
|
||||
resources = json.loads(result.data)
|
||||
|
||||
assert len(resources) == 1
|
||||
assert resources[0]["uri"] == "config://app"
|
||||
assert resources[0]["name"] == "app_config"
|
||||
|
||||
async def test_lists_resource_templates(self):
|
||||
"""list_resources returns templates with uri_template."""
|
||||
mcp = FastMCP("Test")
|
||||
|
||||
@mcp.resource("file://{path}")
|
||||
def read_file(path: str) -> str:
|
||||
return f"content of {path}"
|
||||
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("list_resources", {})
|
||||
resources = json.loads(result.data)
|
||||
|
||||
assert len(resources) == 1
|
||||
assert resources[0]["uri_template"] == "file://{path}"
|
||||
assert "uri" not in resources[0]
|
||||
|
||||
async def test_lists_both_resources_and_templates(self):
|
||||
"""list_resources returns both static and templated resources."""
|
||||
mcp = FastMCP("Test")
|
||||
|
||||
@mcp.resource("config://app")
|
||||
def app_config() -> str:
|
||||
return "config"
|
||||
|
||||
@mcp.resource("file://{path}")
|
||||
def read_file(path: str) -> str:
|
||||
return f"content of {path}"
|
||||
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("list_resources", {})
|
||||
resources = json.loads(result.data)
|
||||
|
||||
assert len(resources) == 2
|
||||
# One has uri, one has uri_template
|
||||
uris = [r.get("uri") for r in resources if r.get("uri")]
|
||||
templates = [
|
||||
r.get("uri_template") for r in resources if r.get("uri_template")
|
||||
]
|
||||
assert uris == ["config://app"]
|
||||
assert templates == ["file://{path}"]
|
||||
|
||||
async def test_empty_when_no_resources(self):
|
||||
"""list_resources returns empty list when no resources exist."""
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("list_resources", {})
|
||||
assert json.loads(result.data) == []
|
||||
|
||||
|
||||
class TestReadResourceTool:
|
||||
"""Test the read_resource tool."""
|
||||
|
||||
async def test_reads_static_resource(self):
|
||||
"""read_resource reads a static resource by URI."""
|
||||
mcp = FastMCP("Test")
|
||||
|
||||
@mcp.resource("config://app")
|
||||
def app_config() -> str:
|
||||
return "my config data"
|
||||
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("read_resource", {"uri": "config://app"})
|
||||
assert result.data == "my config data"
|
||||
|
||||
async def test_reads_templated_resource(self):
|
||||
"""read_resource reads a templated resource with parameters."""
|
||||
mcp = FastMCP("Test")
|
||||
|
||||
@mcp.resource("user://{user_id}/profile")
|
||||
def user_profile(user_id: str) -> str:
|
||||
return f"Profile for user {user_id}"
|
||||
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool(
|
||||
"read_resource", {"uri": "user://123/profile"}
|
||||
)
|
||||
assert result.data == "Profile for user 123"
|
||||
|
||||
async def test_error_on_unknown_resource(self):
|
||||
"""read_resource raises error for unknown URI."""
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
with pytest.raises(ToolError, match="Unknown resource"):
|
||||
await client.call_tool("read_resource", {"uri": "unknown://resource"})
|
||||
|
||||
async def test_reads_binary_as_base64(self):
|
||||
"""read_resource returns binary content as base64."""
|
||||
mcp = FastMCP("Test")
|
||||
|
||||
@mcp.resource("data://binary", mime_type="application/octet-stream")
|
||||
def binary_data() -> bytes:
|
||||
return b"\x00\x01\x02\x03"
|
||||
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool("read_resource", {"uri": "data://binary"})
|
||||
# Should be base64 encoded
|
||||
decoded = base64.b64decode(result.data)
|
||||
assert decoded == b"\x00\x01\x02\x03"
|
||||
|
||||
|
||||
class TestResourcesAsToolsWithNamespace:
|
||||
"""Test ResourcesAsTools combined with other transforms."""
|
||||
|
||||
async def test_works_with_namespace_on_provider(self):
|
||||
"""ResourcesAsTools works when provider has Namespace transform."""
|
||||
from fastmcp.server.providers import FastMCPProvider
|
||||
from fastmcp.server.transforms import Namespace
|
||||
|
||||
sub = FastMCP("Sub")
|
||||
|
||||
@sub.resource("config://app")
|
||||
def app_config() -> str:
|
||||
return "sub config"
|
||||
|
||||
main = FastMCP("Main")
|
||||
provider = FastMCPProvider(sub)
|
||||
provider.add_transform(Namespace("sub"))
|
||||
main.add_provider(provider)
|
||||
main.add_transform(ResourcesAsTools(main))
|
||||
|
||||
async with Client(main) as client:
|
||||
result = await client.call_tool("list_resources", {})
|
||||
resources = json.loads(result.data)
|
||||
|
||||
# Resource should have namespaced URI
|
||||
assert len(resources) == 1
|
||||
assert resources[0]["uri"] == "config://sub/app"
|
||||
|
||||
|
||||
class TestResourcesAsToolsRepr:
|
||||
"""Test ResourcesAsTools repr."""
|
||||
|
||||
def test_repr(self):
|
||||
"""Transform has useful repr."""
|
||||
mcp = FastMCP("Test")
|
||||
transform = ResourcesAsTools(mcp)
|
||||
assert "ResourcesAsTools" in repr(transform)
|
||||
Loading…
Add table
Add a link
Reference in a new issue