Skip to content

Commit c2787df

Browse files
committed
feat(mcpserver): add add_resource_template() for programmatic template registration
Add add_resource_template() to both ResourceManager and MCPServer so ResourceTemplate instances can be registered directly, without the @resource decorator. Duplicate uri_template registrations warn and return the existing template, mirroring add_resource() semantics. add_template() now delegates to the new method.
1 parent 57394b0 commit c2787df

5 files changed

Lines changed: 102 additions & 3 deletions

File tree

docs/servers/resources.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ The same rule applies to anything else JSON-serialisable: a list, a Pydantic mod
127127
`BinaryResource`, `FileResource`, `HttpResource`, `DirectoryResource`) that you register
128128
with `mcp.add_resource(...)`.
129129

130+
Template resources can be registered programmatically too: build a `ResourceTemplate`
131+
with `ResourceTemplate.from_function(...)` and call `mcp.add_resource_template(...)`
132+
instead of decorating a function.
133+
130134
A client can also **subscribe** to a resource and be notified when it changes; that's the client's half of the story and it lives in **[The Client](../client/index.md)**.
131135

132136
## Recap

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ def add_resource(self, resource: Resource) -> Resource:
5757
self._resources[str(resource.uri)] = resource
5858
return resource
5959

60+
def add_resource_template(self, template: ResourceTemplate) -> ResourceTemplate:
61+
"""Add a resource template to the manager.
62+
63+
Args:
64+
template: A ResourceTemplate instance to add.
65+
66+
Returns:
67+
The added template. If a template with the same uri_template
68+
already exists, returns the existing template.
69+
"""
70+
logger.debug(
71+
"Adding resource template",
72+
extra={"uri_template": template.uri_template, "name": template.name},
73+
)
74+
existing = self._templates.get(template.uri_template)
75+
if existing:
76+
if self.warn_on_duplicate_resources:
77+
logger.warning(f"Resource template already exists: {template.uri_template}")
78+
return existing
79+
self._templates[template.uri_template] = template
80+
return template
81+
6082
def add_template(
6183
self,
6284
fn: Callable[..., Any],
@@ -83,8 +105,7 @@ def add_template(
83105
meta=meta,
84106
security=security,
85107
)
86-
self._templates[template.uri_template] = template
87-
return template
108+
return self.add_resource_template(template)
88109

89110
async def get_resource(
90111
self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]

src/mcp/server/mcpserver/server.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
Resource,
8080
ResourceManager,
8181
ResourceSecurity,
82+
ResourceTemplate,
8283
)
8384
from mcp.server.mcpserver.tools import Tool, ToolManager
8485
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
@@ -730,6 +731,14 @@ def add_resource(self, resource: Resource) -> None:
730731
"""
731732
self._resource_manager.add_resource(resource)
732733

734+
def add_resource_template(self, template: ResourceTemplate) -> None:
735+
"""Add a resource template to the server.
736+
737+
Args:
738+
template: A ResourceTemplate instance to add.
739+
"""
740+
self._resource_manager.add_resource_template(template)
741+
733742
def resource(
734743
self,
735744
uri: str,

tests/server/mcpserver/resources/test_resource_manager.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def greet(name: str) -> str:
9090
return f"Hello, {name}!"
9191

9292
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
93-
manager._templates[template.uri_template] = template
93+
manager.add_resource_template(template)
9494

9595
resource = await manager.get_resource(AnyUrl("greet://world"), Context())
9696
assert isinstance(resource, FunctionResource)
@@ -106,6 +106,45 @@ async def test_get_unknown_resource():
106106
await manager.get_resource(AnyUrl("unknown://test"), Context())
107107

108108

109+
def test_add_resource_template():
110+
"""Test adding a template through add_resource_template()."""
111+
manager = ResourceManager()
112+
template = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="items")
113+
added = manager.add_resource_template(template)
114+
assert added == template
115+
assert manager.list_templates() == [template]
116+
117+
118+
def test_add_duplicate_resource_template(caplog: pytest.LogCaptureFixture):
119+
"""Adding a template whose uri_template is already registered returns the existing template and warns."""
120+
manager = ResourceManager()
121+
first = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="items")
122+
second = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="other")
123+
assert first is not second
124+
125+
added = manager.add_resource_template(first)
126+
assert added is first
127+
128+
returned = manager.add_resource_template(second)
129+
assert returned is first
130+
assert "Resource template already exists: resource://items/{id}" in caplog.text
131+
assert manager.list_templates() == [first]
132+
133+
134+
def test_disable_warn_on_duplicate_resource_templates(caplog: pytest.LogCaptureFixture):
135+
"""Adding a duplicate template does not warn when warnings are disabled."""
136+
manager = ResourceManager(warn_on_duplicate_resources=False)
137+
first = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="items")
138+
second = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="other")
139+
140+
manager.add_resource_template(first)
141+
returned = manager.add_resource_template(second)
142+
143+
assert returned is first
144+
assert "Resource template already exists" not in caplog.text
145+
assert manager.list_templates() == [first]
146+
147+
109148
def test_list_resources(temp_file: Path):
110149
"""Test listing all resources."""
111150
manager = ResourceManager()

tests/server/mcpserver/test_server.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
5454
from mcp.server.mcpserver.prompts.base import Message, UserMessage
5555
from mcp.server.mcpserver.resources import FileResource, FunctionResource
56+
from mcp.server.mcpserver.resources import ResourceTemplate as ServerResourceTemplate
5657
from mcp.server.mcpserver.utilities.types import Audio, Image
5758
from mcp.server.subscriptions import (
5859
InMemorySubscriptionBus,
@@ -1541,6 +1542,31 @@ def read_doc(path: str) -> str:
15411542
assert [t.uri_template for t in templates] == ["file://docs/{+path}"]
15421543

15431544

1545+
async def test_add_resource_template():
1546+
"""A ResourceTemplate registered via add_resource_template() is listed and readable."""
1547+
mcp = MCPServer()
1548+
1549+
def get_data(name: str) -> str:
1550+
return f"Data for {name}"
1551+
1552+
template = ServerResourceTemplate.from_function(fn=get_data, uri_template="resource://{name}/data")
1553+
mcp.add_resource_template(template)
1554+
1555+
templates = await mcp.list_resource_templates()
1556+
assert templates == snapshot(
1557+
[
1558+
ResourceTemplate(
1559+
name="get_data", uri_template="resource://{name}/data", description="", mime_type="text/plain"
1560+
)
1561+
]
1562+
)
1563+
1564+
async with Client(mcp) as client:
1565+
result = await client.read_resource("resource://test/data")
1566+
assert isinstance(result.contents[0], TextResourceContents)
1567+
assert result.contents[0].text == "Data for test"
1568+
1569+
15441570
async def test_resource_decorator_rejects_malformed_template():
15451571
mcp = MCPServer()
15461572
with pytest.raises(InvalidUriTemplate, match="Unclosed expression"):

0 commit comments

Comments
 (0)