Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/servers/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ The same rule applies to anything else JSON-serialisable: a list, a Pydantic mod
`BinaryResource`, `FileResource`, `HttpResource`, `DirectoryResource`) that you register
with `mcp.add_resource(...)`.

Template resources can be registered programmatically too: build a `ResourceTemplate`
with `ResourceTemplate.from_function(...)` and call `mcp.add_resource_template(...)`
instead of decorating a function.

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)**.

## Recap
Expand Down
25 changes: 23 additions & 2 deletions src/mcp/server/mcpserver/resources/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ def add_resource(self, resource: Resource) -> Resource:
self._resources[str(resource.uri)] = resource
return resource

def add_resource_template(self, template: ResourceTemplate) -> ResourceTemplate:
"""Add a resource template to the manager.

Args:
template: A ResourceTemplate instance to add.

Returns:
The added template. If a template with the same uri_template
already exists, returns the existing template.
"""
logger.debug(
"Adding resource template",
extra={"uri_template": template.uri_template, "name": template.name},
)
existing = self._templates.get(template.uri_template)
if existing:
if self.warn_on_duplicate_resources:
logger.warning(f"Resource template already exists: {template.uri_template}")
return existing
self._templates[template.uri_template] = template
return template

def add_template(
self,
fn: Callable[..., Any],
Expand All @@ -83,8 +105,7 @@ def add_template(
meta=meta,
security=security,
)
self._templates[template.uri_template] = template
return template
return self.add_resource_template(template)

async def get_resource(
self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]
Expand Down
9 changes: 9 additions & 0 deletions src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
Resource,
ResourceManager,
ResourceSecurity,
ResourceTemplate,
)
from mcp.server.mcpserver.tools import Tool, ToolManager
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
Expand Down Expand Up @@ -730,6 +731,14 @@ def add_resource(self, resource: Resource) -> None:
"""
self._resource_manager.add_resource(resource)

def add_resource_template(self, template: ResourceTemplate) -> None:
"""Add a resource template to the server.

Args:
template: A ResourceTemplate instance to add.
"""
self._resource_manager.add_resource_template(template)

def resource(
self,
uri: str,
Expand Down
41 changes: 40 additions & 1 deletion tests/server/mcpserver/resources/test_resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def greet(name: str) -> str:
return f"Hello, {name}!"

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

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


def test_add_resource_template():
"""Test adding a template through add_resource_template()."""
manager = ResourceManager()
template = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="items")
added = manager.add_resource_template(template)
assert added == template
assert manager.list_templates() == [template]


def test_add_duplicate_resource_template(caplog: pytest.LogCaptureFixture):
"""Adding a template whose uri_template is already registered returns the existing template and warns."""
manager = ResourceManager()
first = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="items")
second = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="other")
assert first is not second

added = manager.add_resource_template(first)
assert added is first

returned = manager.add_resource_template(second)
assert returned is first
assert "Resource template already exists: resource://items/{id}" in caplog.text
assert manager.list_templates() == [first]


def test_disable_warn_on_duplicate_resource_templates(caplog: pytest.LogCaptureFixture):
"""Adding a duplicate template does not warn when warnings are disabled."""
manager = ResourceManager(warn_on_duplicate_resources=False)
first = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="items")
second = ResourceTemplate.from_function(fn=get_item, uri_template="resource://items/{id}", name="other")

manager.add_resource_template(first)
returned = manager.add_resource_template(second)

assert returned is first
assert "Resource template already exists" not in caplog.text
assert manager.list_templates() == [first]


def test_list_resources(temp_file: Path):
"""Test listing all resources."""
manager = ResourceManager()
Expand Down
26 changes: 26 additions & 0 deletions tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
from mcp.server.mcpserver.prompts.base import Message, UserMessage
from mcp.server.mcpserver.resources import FileResource, FunctionResource
from mcp.server.mcpserver.resources import ResourceTemplate as ServerResourceTemplate
from mcp.server.mcpserver.utilities.types import Audio, Image
from mcp.server.subscriptions import (
InMemorySubscriptionBus,
Expand Down Expand Up @@ -1541,6 +1542,31 @@ def read_doc(path: str) -> str:
assert [t.uri_template for t in templates] == ["file://docs/{+path}"]


async def test_add_resource_template():
"""A ResourceTemplate registered via add_resource_template() is listed and readable."""
mcp = MCPServer()

def get_data(name: str) -> str:
return f"Data for {name}"

template = ServerResourceTemplate.from_function(fn=get_data, uri_template="resource://{name}/data")
mcp.add_resource_template(template)

templates = await mcp.list_resource_templates()
assert templates == snapshot(
[
ResourceTemplate(
name="get_data", uri_template="resource://{name}/data", description="", mime_type="text/plain"
)
]
)

async with Client(mcp) as client:
result = await client.read_resource("resource://test/data")
assert isinstance(result.contents[0], TextResourceContents)
assert result.contents[0].text == "Data for test"


async def test_resource_decorator_rejects_malformed_template():
mcp = MCPServer()
with pytest.raises(InvalidUriTemplate, match="Unclosed expression"):
Expand Down
Loading