Jeremiah Lowin commited on
Commit
7ea3e33
·
1 Parent(s): d92cb3f

Update type handling for resources

Browse files
examples/desktop.py CHANGED
@@ -10,22 +10,21 @@ from pathlib import Path
10
  from fastmcp.server import FastMCP
11
 
12
  # Create server
13
- app = FastMCP("desktop")
14
 
15
- # Add desktop as a directory resource
16
- desktop = Path.home() / "Desktop"
17
- app.add_dir_resource(
18
- str(desktop),
19
- recursive=True,
20
- name="Desktop",
21
- description="Files on the desktop",
22
- )
23
 
 
 
 
 
 
24
 
25
- def main123():
26
- # Run the server
27
- asyncio.run(FastMCP.run_stdio(app))
 
 
28
 
29
 
30
  if __name__ == "__main__":
31
- main123()
 
10
  from fastmcp.server import FastMCP
11
 
12
  # Create server
13
+ mcp = FastMCP("desktop")
14
 
 
 
 
 
 
 
 
 
15
 
16
+ @mcp.resource("desktop")
17
+ def desktop() -> list[str]:
18
+ """List the files in the desktop directory"""
19
+ desktop = Path.home() / "Desktop"
20
+ return [str(f) for f in desktop.iterdir()]
21
 
22
+
23
+ @mcp.tool()
24
+ def add(a: int, b: int) -> int:
25
+ """Add two numbers"""
26
+ return a + b
27
 
28
 
29
  if __name__ == "__main__":
30
+ asyncio.run(FastMCP.run_stdio(mcp))
src/fastmcp/resources.py CHANGED
@@ -1,14 +1,14 @@
1
- """Resource management for FastMCP."""
2
 
3
  import abc
4
  import asyncio
5
  import json
6
  from pathlib import Path
7
- from typing import Dict, Optional, Callable, Any
8
- from urllib.parse import parse_qs, urlparse
9
 
10
  import httpx
11
  from pydantic import BaseModel, field_validator
 
12
 
13
  from .utilities.logging import get_logger
14
 
@@ -18,26 +18,22 @@ logger = get_logger(__name__)
18
  class Resource(BaseModel):
19
  """Base class for all resources."""
20
 
21
- uri: str
22
  name: str
23
  description: Optional[str] = None
24
  mime_type: str = "text/plain"
25
 
26
- @field_validator("uri")
27
  @classmethod
28
- def validate_uri_format(cls, uri: str) -> str:
29
- """Validate URI follows [protocol]://[host]/[path] format."""
30
- parsed = urlparse(uri)
31
-
32
- # Check protocol exists and is not empty
33
- if not parsed.scheme:
34
- raise ValueError("URI must have a protocol (e.g., 'http://', 'file://')")
35
-
36
- # Check host exists and is not empty
37
- if not parsed.netloc and not parsed.path:
38
- raise ValueError("URI must have a host or path")
39
-
40
- return uri
41
 
42
  @abc.abstractmethod
43
  async def read(self) -> str:
@@ -48,33 +44,35 @@ class Resource(BaseModel):
48
  class FunctionResource(Resource):
49
  """A resource that is generated by a function call.
50
 
51
- The function is called with kwargs parsed from the URI query string.
52
- For example, a URI of "fn://my_func?x=1&y=2" will call the function with
53
- kwargs {"x": "1", "y": "2"}.
54
  """
55
 
56
- func: Callable[..., Any]
 
57
 
58
- def _parse_uri_params(self) -> Dict[str, str]:
59
- """Parse URI query string into kwargs."""
60
- parsed = urlparse(self.uri)
61
- if not parsed.query:
62
- return {}
63
- # parse_qs returns Dict[str, List[str]], we want Dict[str, str]
64
- params = parse_qs(parsed.query)
65
- return {k: v[0] for k, v in params.items()}
66
 
67
  async def read(self) -> str:
68
- """Read the resource content by calling the function with URI params."""
69
  try:
70
- kwargs = self._parse_uri_params()
71
- result = await asyncio.to_thread(self.func, **kwargs)
 
 
 
 
72
  if isinstance(result, Resource):
73
  return await result.read()
74
  if isinstance(result, bytes):
75
  return result.decode()
76
  if not isinstance(result, str):
77
- return str(result)
 
 
 
78
  return result
79
  except Exception as e:
80
  raise ValueError(f"Error calling function {self.func.__name__}: {e}")
@@ -179,33 +177,14 @@ class ResourceManager:
179
  self._resources: Dict[str, Resource] = {}
180
  self.warn_on_duplicate_resources = warn_on_duplicate_resources
181
 
182
- def get_resource(self, uri: str) -> Optional[Resource]:
183
- """Get resource by URI.
184
-
185
- First tries to find an exact match for the URI. If none is found,
186
- tries to match against any FunctionResources with wildcard patterns.
187
- """
188
  logger.debug("Getting resource", extra={"uri": uri})
189
 
190
- # First try exact match
191
  if resource := self._resources.get(uri):
192
  return resource
193
 
194
- # Then try pattern matching for FunctionResources
195
- for resource in self._resources.values():
196
- if isinstance(resource, FunctionResource) and hasattr(
197
- resource, "uri_regex"
198
- ):
199
- if resource.uri_regex.match(uri):
200
- # Create a new instance with the actual URI
201
- return FunctionResource(
202
- uri=uri, # Use actual URI
203
- name=resource.name,
204
- description=resource.description,
205
- mime_type=resource.mime_type,
206
- func=resource.func,
207
- )
208
-
209
  raise ValueError(f"Unknown resource: {uri}")
210
 
211
  def list_resources(self) -> list[Resource]:
@@ -231,10 +210,10 @@ class ResourceManager:
231
  "name": resource.name,
232
  },
233
  )
234
- existing = self._resources.get(resource.uri)
235
  if existing:
236
  if self.warn_on_duplicate_resources:
237
  logger.warning(f"Resource already exists: {resource.uri}")
238
  return existing
239
- self._resources[resource.uri] = resource
240
  return resource
 
1
+ import pydantic.json
2
 
3
  import abc
4
  import asyncio
5
  import json
6
  from pathlib import Path
7
+ from typing import Dict, Optional, Callable, Any, Union, Awaitable
 
8
 
9
  import httpx
10
  from pydantic import BaseModel, field_validator
11
+ from pydantic.networks import _BaseUrl
12
 
13
  from .utilities.logging import get_logger
14
 
 
18
  class Resource(BaseModel):
19
  """Base class for all resources."""
20
 
21
+ uri: _BaseUrl
22
  name: str
23
  description: Optional[str] = None
24
  mime_type: str = "text/plain"
25
 
26
+ @field_validator("name", mode="before")
27
  @classmethod
28
+ def set_default_name(cls, name: str | None, info) -> str:
29
+ """Set default name from URI if not provided."""
30
+ if name is not None:
31
+ return name
32
+ # Extract everything after the protocol (e.g., "desktop" from "resource://desktop")
33
+ uri = info.data.get("uri")
34
+ if uri:
35
+ return str(uri).split("://", 1)[1]
36
+ raise ValueError("Either name or uri must be provided")
 
 
 
 
37
 
38
  @abc.abstractmethod
39
  async def read(self) -> str:
 
44
  class FunctionResource(Resource):
45
  """A resource that is generated by a function call.
46
 
47
+ The function can be sync or async and must return a string
48
+ or another Resource.
 
49
  """
50
 
51
+ func: Union[Callable[[], Any], Callable[[], Awaitable[Any]]]
52
+ is_async: bool = False
53
 
54
+ def __init__(self, **data):
55
+ super().__init__(**data)
56
+ self.is_async = asyncio.iscoroutinefunction(self.func)
 
 
 
 
 
57
 
58
  async def read(self) -> str:
59
+ """Read the resource content by calling the function."""
60
  try:
61
+ result = (
62
+ await self.func()
63
+ if self.is_async
64
+ else await asyncio.to_thread(self.func)
65
+ )
66
+
67
  if isinstance(result, Resource):
68
  return await result.read()
69
  if isinstance(result, bytes):
70
  return result.decode()
71
  if not isinstance(result, str):
72
+ try:
73
+ return json.dumps(result, default=pydantic.json.pydantic_encoder)
74
+ except json.JSONDecodeError:
75
+ return str(result)
76
  return result
77
  except Exception as e:
78
  raise ValueError(f"Error calling function {self.func.__name__}: {e}")
 
177
  self._resources: Dict[str, Resource] = {}
178
  self.warn_on_duplicate_resources = warn_on_duplicate_resources
179
 
180
+ def get_resource(self, uri: Union[_BaseUrl, str]) -> Optional[Resource]:
181
+ """Get resource by URI."""
182
+ uri = str(uri)
 
 
 
183
  logger.debug("Getting resource", extra={"uri": uri})
184
 
 
185
  if resource := self._resources.get(uri):
186
  return resource
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  raise ValueError(f"Unknown resource: {uri}")
189
 
190
  def list_resources(self) -> list[Resource]:
 
210
  "name": resource.name,
211
  },
212
  )
213
+ existing = self._resources.get(str(resource.uri))
214
  if existing:
215
  if self.warn_on_duplicate_resources:
216
  logger.warning(f"Resource already exists: {resource.uri}")
217
  return existing
218
+ self._resources[str(resource.uri)] = resource
219
  return resource
src/fastmcp/server.py CHANGED
@@ -17,6 +17,7 @@ from .exceptions import ResourceError
17
  from .resources import Resource, FunctionResource, ResourceManager
18
  from .tools import ToolManager
19
  from .utilities.logging import get_logger, configure_logging
 
20
 
21
  logger = get_logger(__name__)
22
 
@@ -56,15 +57,20 @@ class FastMCP:
56
  warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
57
  )
58
 
 
 
 
59
  # Configure logging
60
  configure_logging(self.settings.log_level)
61
 
62
- self._setup_handlers()
63
-
64
  @property
65
  def name(self) -> str:
66
  return self._mcp_server.name
67
 
 
 
 
 
68
  def _setup_handlers(self) -> None:
69
  """Set up core MCP protocol handlers."""
70
  self._mcp_server.list_tools()(self.list_tools)
@@ -104,7 +110,7 @@ class FastMCP:
104
  for resource in resources
105
  ]
106
 
107
- async def read_resource(self, uri: str) -> Union[str, bytes]:
108
  """Read a resource by URI."""
109
  resource = self._resource_manager.get_resource(uri)
110
  if not resource:
@@ -170,42 +176,40 @@ class FastMCP:
170
 
171
  def resource(
172
  self,
173
- name: str,
174
  *,
 
175
  description: Optional[str] = None,
176
  mime_type: Optional[str] = None,
177
  ) -> Callable:
178
- """Decorator to register a function as a dynamic resource.
179
 
180
- The function will be called with kwargs parsed from the URI query string.
181
- For example, a URI of "fn://my_func?x=1&y=2" will call the function with
182
- kwargs {"x": "1", "y": "2"}.
183
 
184
  Args:
185
- name: Name for the resource (used in fn:// URI)
186
  description: Optional description of the resource
187
  mime_type: Optional MIME type for the resource
188
 
189
  Example:
190
- @server.resource("my_func")
191
- def get_data(x: str, y: str) -> str:
192
- # Called with fn://my_func?x=1&y=2
193
- return f"x={x}, y={y}"
194
  """
195
  # Check if user passed function directly instead of calling decorator
196
- if callable(name):
197
  raise TypeError(
198
  "The @resource decorator was used incorrectly. "
199
- "Did you forget to call it? Use @resource('name') instead of @resource"
200
  )
201
 
202
  def decorator(func: Callable) -> Callable:
203
  @functools.wraps(func)
204
- def wrapper(**kwargs) -> Any:
205
- return func(**kwargs)
206
 
207
  resource = FunctionResource(
208
- uri=f"fn://{name}", # Base URI, params added when called
209
  name=name,
210
  description=description,
211
  mime_type=mime_type or "text/plain",
@@ -216,10 +220,6 @@ class FastMCP:
216
 
217
  return decorator
218
 
219
- async def run(self, *args, **kwargs) -> None:
220
- """Run the FastMCP server."""
221
- await self._mcp_server.run(*args, **kwargs)
222
-
223
  @classmethod
224
  async def run_stdio(cls, app: "FastMCP") -> None:
225
  """Run the server using stdio transport."""
 
17
  from .resources import Resource, FunctionResource, ResourceManager
18
  from .tools import ToolManager
19
  from .utilities.logging import get_logger, configure_logging
20
+ from pydantic.networks import _BaseUrl
21
 
22
  logger = get_logger(__name__)
23
 
 
57
  warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
58
  )
59
 
60
+ # Set up MCP protocol handlers
61
+ self._setup_handlers()
62
+
63
  # Configure logging
64
  configure_logging(self.settings.log_level)
65
 
 
 
66
  @property
67
  def name(self) -> str:
68
  return self._mcp_server.name
69
 
70
+ async def run(self, *args, **kwargs) -> None:
71
+ """Run the FastMCP server."""
72
+ await self._mcp_server.run(*args, **kwargs)
73
+
74
  def _setup_handlers(self) -> None:
75
  """Set up core MCP protocol handlers."""
76
  self._mcp_server.list_tools()(self.list_tools)
 
110
  for resource in resources
111
  ]
112
 
113
+ async def read_resource(self, uri: _BaseUrl) -> Union[str, bytes]:
114
  """Read a resource by URI."""
115
  resource = self._resource_manager.get_resource(uri)
116
  if not resource:
 
176
 
177
  def resource(
178
  self,
179
+ uri: str,
180
  *,
181
+ name: Optional[str] = None,
182
  description: Optional[str] = None,
183
  mime_type: Optional[str] = None,
184
  ) -> Callable:
185
+ """Decorator to register a function as a resource.
186
 
187
+ The function will be called when the resource is read to generate its content.
 
 
188
 
189
  Args:
190
+ uri: URI for the resource (e.g. "resource://my-resource")
191
  description: Optional description of the resource
192
  mime_type: Optional MIME type for the resource
193
 
194
  Example:
195
+ @server.resource("resource://my-resource")
196
+ def get_data() -> str:
197
+ return "Hello, world!"
 
198
  """
199
  # Check if user passed function directly instead of calling decorator
200
+ if callable(uri):
201
  raise TypeError(
202
  "The @resource decorator was used incorrectly. "
203
+ "Did you forget to call it? Use @resource('uri') instead of @resource"
204
  )
205
 
206
  def decorator(func: Callable) -> Callable:
207
  @functools.wraps(func)
208
+ def wrapper() -> Any:
209
+ return func()
210
 
211
  resource = FunctionResource(
212
+ uri=uri,
213
  name=name,
214
  description=description,
215
  mime_type=mime_type or "text/plain",
 
220
 
221
  return decorator
222
 
 
 
 
 
223
  @classmethod
224
  async def run_stdio(cls, app: "FastMCP") -> None:
225
  """Run the server using stdio transport."""
src/fastmcp/tools.py CHANGED
@@ -3,7 +3,7 @@
3
  import inspect
4
  from typing import Any, Callable, Dict, Optional
5
 
6
- from pydantic import BaseModel, Field, TypeAdapter
7
 
8
  from .exceptions import ToolError
9
  from .utilities.logging import get_logger
@@ -37,13 +37,16 @@ class Tool(BaseModel):
37
  is_async = inspect.iscoroutinefunction(func)
38
 
39
  # Get schema from TypeAdapter - will fail if function isn't properly typed
40
- schema = TypeAdapter(func).json_schema()
 
 
 
41
 
42
  return cls(
43
  func=func,
44
  name=func_name,
45
  description=func_doc,
46
- parameters=schema,
47
  is_async=is_async,
48
  )
49
 
@@ -94,4 +97,5 @@ class ToolManager:
94
  tool = self.get_tool(name)
95
  if not tool:
96
  raise ToolError(f"Unknown tool: {name}")
 
97
  return await tool.run(arguments)
 
3
  import inspect
4
  from typing import Any, Callable, Dict, Optional
5
 
6
+ from pydantic import BaseModel, Field, TypeAdapter, validate_call
7
 
8
  from .exceptions import ToolError
9
  from .utilities.logging import get_logger
 
37
  is_async = inspect.iscoroutinefunction(func)
38
 
39
  # Get schema from TypeAdapter - will fail if function isn't properly typed
40
+ parameters = TypeAdapter(func).json_schema()
41
+
42
+ # ensure the arguments are properly cast
43
+ func = validate_call(func)
44
 
45
  return cls(
46
  func=func,
47
  name=func_name,
48
  description=func_doc,
49
+ parameters=parameters,
50
  is_async=is_async,
51
  )
52
 
 
97
  tool = self.get_tool(name)
98
  if not tool:
99
  raise ToolError(f"Unknown tool: {name}")
100
+
101
  return await tool.run(arguments)
tests/__init__.py ADDED
File without changes
tests/servers/__init__.py ADDED
File without changes
tests/servers/test_file_browser.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastmcp import FastMCP
3
+ import pytest
4
+ from pathlib import Path
5
+
6
+
7
+ @pytest.fixture(scope="session")
8
+ def test_dir(tmp_path_factory) -> Path:
9
+ """Create a temporary directory with test files."""
10
+ tmp = tmp_path_factory.mktemp("test_files")
11
+
12
+ # Create test files
13
+ (tmp / "example.py").write_text("print('hello world')")
14
+ (tmp / "readme.md").write_text("# Test Directory\nThis is a test.")
15
+ (tmp / "config.json").write_text('{"test": true}')
16
+
17
+ return tmp
18
+
19
+
20
+ @pytest.fixture
21
+ def mcp(test_dir: Path) -> FastMCP:
22
+ mcp = FastMCP()
23
+
24
+ @mcp.resource("fs://test_dir")
25
+ def list_files() -> list[str]:
26
+ """List the files in the test directory"""
27
+ return [str(f) for f in test_dir.iterdir()]
28
+
29
+ return mcp
30
+
31
+
32
+ async def test_list_resources(mcp: FastMCP):
33
+ resources = await mcp.list_resources()
34
+ assert len(resources) == 1
35
+ assert str(resources[0].uri) == "fs://test_dir"
36
+ assert resources[0].name == "test_dir"
37
+
38
+
39
+ async def test_read_resource(mcp: FastMCP):
40
+ files = await mcp.read_resource("fs://test_dir")
41
+ files = json.loads(files)
42
+
43
+ assert isinstance(files, list)
44
+ assert len(files) == 3
45
+ assert any("example.py" in f for f in files)
46
+ assert any("readme.md" in f for f in files)
47
+ assert any("config.json" in f for f in files)
tests/test_resource_manager.py CHANGED
@@ -54,10 +54,10 @@ class TestResourceValidation:
54
  name="test",
55
  func=dummy_func,
56
  )
57
- assert resource.uri == "http://example.com/data"
58
 
59
  # Missing protocol
60
- with pytest.raises(ValueError, match="URI must have a protocol"):
61
  FunctionResource(
62
  uri="invalid",
63
  name="test",
@@ -65,7 +65,7 @@ class TestResourceValidation:
65
  )
66
 
67
  # Missing host
68
- with pytest.raises(ValueError, match="URI must have a host"):
69
  FunctionResource(
70
  uri="http://",
71
  name="test",
@@ -85,7 +85,7 @@ class TestFileResource:
85
  mime_type="text/plain",
86
  path=temp_file,
87
  )
88
- assert resource.uri == f"file://{temp_file}"
89
  assert resource.name == "test"
90
  assert resource.description == "test file"
91
  assert resource.mime_type == "text/plain"
@@ -162,13 +162,13 @@ class TestFunctionResource:
162
  mime_type="text/plain",
163
  func=my_func,
164
  )
165
- assert resource.uri == "fn://test"
166
  assert resource.name == "test"
167
  assert resource.description == "test function"
168
  assert resource.mime_type == "text/plain"
169
  assert resource.func == my_func
170
 
171
- async def test_function_resource_read_no_params(self):
172
  """Test reading a FunctionResource with no parameters."""
173
 
174
  def my_func() -> str:
@@ -182,86 +182,6 @@ class TestFunctionResource:
182
  content = await resource.read()
183
  assert content == "test content"
184
 
185
- async def test_function_resource_read_with_params(self):
186
- """Test reading a FunctionResource with query parameters."""
187
-
188
- def my_func(x: str, y: str) -> str:
189
- return f"x={x}, y={y}"
190
-
191
- resource = FunctionResource(
192
- uri="fn://test?x=1&y=2",
193
- name="test",
194
- func=my_func,
195
- )
196
- content = await resource.read()
197
- assert content == "x=1, y=2"
198
-
199
- async def test_function_resource_read_returns_resource(self, temp_file: Path):
200
- """Test reading a FunctionResource that returns another Resource."""
201
-
202
- def my_func(name: str = "test") -> FileResource:
203
- return FileResource(
204
- uri=f"file://{temp_file}",
205
- name=name,
206
- path=temp_file,
207
- )
208
-
209
- resource = FunctionResource(
210
- uri="fn://test?name=example",
211
- name="test",
212
- func=my_func,
213
- )
214
- content = await resource.read()
215
- assert content == "test content"
216
-
217
- async def test_function_resource_read_error(self):
218
- """Test error handling when reading a FunctionResource."""
219
-
220
- def my_func(x: str) -> str:
221
- raise ValueError(f"test error: {x}")
222
-
223
- resource = FunctionResource(
224
- uri="fn://test?x=bad",
225
- name="test",
226
- func=my_func,
227
- )
228
- with pytest.raises(
229
- ValueError, match="Error calling function my_func: test error: bad"
230
- ):
231
- await resource.read()
232
-
233
- def test_parse_uri_params(self):
234
- """Test parsing URI parameters."""
235
-
236
- def my_func() -> str:
237
- return "test"
238
-
239
- resource = FunctionResource(
240
- uri="fn://test?x=1&y=hello&z=true",
241
- name="test",
242
- func=my_func,
243
- )
244
- params = resource._parse_uri_params()
245
- assert params == {
246
- "x": "1",
247
- "y": "hello",
248
- "z": "true",
249
- }
250
-
251
- def test_parse_uri_no_params(self):
252
- """Test parsing URI with no parameters."""
253
-
254
- def my_func() -> str:
255
- return "test"
256
-
257
- resource = FunctionResource(
258
- uri="fn://test",
259
- name="test",
260
- func=my_func,
261
- )
262
- params = resource._parse_uri_params()
263
- assert params == {}
264
-
265
 
266
  class TestResourceManagerAdd:
267
  """Test ResourceManager add functionality."""
@@ -278,7 +198,7 @@ class TestResourceManagerAdd:
278
  )
279
  added = manager.add_resource(resource)
280
  assert isinstance(added, FileResource)
281
- assert added.uri == f"file://{temp_file}"
282
  assert added.name == "test"
283
  assert added.description == "test file"
284
  assert added.mime_type == "text/plain"
 
54
  name="test",
55
  func=dummy_func,
56
  )
57
+ assert str(resource.uri) == "http://example.com/data"
58
 
59
  # Missing protocol
60
+ with pytest.raises(ValueError, match="Input should be a valid URL"):
61
  FunctionResource(
62
  uri="invalid",
63
  name="test",
 
65
  )
66
 
67
  # Missing host
68
+ with pytest.raises(ValueError, match="Input should be a valid URL"):
69
  FunctionResource(
70
  uri="http://",
71
  name="test",
 
85
  mime_type="text/plain",
86
  path=temp_file,
87
  )
88
+ assert str(resource.uri) == f"file://{temp_file}"
89
  assert resource.name == "test"
90
  assert resource.description == "test file"
91
  assert resource.mime_type == "text/plain"
 
162
  mime_type="text/plain",
163
  func=my_func,
164
  )
165
+ assert str(resource.uri) == "fn://test"
166
  assert resource.name == "test"
167
  assert resource.description == "test function"
168
  assert resource.mime_type == "text/plain"
169
  assert resource.func == my_func
170
 
171
+ async def test_function_resource_read(self):
172
  """Test reading a FunctionResource with no parameters."""
173
 
174
  def my_func() -> str:
 
182
  content = await resource.read()
183
  assert content == "test content"
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  class TestResourceManagerAdd:
187
  """Test ResourceManager add functionality."""
 
198
  )
199
  added = manager.add_resource(resource)
200
  assert isinstance(added, FileResource)
201
+ assert str(added.uri) == f"file://{temp_file}"
202
  assert added.name == "test"
203
  assert added.description == "test file"
204
  assert added.mime_type == "text/plain"
tests/test_server.py CHANGED
@@ -31,7 +31,7 @@ class TestServer:
31
  async def test_add_resource_decorator(self):
32
  mcp = FastMCP()
33
 
34
- @mcp.resource("data")
35
  def get_data(x: str) -> str:
36
  return f"Data: {x}"
37
 
 
31
  async def test_add_resource_decorator(self):
32
  mcp = FastMCP()
33
 
34
+ @mcp.resource("r://data")
35
  def get_data(x: str) -> str:
36
  return f"Data: {x}"
37