Skip to content

Commit 9fbeaf6

Browse files
test: Add comprehensive tests for ResourceContents direct return functionality
Added test coverage for the new feature allowing resources to return TextResourceContents and BlobResourceContents objects directly: Low-level server tests (test_read_resource_direct.py): - Test direct TextResourceContents return - Test direct BlobResourceContents return - Test mixed direct and wrapped content - Test multiple ResourceContents objects FastMCP tests (test_resource_contents_direct.py): - Test custom resources returning ResourceContents - Test function resources returning ResourceContents - Test resource templates with ResourceContents - Test mixed traditional and direct resources All tests verify proper handling and pass-through of ResourceContents objects without wrapping them in ReadResourceContents. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 94e7668 commit 9fbeaf6

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""Test FastMCP resources returning ResourceContents directly."""
2+
3+
import pytest
4+
from pydantic import AnyUrl
5+
6+
from mcp.server.fastmcp import FastMCP
7+
from mcp.server.fastmcp.resources import TextResource
8+
from mcp.types import BlobResourceContents, TextResourceContents
9+
10+
11+
@pytest.mark.anyio
12+
async def test_resource_returns_text_resource_contents_directly():
13+
"""Test a custom resource that returns TextResourceContents directly."""
14+
app = FastMCP("test")
15+
16+
class DirectTextResource(TextResource):
17+
"""A resource that returns TextResourceContents directly."""
18+
19+
async def read(self):
20+
# Return TextResourceContents directly instead of str
21+
return TextResourceContents(
22+
uri=self.uri,
23+
text="Direct TextResourceContents content",
24+
mimeType="text/markdown",
25+
)
26+
27+
# Add the resource
28+
app.add_resource(
29+
DirectTextResource(
30+
uri="resource://direct-text",
31+
name="direct-text",
32+
title="Direct Text Resource",
33+
description="Returns TextResourceContents directly",
34+
text="This is ignored since we override read()",
35+
)
36+
)
37+
38+
# Read the resource
39+
contents = await app.read_resource("resource://direct-text")
40+
contents_list = list(contents)
41+
42+
# Verify the result
43+
assert len(contents_list) == 1
44+
content = contents_list[0]
45+
assert isinstance(content, TextResourceContents)
46+
assert content.text == "Direct TextResourceContents content"
47+
assert content.mimeType == "text/markdown"
48+
assert str(content.uri) == "resource://direct-text"
49+
50+
51+
@pytest.mark.anyio
52+
async def test_resource_returns_blob_resource_contents_directly():
53+
"""Test a custom resource that returns BlobResourceContents directly."""
54+
app = FastMCP("test")
55+
56+
class DirectBlobResource(TextResource):
57+
"""A resource that returns BlobResourceContents directly."""
58+
59+
async def read(self):
60+
# Return BlobResourceContents directly
61+
return BlobResourceContents(
62+
uri=self.uri,
63+
blob="SGVsbG8gRmFzdE1DUA==", # "Hello FastMCP" in base64
64+
mimeType="application/pdf",
65+
)
66+
67+
# Add the resource
68+
app.add_resource(
69+
DirectBlobResource(
70+
uri="resource://direct-blob",
71+
name="direct-blob",
72+
title="Direct Blob Resource",
73+
description="Returns BlobResourceContents directly",
74+
text="This is ignored since we override read()",
75+
)
76+
)
77+
78+
# Read the resource
79+
contents = await app.read_resource("resource://direct-blob")
80+
contents_list = list(contents)
81+
82+
# Verify the result
83+
assert len(contents_list) == 1
84+
content = contents_list[0]
85+
assert isinstance(content, BlobResourceContents)
86+
assert content.blob == "SGVsbG8gRmFzdE1DUA=="
87+
assert content.mimeType == "application/pdf"
88+
assert str(content.uri) == "resource://direct-blob"
89+
90+
91+
@pytest.mark.anyio
92+
async def test_function_resource_returns_resource_contents():
93+
"""Test function resource returning ResourceContents directly."""
94+
app = FastMCP("test")
95+
96+
@app.resource("resource://function-text-contents")
97+
async def get_text_contents() -> TextResourceContents:
98+
"""Return TextResourceContents directly from function resource."""
99+
return TextResourceContents(
100+
uri=AnyUrl("resource://function-text-contents"),
101+
text="Function returned TextResourceContents",
102+
mimeType="text/x-python",
103+
)
104+
105+
@app.resource("resource://function-blob-contents")
106+
def get_blob_contents() -> BlobResourceContents:
107+
"""Return BlobResourceContents directly from function resource."""
108+
return BlobResourceContents(
109+
uri=AnyUrl("resource://function-blob-contents"),
110+
blob="RnVuY3Rpb24gYmxvYg==", # "Function blob" in base64
111+
mimeType="image/png",
112+
)
113+
114+
# Read text resource
115+
text_contents = await app.read_resource("resource://function-text-contents")
116+
text_list = list(text_contents)
117+
assert len(text_list) == 1
118+
text_content = text_list[0]
119+
assert isinstance(text_content, TextResourceContents)
120+
assert text_content.text == "Function returned TextResourceContents"
121+
assert text_content.mimeType == "text/x-python"
122+
123+
# Read blob resource
124+
blob_contents = await app.read_resource("resource://function-blob-contents")
125+
blob_list = list(blob_contents)
126+
assert len(blob_list) == 1
127+
blob_content = blob_list[0]
128+
assert isinstance(blob_content, BlobResourceContents)
129+
assert blob_content.blob == "RnVuY3Rpb24gYmxvYg=="
130+
assert blob_content.mimeType == "image/png"
131+
132+
133+
@pytest.mark.anyio
134+
async def test_mixed_traditional_and_direct_resources():
135+
"""Test server with both traditional and direct ResourceContents resources."""
136+
app = FastMCP("test")
137+
138+
# Traditional string resource
139+
@app.resource("resource://traditional")
140+
def traditional_resource() -> str:
141+
return "Traditional string content"
142+
143+
# Direct ResourceContents resource
144+
@app.resource("resource://direct")
145+
def direct_resource() -> TextResourceContents:
146+
return TextResourceContents(
147+
uri=AnyUrl("resource://direct"),
148+
text="Direct ResourceContents content",
149+
mimeType="text/html",
150+
)
151+
152+
# Read traditional resource (will be wrapped)
153+
trad_contents = await app.read_resource("resource://traditional")
154+
trad_list = list(trad_contents)
155+
assert len(trad_list) == 1
156+
# The content type might be ReadResourceContents, but we're checking the behavior
157+
158+
# Read direct ResourceContents
159+
direct_contents = await app.read_resource("resource://direct")
160+
direct_list = list(direct_contents)
161+
assert len(direct_list) == 1
162+
direct_content = direct_list[0]
163+
assert isinstance(direct_content, TextResourceContents)
164+
assert direct_content.text == "Direct ResourceContents content"
165+
assert direct_content.mimeType == "text/html"
166+
167+
168+
@pytest.mark.anyio
169+
async def test_resource_template_returns_resource_contents():
170+
"""Test resource template returning ResourceContents directly."""
171+
app = FastMCP("test")
172+
173+
@app.resource("resource://{category}/{item}")
174+
async def get_item_contents(category: str, item: str) -> TextResourceContents:
175+
"""Return TextResourceContents for template resource."""
176+
return TextResourceContents(
177+
uri=AnyUrl(f"resource://{category}/{item}"),
178+
text=f"Content for {item} in {category}",
179+
mimeType="text/plain",
180+
)
181+
182+
# Read templated resource
183+
contents = await app.read_resource("resource://books/python")
184+
contents_list = list(contents)
185+
assert len(contents_list) == 1
186+
content = contents_list[0]
187+
assert isinstance(content, TextResourceContents)
188+
assert content.text == "Content for python in books"
189+
assert content.mimeType == "text/plain"
190+
assert str(content.uri) == "resource://books/python"
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
from collections.abc import Iterable
2+
from pathlib import Path
3+
from tempfile import NamedTemporaryFile
4+
5+
import pytest
6+
from pydantic import AnyUrl, FileUrl
7+
8+
import mcp.types as types
9+
from mcp.server.lowlevel.server import ReadResourceContents, Server
10+
11+
12+
@pytest.fixture
13+
def temp_file():
14+
"""Create a temporary file for testing."""
15+
with NamedTemporaryFile(mode="w", delete=False) as f:
16+
f.write("test content")
17+
path = Path(f.name).resolve()
18+
yield path
19+
try:
20+
path.unlink()
21+
except FileNotFoundError:
22+
pass
23+
24+
25+
@pytest.mark.anyio
26+
async def test_read_resource_direct_text_resource_contents(temp_file: Path):
27+
"""Test returning TextResourceContents directly from read_resource handler."""
28+
server = Server("test")
29+
30+
@server.read_resource()
31+
async def read_resource(uri: AnyUrl) -> Iterable[types.TextResourceContents]:
32+
return [
33+
types.TextResourceContents(
34+
uri=uri,
35+
text="Direct text content",
36+
mimeType="text/markdown",
37+
)
38+
]
39+
40+
# Get the handler directly from the server
41+
handler = server.request_handlers[types.ReadResourceRequest]
42+
43+
# Create a request
44+
request = types.ReadResourceRequest(
45+
params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())),
46+
)
47+
48+
# Call the handler
49+
result = await handler(request)
50+
assert isinstance(result.root, types.ReadResourceResult)
51+
assert len(result.root.contents) == 1
52+
53+
content = result.root.contents[0]
54+
assert isinstance(content, types.TextResourceContents)
55+
assert content.text == "Direct text content"
56+
assert content.mimeType == "text/markdown"
57+
assert str(content.uri) == temp_file.as_uri()
58+
59+
60+
@pytest.mark.anyio
61+
async def test_read_resource_direct_blob_resource_contents(temp_file: Path):
62+
"""Test returning BlobResourceContents directly from read_resource handler."""
63+
server = Server("test")
64+
65+
@server.read_resource()
66+
async def read_resource(uri: AnyUrl) -> Iterable[types.BlobResourceContents]:
67+
return [
68+
types.BlobResourceContents(
69+
uri=uri,
70+
blob="SGVsbG8gV29ybGQ=", # "Hello World" in base64
71+
mimeType="application/pdf",
72+
)
73+
]
74+
75+
# Get the handler directly from the server
76+
handler = server.request_handlers[types.ReadResourceRequest]
77+
78+
# Create a request
79+
request = types.ReadResourceRequest(
80+
params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())),
81+
)
82+
83+
# Call the handler
84+
result = await handler(request)
85+
assert isinstance(result.root, types.ReadResourceResult)
86+
assert len(result.root.contents) == 1
87+
88+
content = result.root.contents[0]
89+
assert isinstance(content, types.BlobResourceContents)
90+
assert content.blob == "SGVsbG8gV29ybGQ="
91+
assert content.mimeType == "application/pdf"
92+
assert str(content.uri) == temp_file.as_uri()
93+
94+
95+
@pytest.mark.anyio
96+
async def test_read_resource_mixed_contents(temp_file: Path):
97+
"""Test mixing direct ResourceContents with ReadResourceContents."""
98+
server = Server("test")
99+
100+
@server.read_resource()
101+
async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents | types.TextResourceContents]:
102+
return [
103+
types.TextResourceContents(
104+
uri=uri,
105+
text="Direct ResourceContents",
106+
mimeType="text/plain",
107+
),
108+
ReadResourceContents(content="Wrapped content", mime_type="text/html"),
109+
]
110+
111+
# Get the handler directly from the server
112+
handler = server.request_handlers[types.ReadResourceRequest]
113+
114+
# Create a request
115+
request = types.ReadResourceRequest(
116+
params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())),
117+
)
118+
119+
# Call the handler
120+
result = await handler(request)
121+
assert isinstance(result.root, types.ReadResourceResult)
122+
assert len(result.root.contents) == 2
123+
124+
# First content is direct ResourceContents
125+
content1 = result.root.contents[0]
126+
assert isinstance(content1, types.TextResourceContents)
127+
assert content1.text == "Direct ResourceContents"
128+
assert content1.mimeType == "text/plain"
129+
130+
# Second content is wrapped ReadResourceContents
131+
content2 = result.root.contents[1]
132+
assert isinstance(content2, types.TextResourceContents)
133+
assert content2.text == "Wrapped content"
134+
assert content2.mimeType == "text/html"
135+
136+
137+
@pytest.mark.anyio
138+
async def test_read_resource_multiple_resource_contents(temp_file: Path):
139+
"""Test returning multiple ResourceContents objects."""
140+
server = Server("test")
141+
142+
@server.read_resource()
143+
async def read_resource(uri: AnyUrl) -> Iterable[types.TextResourceContents | types.BlobResourceContents]:
144+
return [
145+
types.TextResourceContents(
146+
uri=uri,
147+
text="First text content",
148+
mimeType="text/plain",
149+
),
150+
types.BlobResourceContents(
151+
uri=uri,
152+
blob="U2Vjb25kIGNvbnRlbnQ=", # "Second content" in base64
153+
mimeType="application/octet-stream",
154+
),
155+
types.TextResourceContents(
156+
uri=uri,
157+
text="Third text content",
158+
mimeType="text/markdown",
159+
),
160+
]
161+
162+
# Get the handler directly from the server
163+
handler = server.request_handlers[types.ReadResourceRequest]
164+
165+
# Create a request
166+
request = types.ReadResourceRequest(
167+
params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())),
168+
)
169+
170+
# Call the handler
171+
result = await handler(request)
172+
assert isinstance(result.root, types.ReadResourceResult)
173+
assert len(result.root.contents) == 3
174+
175+
# Check first content
176+
content1 = result.root.contents[0]
177+
assert isinstance(content1, types.TextResourceContents)
178+
assert content1.text == "First text content"
179+
assert content1.mimeType == "text/plain"
180+
181+
# Check second content
182+
content2 = result.root.contents[1]
183+
assert isinstance(content2, types.BlobResourceContents)
184+
assert content2.blob == "U2Vjb25kIGNvbnRlbnQ="
185+
assert content2.mimeType == "application/octet-stream"
186+
187+
# Check third content
188+
content3 = result.root.contents[2]
189+
assert isinstance(content3, types.TextResourceContents)
190+
assert content3.text == "Third text content"
191+
assert content3.mimeType == "text/markdown"

0 commit comments

Comments
 (0)