Add resources parameter to MCPServer (#2414)

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
Matt LeMay 2026-04-12 07:42:12 -05:00 • committed by GitHub
commit c5f12ec1e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 170 additions and 185 deletions

View file

@ -22,28 +22,26 @@ logger = get_logger(__name__)
class ResourceManager: class ResourceManager:
"""Manages MCPServer resources.""" """Manages MCPServer resources."""
def __init__(self, warn_on_duplicate_resources: bool = True): def __init__(self, warn_on_duplicate_resources: bool = True, *, resources: list[Resource] | None = None):
self._resources: dict[str, Resource] = {} self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {} self._templates: dict[str, ResourceTemplate] = {}
self.warn_on_duplicate_resources = warn_on_duplicate_resources self.warn_on_duplicate_resources = warn_on_duplicate_resources
for resource in resources or ():
self.add_resource(resource)
def add_resource(self, resource: Resource) -> Resource: def add_resource(self, resource: Resource) -> Resource:
"""Add a resource to the manager. """Add a resource to the manager.
Args: Args:
resource: A Resource instance to add resource: A Resource instance to add.
Returns: Returns:
The added resource. If a resource with the same URI already exists, The added resource. If a resource with the same URI already exists, returns the existing resource.
returns the existing resource.
""" """
logger.debug( logger.debug(
"Adding resource", "Adding resource",
extra={ extra={"uri": resource.uri, "type": type(resource).__name__, "resource_name": resource.name},
"uri": resource.uri,
"type": type(resource).__name__,
"resource_name": resource.name,
},
) )
existing = self._resources.get(str(resource.uri)) existing = self._resources.get(str(resource.uri))
if existing: if existing:

View file

@ -140,6 +140,7 @@ class MCPServer(Generic[LifespanResultT]):
token_verifier: TokenVerifier | None = None, token_verifier: TokenVerifier | None = None,
*, *,
tools: list[Tool] | None = None, tools: list[Tool] | None = None,
resources: list[Resource] | None = None,
debug: bool = False, debug: bool = False,
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO", log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
warn_on_duplicate_resources: bool = True, warn_on_duplicate_resources: bool = True,
@ -162,7 +163,9 @@ class MCPServer(Generic[LifespanResultT]):
self.dependencies = self.settings.dependencies self.dependencies = self.settings.dependencies
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools) self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources) self._resource_manager = ResourceManager(
resources=resources, warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
)
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts) self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
self._lowlevel_server = Server( self._lowlevel_server = Server(
name=name or "mcp-server", name=name or "mcp-server",

View file

@ -18,15 +18,9 @@ logger = get_logger(__name__)
class ToolManager: class ToolManager:
"""Manages MCPServer tools.""" """Manages MCPServer tools."""
def __init__( def __init__(self, warn_on_duplicate_tools: bool = True, *, tools: list[Tool] | None = None):
self,
warn_on_duplicate_tools: bool = True,
*,
tools: list[Tool] | None = None,
):
self._tools: dict[str, Tool] = {} self._tools: dict[str, Tool] = {}
if tools is not None: for tool in tools or ():
for tool in tools:
if warn_on_duplicate_tools and tool.name in self._tools: if warn_on_duplicate_tools and tool.name in self._tools:
logger.warning(f"Tool already exists: {tool.name}") logger.warning(f"Tool already exists: {tool.name}")
self._tools[tool.name] = tool self._tools[tool.name] = tool

View file

@ -1,5 +1,5 @@
import logging
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile
import pytest import pytest
from pydantic import AnyUrl from pydantic import AnyUrl
@ -8,101 +8,87 @@ from mcp.server.mcpserver import Context
from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate
@pytest.fixture @pytest.fixture()
def temp_file(): def temp_file(tmp_path: Path):
"""Create a temporary file for testing. """Create a temporary file for testing.
File is automatically cleaned up after the test if it still exists. File is automatically cleaned up after the test if it still exists.
""" """
content = "test content" tmp_file = tmp_path / "file"
with NamedTemporaryFile(mode="w", delete=False) as f: tmp_file.touch()
f.write(content) yield tmp_file
path = Path(f.name).resolve()
yield path
try: # pragma: lax no cover
path.unlink()
except FileNotFoundError: # pragma: lax no cover
pass # File was already deleted by the test
class TestResourceManager: def test_init_with_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
"""Test ResourceManager functionality.""" resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
manager = ResourceManager(resources=[resource])
assert manager.list_resources() == [resource]
def test_add_resource(self, temp_file: Path): duplicate_resource = FileResource(uri=f"file://{temp_file}", name="duplicate", path=temp_file)
with caplog.at_level(logging.WARNING):
manager = ResourceManager(True, resources=[resource, duplicate_resource])
assert "Resource already exists" in caplog.text
assert manager.list_resources() == [resource]
def test_add_resource(temp_file: Path):
"""Test adding a resource.""" """Test adding a resource."""
manager = ResourceManager() manager = ResourceManager()
resource = FileResource( resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
added = manager.add_resource(resource) added = manager.add_resource(resource)
assert added == resource assert added == resource
assert manager.list_resources() == [resource] assert manager.list_resources() == [resource]
def test_add_duplicate_resource(self, temp_file: Path):
def test_add_duplicate_resource(temp_file: Path):
"""Test adding the same resource twice.""" """Test adding the same resource twice."""
manager = ResourceManager() manager = ResourceManager()
resource = FileResource( resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
first = manager.add_resource(resource) first = manager.add_resource(resource)
second = manager.add_resource(resource) second = manager.add_resource(resource)
assert first == second assert first == second
assert manager.list_resources() == [resource] assert manager.list_resources() == [resource]
def test_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
def test_warn_on_duplicate_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
"""Test warning on duplicate resources.""" """Test warning on duplicate resources."""
manager = ResourceManager() manager = ResourceManager()
resource = FileResource( resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
manager.add_resource(resource) manager.add_resource(resource)
manager.add_resource(resource) manager.add_resource(resource)
assert "Resource already exists" in caplog.text assert "Resource already exists" in caplog.text
def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
def test_disable_warn_on_duplicate_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
"""Test disabling warning on duplicate resources.""" """Test disabling warning on duplicate resources."""
manager = ResourceManager(warn_on_duplicate_resources=False) manager = ResourceManager(warn_on_duplicate_resources=False)
resource = FileResource( resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
manager.add_resource(resource) manager.add_resource(resource)
manager.add_resource(resource) manager.add_resource(resource)
assert "Resource already exists" not in caplog.text assert "Resource already exists" not in caplog.text
@pytest.mark.anyio
async def test_get_resource(self, temp_file: Path): @pytest.mark.anyio
async def test_get_resource(temp_file: Path):
"""Test getting a resource by URI.""" """Test getting a resource by URI."""
manager = ResourceManager() manager = ResourceManager()
resource = FileResource( resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
uri=f"file://{temp_file}",
name="test",
path=temp_file,
)
manager.add_resource(resource) manager.add_resource(resource)
retrieved = await manager.get_resource(resource.uri, Context()) retrieved = await manager.get_resource(resource.uri, Context())
assert retrieved == resource assert retrieved == resource
@pytest.mark.anyio
async def test_get_resource_from_template(self): @pytest.mark.anyio
async def test_get_resource_from_template():
"""Test getting a resource through a template.""" """Test getting a resource through a template."""
manager = ResourceManager() manager = ResourceManager()
def greet(name: str) -> str: def greet(name: str) -> str:
return f"Hello, {name}!" return f"Hello, {name}!"
template = ResourceTemplate.from_function( template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
fn=greet,
uri_template="greet://{name}",
name="greeter",
)
manager._templates[template.uri_template] = template manager._templates[template.uri_template] = template
resource = await manager.get_resource(AnyUrl("greet://world"), Context()) resource = await manager.get_resource(AnyUrl("greet://world"), Context())
@ -110,68 +96,46 @@ class TestResourceManager:
content = await resource.read() content = await resource.read()
assert content == "Hello, world!" assert content == "Hello, world!"
@pytest.mark.anyio
async def test_get_unknown_resource(self): @pytest.mark.anyio
async def test_get_unknown_resource():
"""Test getting a non-existent resource.""" """Test getting a non-existent resource."""
manager = ResourceManager() manager = ResourceManager()
with pytest.raises(ValueError, match="Unknown resource"): with pytest.raises(ValueError, match="Unknown resource"):
await manager.get_resource(AnyUrl("unknown://test"), Context()) await manager.get_resource(AnyUrl("unknown://test"), Context())
def test_list_resources(self, temp_file: Path):
def test_list_resources(temp_file: Path):
"""Test listing all resources.""" """Test listing all resources."""
manager = ResourceManager() manager = ResourceManager()
resource1 = FileResource( resource1 = FileResource(uri=f"file://{temp_file}", name="test1", path=temp_file)
uri=f"file://{temp_file}", resource2 = FileResource(uri=f"file://{temp_file}2", name="test2", path=temp_file)
name="test1",
path=temp_file,
)
resource2 = FileResource(
uri=f"file://{temp_file}2",
name="test2",
path=temp_file,
)
manager.add_resource(resource1) manager.add_resource(resource1)
manager.add_resource(resource2) manager.add_resource(resource2)
resources = manager.list_resources() resources = manager.list_resources()
assert len(resources) == 2 assert len(resources) == 2
assert resources == [resource1, resource2] assert resources == [resource1, resource2]
class TestResourceManagerMetadata: def get_item(id: str) -> str: ...
"""Test ResourceManager Metadata"""
def test_add_template_with_metadata(self):
def test_add_template_with_metadata():
"""Test that ResourceManager.add_template() accepts and passes meta parameter.""" """Test that ResourceManager.add_template() accepts and passes meta parameter."""
manager = ResourceManager() manager = ResourceManager()
def get_item(id: str) -> str: # pragma: no cover
return f"Item {id}"
metadata = {"source": "database", "cached": True} metadata = {"source": "database", "cached": True}
template = manager.add_template(fn=get_item, uri_template="resource://items/{id}", meta=metadata)
template = manager.add_template(
fn=get_item,
uri_template="resource://items/{id}",
meta=metadata,
)
assert template.meta is not None assert template.meta is not None
assert template.meta == metadata assert template.meta == metadata
assert template.meta["source"] == "database" assert template.meta["source"] == "database"
assert template.meta["cached"] is True assert template.meta["cached"] is True
def test_add_template_without_metadata(self):
def test_add_template_without_metadata():
"""Test that ResourceManager.add_template() works without meta parameter.""" """Test that ResourceManager.add_template() works without meta parameter."""
manager = ResourceManager() manager = ResourceManager()
template = manager.add_template(fn=get_item, uri_template="resource://items/{id}")
def get_item(id: str) -> str: # pragma: no cover
return f"Item {id}"
template = manager.add_template(
fn=get_item,
uri_template="resource://items/{id}",
)
assert template.meta is None assert template.meta is None

View file

@ -685,6 +685,32 @@ class TestServerTools:
class TestServerResources: class TestServerResources:
async def test_init_with_resources(self):
def get_text() -> str:
"""Seeded resource."""
return "Hello from init!"
resource = FunctionResource.from_function(fn=get_text, uri="resource://init", name="init_resource")
mcp = MCPServer(resources=[resource])
async with Client(mcp) as client:
assert client.initialize_result.capabilities.resources is not None
resources = await client.list_resources()
assert len(resources.resources) == 1
listed = resources.resources[0]
assert listed.uri == "resource://init"
assert listed.name == "init_resource"
assert listed.description == "Seeded resource."
result = await client.read_resource("resource://init")
assert len(result.contents) == 1
content = result.contents[0]
assert isinstance(content, TextResourceContents)
assert content.text == "Hello from init!"
async def test_text_resource(self): async def test_text_resource(self):
mcp = MCPServer() mcp = MCPServer()