Add resources parameter to MCPServer (#2414)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
This commit is contained in:
parent
f27d2aac05
commit
c5f12ec1e9
2 changed files with 170 additions and 185 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import pytest
|
||||
from pydantic import AnyUrl
|
||||
|
|
@ -8,170 +8,134 @@ from mcp.server.mcpserver import Context
|
|||
from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_file():
|
||||
@pytest.fixture()
|
||||
def temp_file(tmp_path: Path):
|
||||
"""Create a temporary file for testing.
|
||||
|
||||
File is automatically cleaned up after the test if it still exists.
|
||||
"""
|
||||
content = "test content"
|
||||
with NamedTemporaryFile(mode="w", delete=False) as f:
|
||||
f.write(content)
|
||||
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
|
||||
tmp_file = tmp_path / "file"
|
||||
tmp_file.touch()
|
||||
yield tmp_file
|
||||
|
||||
|
||||
class TestResourceManager:
|
||||
"""Test ResourceManager functionality."""
|
||||
def test_init_with_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
|
||||
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):
|
||||
"""Test adding a resource."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(
|
||||
uri=f"file://{temp_file}",
|
||||
name="test",
|
||||
path=temp_file,
|
||||
)
|
||||
added = manager.add_resource(resource)
|
||||
assert added == resource
|
||||
assert manager.list_resources() == [resource]
|
||||
duplicate_resource = FileResource(uri=f"file://{temp_file}", name="duplicate", path=temp_file)
|
||||
|
||||
def test_add_duplicate_resource(self, temp_file: Path):
|
||||
"""Test adding the same resource twice."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(
|
||||
uri=f"file://{temp_file}",
|
||||
name="test",
|
||||
path=temp_file,
|
||||
)
|
||||
first = manager.add_resource(resource)
|
||||
second = manager.add_resource(resource)
|
||||
assert first == second
|
||||
assert manager.list_resources() == [resource]
|
||||
with caplog.at_level(logging.WARNING):
|
||||
manager = ResourceManager(True, resources=[resource, duplicate_resource])
|
||||
|
||||
def test_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
|
||||
"""Test warning on duplicate resources."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(
|
||||
uri=f"file://{temp_file}",
|
||||
name="test",
|
||||
path=temp_file,
|
||||
)
|
||||
manager.add_resource(resource)
|
||||
manager.add_resource(resource)
|
||||
assert "Resource already exists" in caplog.text
|
||||
|
||||
def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
|
||||
"""Test disabling warning on duplicate resources."""
|
||||
manager = ResourceManager(warn_on_duplicate_resources=False)
|
||||
resource = FileResource(
|
||||
uri=f"file://{temp_file}",
|
||||
name="test",
|
||||
path=temp_file,
|
||||
)
|
||||
manager.add_resource(resource)
|
||||
manager.add_resource(resource)
|
||||
assert "Resource already exists" not in caplog.text
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_resource(self, temp_file: Path):
|
||||
"""Test getting a resource by URI."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(
|
||||
uri=f"file://{temp_file}",
|
||||
name="test",
|
||||
path=temp_file,
|
||||
)
|
||||
manager.add_resource(resource)
|
||||
retrieved = await manager.get_resource(resource.uri, Context())
|
||||
assert retrieved == resource
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_resource_from_template(self):
|
||||
"""Test getting a resource through a template."""
|
||||
manager = ResourceManager()
|
||||
|
||||
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
|
||||
|
||||
resource = await manager.get_resource(AnyUrl("greet://world"), Context())
|
||||
assert isinstance(resource, FunctionResource)
|
||||
content = await resource.read()
|
||||
assert content == "Hello, world!"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_unknown_resource(self):
|
||||
"""Test getting a non-existent resource."""
|
||||
manager = ResourceManager()
|
||||
with pytest.raises(ValueError, match="Unknown resource"):
|
||||
await manager.get_resource(AnyUrl("unknown://test"), Context())
|
||||
|
||||
def test_list_resources(self, temp_file: Path):
|
||||
"""Test listing all resources."""
|
||||
manager = ResourceManager()
|
||||
resource1 = FileResource(
|
||||
uri=f"file://{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(resource2)
|
||||
resources = manager.list_resources()
|
||||
assert len(resources) == 2
|
||||
assert resources == [resource1, resource2]
|
||||
assert "Resource already exists" in caplog.text
|
||||
assert manager.list_resources() == [resource]
|
||||
|
||||
|
||||
class TestResourceManagerMetadata:
|
||||
"""Test ResourceManager Metadata"""
|
||||
def test_add_resource(temp_file: Path):
|
||||
"""Test adding a resource."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
|
||||
added = manager.add_resource(resource)
|
||||
assert added == resource
|
||||
assert manager.list_resources() == [resource]
|
||||
|
||||
def test_add_template_with_metadata(self):
|
||||
"""Test that ResourceManager.add_template() accepts and passes meta parameter."""
|
||||
|
||||
manager = ResourceManager()
|
||||
def test_add_duplicate_resource(temp_file: Path):
|
||||
"""Test adding the same resource twice."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
|
||||
first = manager.add_resource(resource)
|
||||
second = manager.add_resource(resource)
|
||||
assert first == second
|
||||
assert manager.list_resources() == [resource]
|
||||
|
||||
def get_item(id: str) -> str: # pragma: no cover
|
||||
return f"Item {id}"
|
||||
|
||||
metadata = {"source": "database", "cached": True}
|
||||
def test_warn_on_duplicate_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
|
||||
"""Test warning on duplicate resources."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
|
||||
manager.add_resource(resource)
|
||||
manager.add_resource(resource)
|
||||
assert "Resource already exists" in caplog.text
|
||||
|
||||
template = manager.add_template(
|
||||
fn=get_item,
|
||||
uri_template="resource://items/{id}",
|
||||
meta=metadata,
|
||||
)
|
||||
|
||||
assert template.meta is not None
|
||||
assert template.meta == metadata
|
||||
assert template.meta["source"] == "database"
|
||||
assert template.meta["cached"] is True
|
||||
def test_disable_warn_on_duplicate_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
|
||||
"""Test disabling warning on duplicate resources."""
|
||||
manager = ResourceManager(warn_on_duplicate_resources=False)
|
||||
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
|
||||
manager.add_resource(resource)
|
||||
manager.add_resource(resource)
|
||||
assert "Resource already exists" not in caplog.text
|
||||
|
||||
def test_add_template_without_metadata(self):
|
||||
"""Test that ResourceManager.add_template() works without meta parameter."""
|
||||
|
||||
manager = ResourceManager()
|
||||
@pytest.mark.anyio
|
||||
async def test_get_resource(temp_file: Path):
|
||||
"""Test getting a resource by URI."""
|
||||
manager = ResourceManager()
|
||||
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
|
||||
manager.add_resource(resource)
|
||||
retrieved = await manager.get_resource(resource.uri, Context())
|
||||
assert retrieved == resource
|
||||
|
||||
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}",
|
||||
)
|
||||
@pytest.mark.anyio
|
||||
async def test_get_resource_from_template():
|
||||
"""Test getting a resource through a template."""
|
||||
manager = ResourceManager()
|
||||
|
||||
assert template.meta is None
|
||||
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
|
||||
|
||||
resource = await manager.get_resource(AnyUrl("greet://world"), Context())
|
||||
assert isinstance(resource, FunctionResource)
|
||||
content = await resource.read()
|
||||
assert content == "Hello, world!"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_unknown_resource():
|
||||
"""Test getting a non-existent resource."""
|
||||
manager = ResourceManager()
|
||||
with pytest.raises(ValueError, match="Unknown resource"):
|
||||
await manager.get_resource(AnyUrl("unknown://test"), Context())
|
||||
|
||||
|
||||
def test_list_resources(temp_file: Path):
|
||||
"""Test listing all resources."""
|
||||
manager = ResourceManager()
|
||||
resource1 = FileResource(uri=f"file://{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(resource2)
|
||||
|
||||
resources = manager.list_resources()
|
||||
assert len(resources) == 2
|
||||
assert resources == [resource1, resource2]
|
||||
|
||||
|
||||
def get_item(id: str) -> str: ...
|
||||
|
||||
|
||||
def test_add_template_with_metadata():
|
||||
"""Test that ResourceManager.add_template() accepts and passes meta parameter."""
|
||||
manager = ResourceManager()
|
||||
metadata = {"source": "database", "cached": True}
|
||||
template = manager.add_template(fn=get_item, uri_template="resource://items/{id}", meta=metadata)
|
||||
|
||||
assert template.meta is not None
|
||||
assert template.meta == metadata
|
||||
assert template.meta["source"] == "database"
|
||||
assert template.meta["cached"] is True
|
||||
|
||||
|
||||
def test_add_template_without_metadata():
|
||||
"""Test that ResourceManager.add_template() works without meta parameter."""
|
||||
manager = ResourceManager()
|
||||
template = manager.add_template(fn=get_item, uri_template="resource://items/{id}")
|
||||
assert template.meta is None
|
||||
|
|
|
|||
|
|
@ -685,6 +685,32 @@ class TestServerTools:
|
|||
|
||||
|
||||
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):
|
||||
mcp = MCPServer()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue