Skip to content

Commit 51e65c8

Browse files
authoredFeb 4, 2025··
Merge pull request #186 from modelcontextprotocol/fix/141-resource-templates
fix: standardize resource response format
2 parents 960b923 + 0d3e02f commit 51e65c8

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed
 

Diff for: ‎src/mcp/client/session.py

+11
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ async def list_resources(self) -> types.ListResourcesResult:
120120
types.ListResourcesResult,
121121
)
122122

123+
async def list_resource_templates(self) -> types.ListResourceTemplatesResult:
124+
"""Send a resources/templates/list request."""
125+
return await self.send_request(
126+
types.ClientRequest(
127+
types.ListResourceTemplatesRequest(
128+
method="resources/templates/list",
129+
)
130+
),
131+
types.ListResourceTemplatesResult,
132+
)
133+
123134
async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult:
124135
"""Send a resources/read request."""
125136
return await self.send_request(

Diff for: ‎tests/issues/test_141_resource_templates.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import pytest
2+
from pydantic import AnyUrl
3+
4+
from mcp.server.fastmcp import FastMCP
5+
from mcp.shared.memory import (
6+
create_connected_server_and_client_session as client_session,
7+
)
8+
from mcp.types import (
9+
ListResourceTemplatesResult,
10+
TextResourceContents,
11+
)
12+
13+
14+
@pytest.mark.anyio
15+
async def test_resource_template_edge_cases():
16+
"""Test server-side resource template validation"""
17+
mcp = FastMCP("Demo")
18+
19+
# Test case 1: Template with multiple parameters
20+
@mcp.resource("resource://users/{user_id}/posts/{post_id}")
21+
def get_user_post(user_id: str, post_id: str) -> str:
22+
return f"Post {post_id} by user {user_id}"
23+
24+
# Test case 2: Template with optional parameter (should fail)
25+
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
26+
27+
@mcp.resource("resource://users/{user_id}/profile")
28+
def get_user_profile(user_id: str, optional_param: str | None = None) -> str:
29+
return f"Profile for user {user_id}"
30+
31+
# Test case 3: Template with mismatched parameters
32+
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
33+
34+
@mcp.resource("resource://users/{user_id}/profile")
35+
def get_user_profile_mismatch(different_param: str) -> str:
36+
return f"Profile for user {different_param}"
37+
38+
# Test case 4: Template with extra function parameters
39+
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
40+
41+
@mcp.resource("resource://users/{user_id}/profile")
42+
def get_user_profile_extra(user_id: str, extra_param: str) -> str:
43+
return f"Profile for user {user_id}"
44+
45+
# Test case 5: Template with missing function parameters
46+
with pytest.raises(ValueError, match="Mismatch between URI parameters"):
47+
48+
@mcp.resource("resource://users/{user_id}/profile/{section}")
49+
def get_user_profile_missing(user_id: str) -> str:
50+
return f"Profile for user {user_id}"
51+
52+
# Verify valid template works
53+
result = await mcp.read_resource("resource://users/123/posts/456")
54+
assert result.content == "Post 456 by user 123"
55+
assert result.mime_type == "text/plain"
56+
57+
# Verify invalid parameters raise error
58+
with pytest.raises(ValueError, match="Unknown resource"):
59+
await mcp.read_resource("resource://users/123/posts") # Missing post_id
60+
61+
with pytest.raises(ValueError, match="Unknown resource"):
62+
await mcp.read_resource(
63+
"resource://users/123/posts/456/extra"
64+
) # Extra path component
65+
66+
67+
@pytest.mark.anyio
68+
async def test_resource_template_client_interaction():
69+
"""Test client-side resource template interaction"""
70+
mcp = FastMCP("Demo")
71+
72+
# Register some templated resources
73+
@mcp.resource("resource://users/{user_id}/posts/{post_id}")
74+
def get_user_post(user_id: str, post_id: str) -> str:
75+
return f"Post {post_id} by user {user_id}"
76+
77+
@mcp.resource("resource://users/{user_id}/profile")
78+
def get_user_profile(user_id: str) -> str:
79+
return f"Profile for user {user_id}"
80+
81+
async with client_session(mcp._mcp_server) as session:
82+
# Initialize the session
83+
await session.initialize()
84+
85+
# List available resources
86+
resources = await session.list_resource_templates()
87+
assert isinstance(resources, ListResourceTemplatesResult)
88+
assert len(resources.resourceTemplates) == 2
89+
90+
# Verify resource templates are listed correctly
91+
templates = [r.uriTemplate for r in resources.resourceTemplates]
92+
assert "resource://users/{user_id}/posts/{post_id}" in templates
93+
assert "resource://users/{user_id}/profile" in templates
94+
95+
# Read a resource with valid parameters
96+
result = await session.read_resource(AnyUrl("resource://users/123/posts/456"))
97+
contents = result.contents[0]
98+
assert isinstance(contents, TextResourceContents)
99+
assert contents.text == "Post 456 by user 123"
100+
assert contents.mimeType == "text/plain"
101+
102+
# Read another resource with valid parameters
103+
result = await session.read_resource(AnyUrl("resource://users/789/profile"))
104+
contents = result.contents[0]
105+
assert isinstance(contents, TextResourceContents)
106+
assert contents.text == "Profile for user 789"
107+
assert contents.mimeType == "text/plain"
108+
109+
# Verify invalid resource URIs raise appropriate errors
110+
with pytest.raises(Exception): # Specific exception type may vary
111+
await session.read_resource(
112+
AnyUrl("resource://users/123/posts")
113+
) # Missing post_id
114+
115+
with pytest.raises(Exception): # Specific exception type may vary
116+
await session.read_resource(
117+
AnyUrl("resource://users/123/invalid")
118+
) # Invalid template

0 commit comments

Comments
 (0)
Please sign in to comment.