Skip to content

Commit c5f12ec

Browse files
mplemayKludex
andauthored
Add resources parameter to MCPServer (#2414)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
1 parent f27d2aa commit c5f12ec

File tree

5 files changed

+170
-185
lines changed

5 files changed

+170
-185
lines changed

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,26 @@
2222
class ResourceManager:
2323
"""Manages MCPServer resources."""
2424

25-
def __init__(self, warn_on_duplicate_resources: bool = True):
25+
def __init__(self, warn_on_duplicate_resources: bool = True, *, resources: list[Resource] | None = None):
2626
self._resources: dict[str, Resource] = {}
2727
self._templates: dict[str, ResourceTemplate] = {}
2828
self.warn_on_duplicate_resources = warn_on_duplicate_resources
2929

30+
for resource in resources or ():
31+
self.add_resource(resource)
32+
3033
def add_resource(self, resource: Resource) -> Resource:
3134
"""Add a resource to the manager.
3235
3336
Args:
34-
resource: A Resource instance to add
37+
resource: A Resource instance to add.
3538
3639
Returns:
37-
The added resource. If a resource with the same URI already exists,
38-
returns the existing resource.
40+
The added resource. If a resource with the same URI already exists, returns the existing resource.
3941
"""
4042
logger.debug(
4143
"Adding resource",
42-
extra={
43-
"uri": resource.uri,
44-
"type": type(resource).__name__,
45-
"resource_name": resource.name,
46-
},
44+
extra={"uri": resource.uri, "type": type(resource).__name__, "resource_name": resource.name},
4745
)
4846
existing = self._resources.get(str(resource.uri))
4947
if existing:

src/mcp/server/mcpserver/server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def __init__(
140140
token_verifier: TokenVerifier | None = None,
141141
*,
142142
tools: list[Tool] | None = None,
143+
resources: list[Resource] | None = None,
143144
debug: bool = False,
144145
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
145146
warn_on_duplicate_resources: bool = True,
@@ -162,7 +163,9 @@ def __init__(
162163
self.dependencies = self.settings.dependencies
163164

164165
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
165-
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)
166+
self._resource_manager = ResourceManager(
167+
resources=resources, warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
168+
)
166169
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
167170
self._lowlevel_server = Server(
168171
name=name or "mcp-server",

src/mcp/server/mcpserver/tools/tool_manager.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,12 @@
1818
class ToolManager:
1919
"""Manages MCPServer tools."""
2020

21-
def __init__(
22-
self,
23-
warn_on_duplicate_tools: bool = True,
24-
*,
25-
tools: list[Tool] | None = None,
26-
):
21+
def __init__(self, warn_on_duplicate_tools: bool = True, *, tools: list[Tool] | None = None):
2722
self._tools: dict[str, Tool] = {}
28-
if tools is not None:
29-
for tool in tools:
30-
if warn_on_duplicate_tools and tool.name in self._tools:
31-
logger.warning(f"Tool already exists: {tool.name}")
32-
self._tools[tool.name] = tool
23+
for tool in tools or ():
24+
if warn_on_duplicate_tools and tool.name in self._tools:
25+
logger.warning(f"Tool already exists: {tool.name}")
26+
self._tools[tool.name] = tool
3327

3428
self.warn_on_duplicate_tools = warn_on_duplicate_tools
3529

Lines changed: 128 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import logging
12
from pathlib import Path
2-
from tempfile import NamedTemporaryFile
33

44
import pytest
55
from pydantic import AnyUrl
@@ -8,170 +8,134 @@
88
from mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate
99

1010

11-
@pytest.fixture
12-
def temp_file():
11+
@pytest.fixture()
12+
def temp_file(tmp_path: Path):
1313
"""Create a temporary file for testing.
1414
1515
File is automatically cleaned up after the test if it still exists.
1616
"""
17-
content = "test content"
18-
with NamedTemporaryFile(mode="w", delete=False) as f:
19-
f.write(content)
20-
path = Path(f.name).resolve()
21-
yield path
22-
try: # pragma: lax no cover
23-
path.unlink()
24-
except FileNotFoundError: # pragma: lax no cover
25-
pass # File was already deleted by the test
26-
27-
28-
class TestResourceManager:
29-
"""Test ResourceManager functionality."""
30-
31-
def test_add_resource(self, temp_file: Path):
32-
"""Test adding a resource."""
33-
manager = ResourceManager()
34-
resource = FileResource(
35-
uri=f"file://{temp_file}",
36-
name="test",
37-
path=temp_file,
38-
)
39-
added = manager.add_resource(resource)
40-
assert added == resource
41-
assert manager.list_resources() == [resource]
42-
43-
def test_add_duplicate_resource(self, temp_file: Path):
44-
"""Test adding the same resource twice."""
45-
manager = ResourceManager()
46-
resource = FileResource(
47-
uri=f"file://{temp_file}",
48-
name="test",
49-
path=temp_file,
50-
)
51-
first = manager.add_resource(resource)
52-
second = manager.add_resource(resource)
53-
assert first == second
54-
assert manager.list_resources() == [resource]
55-
56-
def test_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
57-
"""Test warning on duplicate resources."""
58-
manager = ResourceManager()
59-
resource = FileResource(
60-
uri=f"file://{temp_file}",
61-
name="test",
62-
path=temp_file,
63-
)
64-
manager.add_resource(resource)
65-
manager.add_resource(resource)
66-
assert "Resource already exists" in caplog.text
67-
68-
def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):
69-
"""Test disabling warning on duplicate resources."""
70-
manager = ResourceManager(warn_on_duplicate_resources=False)
71-
resource = FileResource(
72-
uri=f"file://{temp_file}",
73-
name="test",
74-
path=temp_file,
75-
)
76-
manager.add_resource(resource)
77-
manager.add_resource(resource)
78-
assert "Resource already exists" not in caplog.text
79-
80-
@pytest.mark.anyio
81-
async def test_get_resource(self, temp_file: Path):
82-
"""Test getting a resource by URI."""
83-
manager = ResourceManager()
84-
resource = FileResource(
85-
uri=f"file://{temp_file}",
86-
name="test",
87-
path=temp_file,
88-
)
89-
manager.add_resource(resource)
90-
retrieved = await manager.get_resource(resource.uri, Context())
91-
assert retrieved == resource
92-
93-
@pytest.mark.anyio
94-
async def test_get_resource_from_template(self):
95-
"""Test getting a resource through a template."""
96-
manager = ResourceManager()
97-
98-
def greet(name: str) -> str:
99-
return f"Hello, {name}!"
100-
101-
template = ResourceTemplate.from_function(
102-
fn=greet,
103-
uri_template="greet://{name}",
104-
name="greeter",
105-
)
106-
manager._templates[template.uri_template] = template
107-
108-
resource = await manager.get_resource(AnyUrl("greet://world"), Context())
109-
assert isinstance(resource, FunctionResource)
110-
content = await resource.read()
111-
assert content == "Hello, world!"
112-
113-
@pytest.mark.anyio
114-
async def test_get_unknown_resource(self):
115-
"""Test getting a non-existent resource."""
116-
manager = ResourceManager()
117-
with pytest.raises(ValueError, match="Unknown resource"):
118-
await manager.get_resource(AnyUrl("unknown://test"), Context())
119-
120-
def test_list_resources(self, temp_file: Path):
121-
"""Test listing all resources."""
122-
manager = ResourceManager()
123-
resource1 = FileResource(
124-
uri=f"file://{temp_file}",
125-
name="test1",
126-
path=temp_file,
127-
)
128-
resource2 = FileResource(
129-
uri=f"file://{temp_file}2",
130-
name="test2",
131-
path=temp_file,
132-
)
133-
manager.add_resource(resource1)
134-
manager.add_resource(resource2)
135-
resources = manager.list_resources()
136-
assert len(resources) == 2
137-
assert resources == [resource1, resource2]
138-
139-
140-
class TestResourceManagerMetadata:
141-
"""Test ResourceManager Metadata"""
142-
143-
def test_add_template_with_metadata(self):
144-
"""Test that ResourceManager.add_template() accepts and passes meta parameter."""
145-
146-
manager = ResourceManager()
147-
148-
def get_item(id: str) -> str: # pragma: no cover
149-
return f"Item {id}"
150-
151-
metadata = {"source": "database", "cached": True}
152-
153-
template = manager.add_template(
154-
fn=get_item,
155-
uri_template="resource://items/{id}",
156-
meta=metadata,
157-
)
158-
159-
assert template.meta is not None
160-
assert template.meta == metadata
161-
assert template.meta["source"] == "database"
162-
assert template.meta["cached"] is True
163-
164-
def test_add_template_without_metadata(self):
165-
"""Test that ResourceManager.add_template() works without meta parameter."""
166-
167-
manager = ResourceManager()
168-
169-
def get_item(id: str) -> str: # pragma: no cover
170-
return f"Item {id}"
171-
172-
template = manager.add_template(
173-
fn=get_item,
174-
uri_template="resource://items/{id}",
175-
)
176-
177-
assert template.meta is None
17+
tmp_file = tmp_path / "file"
18+
tmp_file.touch()
19+
yield tmp_file
20+
21+
22+
def test_init_with_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
23+
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
24+
manager = ResourceManager(resources=[resource])
25+
assert manager.list_resources() == [resource]
26+
27+
duplicate_resource = FileResource(uri=f"file://{temp_file}", name="duplicate", path=temp_file)
28+
29+
with caplog.at_level(logging.WARNING):
30+
manager = ResourceManager(True, resources=[resource, duplicate_resource])
31+
32+
assert "Resource already exists" in caplog.text
33+
assert manager.list_resources() == [resource]
34+
35+
36+
def test_add_resource(temp_file: Path):
37+
"""Test adding a resource."""
38+
manager = ResourceManager()
39+
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
40+
added = manager.add_resource(resource)
41+
assert added == resource
42+
assert manager.list_resources() == [resource]
43+
44+
45+
def test_add_duplicate_resource(temp_file: Path):
46+
"""Test adding the same resource twice."""
47+
manager = ResourceManager()
48+
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
49+
first = manager.add_resource(resource)
50+
second = manager.add_resource(resource)
51+
assert first == second
52+
assert manager.list_resources() == [resource]
53+
54+
55+
def test_warn_on_duplicate_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
56+
"""Test warning on duplicate resources."""
57+
manager = ResourceManager()
58+
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
59+
manager.add_resource(resource)
60+
manager.add_resource(resource)
61+
assert "Resource already exists" in caplog.text
62+
63+
64+
def test_disable_warn_on_duplicate_resources(temp_file: Path, caplog: pytest.LogCaptureFixture):
65+
"""Test disabling warning on duplicate resources."""
66+
manager = ResourceManager(warn_on_duplicate_resources=False)
67+
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
68+
manager.add_resource(resource)
69+
manager.add_resource(resource)
70+
assert "Resource already exists" not in caplog.text
71+
72+
73+
@pytest.mark.anyio
74+
async def test_get_resource(temp_file: Path):
75+
"""Test getting a resource by URI."""
76+
manager = ResourceManager()
77+
resource = FileResource(uri=f"file://{temp_file}", name="test", path=temp_file)
78+
manager.add_resource(resource)
79+
retrieved = await manager.get_resource(resource.uri, Context())
80+
assert retrieved == resource
81+
82+
83+
@pytest.mark.anyio
84+
async def test_get_resource_from_template():
85+
"""Test getting a resource through a template."""
86+
manager = ResourceManager()
87+
88+
def greet(name: str) -> str:
89+
return f"Hello, {name}!"
90+
91+
template = ResourceTemplate.from_function(fn=greet, uri_template="greet://{name}", name="greeter")
92+
manager._templates[template.uri_template] = template
93+
94+
resource = await manager.get_resource(AnyUrl("greet://world"), Context())
95+
assert isinstance(resource, FunctionResource)
96+
content = await resource.read()
97+
assert content == "Hello, world!"
98+
99+
100+
@pytest.mark.anyio
101+
async def test_get_unknown_resource():
102+
"""Test getting a non-existent resource."""
103+
manager = ResourceManager()
104+
with pytest.raises(ValueError, match="Unknown resource"):
105+
await manager.get_resource(AnyUrl("unknown://test"), Context())
106+
107+
108+
def test_list_resources(temp_file: Path):
109+
"""Test listing all resources."""
110+
manager = ResourceManager()
111+
resource1 = FileResource(uri=f"file://{temp_file}", name="test1", path=temp_file)
112+
resource2 = FileResource(uri=f"file://{temp_file}2", name="test2", path=temp_file)
113+
114+
manager.add_resource(resource1)
115+
manager.add_resource(resource2)
116+
117+
resources = manager.list_resources()
118+
assert len(resources) == 2
119+
assert resources == [resource1, resource2]
120+
121+
122+
def get_item(id: str) -> str: ...
123+
124+
125+
def test_add_template_with_metadata():
126+
"""Test that ResourceManager.add_template() accepts and passes meta parameter."""
127+
manager = ResourceManager()
128+
metadata = {"source": "database", "cached": True}
129+
template = manager.add_template(fn=get_item, uri_template="resource://items/{id}", meta=metadata)
130+
131+
assert template.meta is not None
132+
assert template.meta == metadata
133+
assert template.meta["source"] == "database"
134+
assert template.meta["cached"] is True
135+
136+
137+
def test_add_template_without_metadata():
138+
"""Test that ResourceManager.add_template() works without meta parameter."""
139+
manager = ResourceManager()
140+
template = manager.add_template(fn=get_item, uri_template="resource://items/{id}")
141+
assert template.meta is None

0 commit comments

Comments
 (0)