Skip to content

Commit 13d062e

Browse files
mplemaycursoragent
andcommitted
Add add_resource_template to MCPServer and ResourceManager.
Enables registering pre-built ResourceTemplate instances at init or runtime, parallel to add_resource. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 19fe9fa commit 13d062e

4 files changed

Lines changed: 164 additions & 7 deletions

File tree

src/mcp/server/mcpserver/resources/resource_manager.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,21 @@
2222
class ResourceManager:
2323
"""Manages MCPServer resources."""
2424

25-
def __init__(self, warn_on_duplicate_resources: bool = True, *, resources: list[Resource] | None = None):
25+
def __init__(
26+
self,
27+
warn_on_duplicate_resources: bool = True,
28+
*,
29+
resources: list[Resource] | None = None,
30+
resource_templates: list[ResourceTemplate] | None = None,
31+
):
2632
self._resources: dict[str, Resource] = {}
2733
self._templates: dict[str, ResourceTemplate] = {}
2834
self.warn_on_duplicate_resources = warn_on_duplicate_resources
2935

3036
for resource in resources or ():
3137
self.add_resource(resource)
38+
for template in resource_templates or ():
39+
self.add_resource_template(template)
3240

3341
def add_resource(self, resource: Resource) -> Resource:
3442
"""Add a resource to the manager.
@@ -51,6 +59,31 @@ def add_resource(self, resource: Resource) -> Resource:
5159
self._resources[str(resource.uri)] = resource
5260
return resource
5361

62+
def add_resource_template(self, template: ResourceTemplate) -> ResourceTemplate:
63+
"""Add a resource template to the manager.
64+
65+
Args:
66+
template: A ResourceTemplate instance to add.
67+
68+
Returns:
69+
The added template. If a template with the same URI template already exists, returns the existing template.
70+
"""
71+
logger.debug(
72+
"Adding resource template",
73+
extra={
74+
"uri_template": template.uri_template,
75+
"type": type(template).__name__,
76+
"resource_name": template.name,
77+
},
78+
)
79+
existing = self._templates.get(template.uri_template)
80+
if existing:
81+
if self.warn_on_duplicate_resources:
82+
logger.warning(f"Resource template already exists: {template.uri_template}")
83+
return existing
84+
self._templates[template.uri_template] = template
85+
return template
86+
5487
def add_template(
5588
self,
5689
fn: Callable[..., Any],
@@ -75,8 +108,7 @@ def add_template(
75108
annotations=annotations,
76109
meta=meta,
77110
)
78-
self._templates[template.uri_template] = template
79-
return template
111+
return self.add_resource_template(template)
80112

81113
async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]) -> Resource:
82114
"""Get resource by URI, checking concrete resources first, then templates."""

src/mcp/server/mcpserver/server.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from mcp.server.mcpserver.context import Context
3434
from mcp.server.mcpserver.exceptions import ResourceError
3535
from mcp.server.mcpserver.prompts import Prompt, PromptManager
36-
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager
36+
from mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager, ResourceTemplate
3737
from mcp.server.mcpserver.tools import Tool, ToolManager
3838
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
3939
from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger
@@ -141,6 +141,7 @@ def __init__(
141141
*,
142142
tools: list[Tool] | None = None,
143143
resources: list[Resource] | None = None,
144+
resource_templates: list[ResourceTemplate] | None = None,
144145
debug: bool = False,
145146
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
146147
warn_on_duplicate_resources: bool = True,
@@ -164,7 +165,9 @@ def __init__(
164165

165166
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
166167
self._resource_manager = ResourceManager(
167-
resources=resources, warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
168+
resources=resources,
169+
resource_templates=resource_templates,
170+
warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources,
168171
)
169172
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
170173
self._lowlevel_server = Server(
@@ -624,6 +627,14 @@ def add_resource(self, resource: Resource) -> None:
624627
"""
625628
self._resource_manager.add_resource(resource)
626629

630+
def add_resource_template(self, template: ResourceTemplate) -> None:
631+
"""Add a resource template to the server.
632+
633+
Args:
634+
template: A ResourceTemplate instance to add
635+
"""
636+
self._resource_manager.add_resource_template(template)
637+
627638
def resource(
628639
self,
629640
uri: str,

tests/server/mcpserver/resources/test_resource_manager.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ def temp_file(tmp_path: Path):
1919
yield tmp_file
2020

2121

22+
def test_init_with_resource_templates():
23+
def greet(name: str) -> str:
24+
return f"Hello, {name}!"
25+
26+
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
27+
manager = ResourceManager(resource_templates=[template])
28+
assert manager.list_templates() == [template]
29+
30+
2231
def test_init_with_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
2332
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
2433
manager = ResourceManager(resources=[resource])
@@ -89,7 +98,7 @@ def greet(name: str) -> str:
8998
return f"Hello, {name}!"
9099

91100
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
92-
manager._templates[template.uri_template] = template
101+
manager.add_resource_template(template)
93102

94103
resource = await manager.get_resource(AnyUrl("greet://world"), Context())
95104
assert isinstance(resource, FunctionResource)
@@ -122,6 +131,59 @@ def test_list_resources(temp_file: Path):
122131
def get_item(id: str) -> str: ...
123132

124133

134+
def test_add_resource_template():
135+
"""Test adding a resource template."""
136+
manager = ResourceManager()
137+
138+
def greet(name: str) -> str:
139+
return f"Hello, {name}!"
140+
141+
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
142+
added = manager.add_resource_template(template)
143+
assert added == template
144+
assert manager.list_templates() == [template]
145+
146+
147+
def test_add_duplicate_resource_template():
148+
"""Test adding the same resource template twice."""
149+
manager = ResourceManager()
150+
151+
def greet(name: str) -> str:
152+
return f"Hello, {name}!"
153+
154+
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
155+
first = manager.add_resource_template(template)
156+
second = manager.add_resource_template(template)
157+
assert first == second
158+
assert manager.list_templates() == [template]
159+
160+
161+
def test_warn_on_duplicate_resource_templates(caplog: pytest.LogCaptureFixture):
162+
"""Test warning on duplicate resource templates."""
163+
manager = ResourceManager()
164+
165+
def greet(name: str) -> str:
166+
return f"Hello, {name}!"
167+
168+
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
169+
manager.add_resource_template(template)
170+
manager.add_resource_template(template)
171+
assert "Resource template already exists" in caplog.text
172+
173+
174+
def test_disable_warn_on_duplicate_resource_templates(caplog: pytest.LogCaptureFixture):
175+
"""Test disabling warning on duplicate resource templates."""
176+
manager = ResourceManager(warn_on_duplicate_resources=False)
177+
178+
def greet(name: str) -> str:
179+
return f"Hello, {name}!"
180+
181+
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
182+
manager.add_resource_template(template)
183+
manager.add_resource_template(template)
184+
assert "Resource template already exists" not in caplog.text
185+
186+
125187
def test_add_template_with_metadata():
126188
"""Test that ResourceManager.add_template() accepts and passes meta parameter."""
127189
manager = ResourceManager()

tests/server/mcpserver/test_server.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from mcp.server.mcpserver import Context, MCPServer
1515
from mcp.server.mcpserver.exceptions import ToolError
1616
from mcp.server.mcpserver.prompts.base import Message, UserMessage
17-
from mcp.server.mcpserver.resources import FileResource, FunctionResource
17+
from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceTemplate as ServerResourceTemplate
1818
from mcp.server.mcpserver.utilities.types import Audio, Image
1919
from mcp.server.transport_security import TransportSecuritySettings
2020
from mcp.shared.exceptions import MCPError
@@ -710,6 +710,58 @@ def get_text() -> str:
710710
assert isinstance(content, TextResourceContents)
711711
assert content.text == "Hello from init!"
712712

713+
async def test_init_with_resource_templates(self):
714+
def get_weather(city: str) -> str:
715+
"""Seeded template."""
716+
return f"Weather for {city}"
717+
718+
template = ServerResourceTemplate.from_function(
719+
fn=get_weather,
720+
uri_template="weather://{city}",
721+
name="weather",
722+
description="Seeded template.",
723+
)
724+
725+
mcp = MCPServer(resource_templates=[template])
726+
727+
async with Client(mcp) as client:
728+
templates = await client.list_resource_templates()
729+
assert len(templates.resource_templates) == 1
730+
listed = templates.resource_templates[0]
731+
assert listed.uri_template == "weather://{city}"
732+
assert listed.name == "weather"
733+
assert listed.description == "Seeded template."
734+
735+
result = await client.read_resource("weather://london")
736+
737+
assert len(result.contents) == 1
738+
content = result.contents[0]
739+
assert isinstance(content, TextResourceContents)
740+
assert content.text == "Weather for london"
741+
742+
async def test_add_resource_template(self):
743+
mcp = MCPServer()
744+
745+
def get_weather(city: str) -> str:
746+
return f"Weather for {city}"
747+
748+
template = ServerResourceTemplate.from_function(
749+
fn=get_weather,
750+
uri_template="weather://{city}",
751+
name="weather",
752+
)
753+
mcp.add_resource_template(template)
754+
755+
async with Client(mcp) as client:
756+
templates = await client.list_resource_templates()
757+
assert len(templates.resource_templates) == 1
758+
assert templates.resource_templates[0].uri_template == "weather://{city}"
759+
760+
result = await client.read_resource("weather://paris")
761+
762+
assert isinstance(result.contents[0], TextResourceContents)
763+
assert result.contents[0].text == "Weather for paris"
764+
713765
async def test_text_resource(self):
714766
mcp = MCPServer()
715767

0 commit comments

Comments
 (0)