Jeremiah Lowin commited on
Commit
0feb19f
·
1 Parent(s): bc6a6dd

Expand support for various method interactions

Browse files
src/fastmcp/exceptions.py CHANGED
@@ -17,5 +17,9 @@ class ToolError(FastMCPError):
17
  """Error in tool operations."""
18
 
19
 
 
 
 
 
20
  class InvalidSignature(Exception):
21
  """Invalid signature for use with FastMCP."""
 
17
  """Error in tool operations."""
18
 
19
 
20
+ class PromptError(FastMCPError):
21
+ """Error in prompt operations."""
22
+
23
+
24
  class InvalidSignature(Exception):
25
  """Invalid signature for use with FastMCP."""
src/fastmcp/prompts/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .prompt import Prompt
2
  from .prompt_manager import PromptManager
3
 
4
- __all__ = ["Prompt", "PromptManager"]
 
1
+ from .prompt import Prompt, Message, UserMessage, AssistantMessage
2
  from .prompt_manager import PromptManager
3
 
4
+ __all__ = ["Prompt", "PromptManager", "Message", "UserMessage", "AssistantMessage"]
src/fastmcp/prompts/prompt.py CHANGED
@@ -27,27 +27,17 @@ class Message(BaseModel):
27
  super().__init__(content=content, **kwargs)
28
 
29
 
30
- class UserMessage(Message):
31
  """A message from the user."""
 
32
 
33
- role: Literal["user", "assistant"] = "user"
34
 
35
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
36
- super().__init__(content=content, **kwargs)
37
-
38
-
39
- class AssistantMessage(Message):
40
  """A message from the assistant."""
 
41
 
42
- role: Literal["user", "assistant"] = "assistant"
43
-
44
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
45
- super().__init__(content=content, **kwargs)
46
 
47
-
48
- message_validator = TypeAdapter[UserMessage | AssistantMessage](
49
- UserMessage | AssistantMessage
50
- )
51
 
52
  SyncPromptResult = (
53
  str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
@@ -160,7 +150,7 @@ class Prompt(BaseModel):
160
  messages.append(message_validator.validate_python(msg))
161
  elif isinstance(msg, str):
162
  content = TextContent(type="text", text=msg)
163
- messages.append(UserMessage(content=content))
164
  else:
165
  content = json.dumps(pydantic_core.to_jsonable_python(msg))
166
  messages.append(Message(role="user", content=content))
 
27
  super().__init__(content=content, **kwargs)
28
 
29
 
30
+ def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
31
  """A message from the user."""
32
+ return Message(content=content, role="user", **kwargs)
33
 
 
34
 
35
+ def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
 
 
 
 
36
  """A message from the assistant."""
37
+ return Message(content=content, role="assistant", **kwargs)
38
 
 
 
 
 
39
 
40
+ message_validator = TypeAdapter[Message](Message)
 
 
 
41
 
42
  SyncPromptResult = (
43
  str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
 
150
  messages.append(message_validator.validate_python(msg))
151
  elif isinstance(msg, str):
152
  content = TextContent(type="text", text=msg)
153
+ messages.append(Message(role="user", content=content))
154
  else:
155
  content = json.dumps(pydantic_core.to_jsonable_python(msg))
156
  messages.append(Message(role="user", content=content))
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -3,6 +3,7 @@
3
  from collections.abc import Awaitable, Callable
4
  from typing import Any
5
 
 
6
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
7
  from fastmcp.settings import DuplicateBehavior
8
  from fastmcp.utilities.logging import get_logger
@@ -61,7 +62,7 @@ class PromptManager:
61
  """Render a prompt by name with arguments."""
62
  prompt = self.get_prompt(name)
63
  if not prompt:
64
- raise ValueError(f"Unknown prompt: {name}")
65
 
66
  return await prompt.render(arguments)
67
 
 
3
  from collections.abc import Awaitable, Callable
4
  from typing import Any
5
 
6
+ from fastmcp.exceptions import PromptError
7
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
8
  from fastmcp.settings import DuplicateBehavior
9
  from fastmcp.utilities.logging import get_logger
 
62
  """Render a prompt by name with arguments."""
63
  prompt = self.get_prompt(name)
64
  if not prompt:
65
+ raise PromptError(f"Unknown prompt: {name}")
66
 
67
  return await prompt.render(arguments)
68
 
src/fastmcp/resources/__init__.py CHANGED
@@ -1,5 +1,4 @@
1
  from .resource import Resource
2
- from .resource_manager import ResourceManager
3
  from .template import ResourceTemplate
4
  from .types import (
5
  BinaryResource,
@@ -9,6 +8,7 @@ from .types import (
9
  HttpResource,
10
  TextResource,
11
  )
 
12
 
13
  __all__ = [
14
  "Resource",
 
1
  from .resource import Resource
 
2
  from .template import ResourceTemplate
3
  from .types import (
4
  BinaryResource,
 
8
  HttpResource,
9
  TextResource,
10
  )
11
+ from .resource_manager import ResourceManager
12
 
13
  __all__ = [
14
  "Resource",
src/fastmcp/resources/resource_manager.py CHANGED
@@ -1,11 +1,14 @@
1
  """Resource manager functionality."""
2
 
 
 
3
  from collections.abc import Callable
4
  from typing import Any
5
 
6
  from pydantic import AnyUrl
7
 
8
- from fastmcp.resources.resource import Resource
 
9
  from fastmcp.resources.template import ResourceTemplate
10
  from fastmcp.settings import DuplicateBehavior
11
  from fastmcp.utilities.logging import get_logger
@@ -21,16 +24,86 @@ class ResourceManager:
21
  self._templates: dict[str, ResourceTemplate] = {}
22
  self.duplicate_behavior = duplicate_behavior
23
 
24
- def add_resource(self, resource: Resource) -> Resource:
25
- """Add a resource to the manager.
 
 
 
 
 
 
 
 
26
 
27
  Args:
28
- resource: A Resource instance to add
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  Returns:
31
  The added resource. If a resource with the same URI already exists,
32
  returns the existing resource.
33
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  logger.debug(
35
  "Adding resource",
36
  extra={
@@ -63,6 +136,17 @@ class ResourceManager:
63
  tags: set[str] | None = None,
64
  ) -> ResourceTemplate:
65
  """Create a template from a function."""
 
 
 
 
 
 
 
 
 
 
 
66
  template = ResourceTemplate.from_function(
67
  fn,
68
  uri_template=uri_template,
@@ -122,7 +206,7 @@ class ResourceManager:
122
  except Exception as e:
123
  raise ValueError(f"Error creating resource from template: {e}")
124
 
125
- raise ValueError(f"Unknown resource: {uri}")
126
 
127
  def list_resources(self) -> list[Resource]:
128
  """List all registered resources."""
 
1
  """Resource manager functionality."""
2
 
3
+ import inspect
4
+ import re
5
  from collections.abc import Callable
6
  from typing import Any
7
 
8
  from pydantic import AnyUrl
9
 
10
+ from fastmcp.exceptions import ResourceError
11
+ from fastmcp.resources import FunctionResource, Resource
12
  from fastmcp.resources.template import ResourceTemplate
13
  from fastmcp.settings import DuplicateBehavior
14
  from fastmcp.utilities.logging import get_logger
 
24
  self._templates: dict[str, ResourceTemplate] = {}
25
  self.duplicate_behavior = duplicate_behavior
26
 
27
+ def add_resource_or_template_from_fn(
28
+ self,
29
+ fn: Callable[..., Any],
30
+ uri: str,
31
+ name: str | None = None,
32
+ description: str | None = None,
33
+ mime_type: str | None = None,
34
+ tags: set[str] | None = None,
35
+ ) -> Resource | ResourceTemplate:
36
+ """Add a resource or template to the manager from a function.
37
 
38
  Args:
39
+ fn: The function to register as a resource or template
40
+ uri: The URI for the resource or template
41
+ name: Optional name for the resource or template
42
+ description: Optional description of the resource or template
43
+ mime_type: Optional MIME type for the resource or template
44
+ tags: Optional set of tags for categorizing the resource or template
45
+
46
+ Returns:
47
+ The added resource or template. If a resource or template with the same URI already exists,
48
+ returns the existing resource or template.
49
+ """
50
+ # Check if this should be a template
51
+ has_uri_params = "{" in uri and "}" in uri
52
+ has_func_params = bool(inspect.signature(fn).parameters)
53
+
54
+ if has_uri_params and has_func_params:
55
+ return self.add_template_from_fn(
56
+ fn, uri, name, description, mime_type, tags
57
+ )
58
+ elif not has_uri_params and not has_func_params:
59
+ return self.add_resource_from_fn(
60
+ fn, uri, name, description, mime_type, tags
61
+ )
62
+ else:
63
+ raise ValueError(
64
+ "Invalid resource or template definition due to a "
65
+ "mismatch between URI parameters and function parameters."
66
+ )
67
+
68
+ def add_resource_from_fn(
69
+ self,
70
+ fn: Callable[..., Any],
71
+ uri: str,
72
+ name: str | None = None,
73
+ description: str | None = None,
74
+ mime_type: str | None = None,
75
+ tags: set[str] | None = None,
76
+ ) -> Resource:
77
+ """Add a resource to the manager from a function.
78
+
79
+ Args:
80
+ fn: The function to register as a resource
81
+ uri: The URI for the resource
82
+ name: Optional name for the resource
83
+ description: Optional description of the resource
84
+ mime_type: Optional MIME type for the resource
85
+ tags: Optional set of tags for categorizing the resource
86
 
87
  Returns:
88
  The added resource. If a resource with the same URI already exists,
89
  returns the existing resource.
90
  """
91
+ resource = FunctionResource(
92
+ uri=AnyUrl(uri),
93
+ name=name,
94
+ description=description,
95
+ mime_type=mime_type or "text/plain",
96
+ fn=fn,
97
+ tags=tags or set(),
98
+ )
99
+ return self.add_resource(resource)
100
+
101
+ def add_resource(self, resource: Resource) -> Resource:
102
+ """Add a resource to the manager.
103
+
104
+ Args:
105
+ resource: A Resource instance to add
106
+ """
107
  logger.debug(
108
  "Adding resource",
109
  extra={
 
136
  tags: set[str] | None = None,
137
  ) -> ResourceTemplate:
138
  """Create a template from a function."""
139
+
140
+ # Validate that URI params match function params
141
+ uri_params = set(re.findall(r"{(\w+)}", uri_template))
142
+ func_params = set(inspect.signature(fn).parameters.keys())
143
+
144
+ if uri_params != func_params:
145
+ raise ValueError(
146
+ f"Mismatch between URI parameters {uri_params} "
147
+ f"and function parameters {func_params}"
148
+ )
149
+
150
  template = ResourceTemplate.from_function(
151
  fn,
152
  uri_template=uri_template,
 
206
  except Exception as e:
207
  raise ValueError(f"Error creating resource from template: {e}")
208
 
209
+ raise ResourceError(f"Unknown resource: {uri}")
210
 
211
  def list_resources(self) -> list[Resource]:
212
  """List all registered resources."""
src/fastmcp/server/context.py CHANGED
@@ -118,7 +118,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
118
  assert self._fastmcp is not None, (
119
  "Context is not available outside of a request"
120
  )
121
- return await self._fastmcp.read_resource(uri)
122
 
123
  async def log(
124
  self,
 
118
  assert self._fastmcp is not None, (
119
  "Context is not available outside of a request"
120
  )
121
+ return await self._fastmcp._mcp_read_resource(uri)
122
 
123
  async def log(
124
  self,
src/fastmcp/server/proxy.py CHANGED
@@ -1,11 +1,11 @@
1
  from typing import Any, cast
2
 
3
  import mcp.types
4
- from mcp.types import BlobResourceContents, PromptMessage, TextResourceContents
5
 
6
  import fastmcp
7
  from fastmcp.client import Client
8
- from fastmcp.prompts import Prompt
9
  from fastmcp.resources import Resource, ResourceTemplate
10
  from fastmcp.server.context import Context
11
  from fastmcp.server.server import FastMCP
@@ -142,10 +142,10 @@ class ProxyPrompt(Prompt):
142
  fn=_proxy_passthrough,
143
  )
144
 
145
- async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
146
  async with self._client:
147
  result = await self._client.get_prompt(self.name, arguments)
148
- return result.messages
149
 
150
 
151
  class FastMCPProxy(FastMCP):
 
1
  from typing import Any, cast
2
 
3
  import mcp.types
4
+ from mcp.types import BlobResourceContents, TextResourceContents
5
 
6
  import fastmcp
7
  from fastmcp.client import Client
8
+ from fastmcp.prompts import Message, Prompt
9
  from fastmcp.resources import Resource, ResourceTemplate
10
  from fastmcp.server.context import Context
11
  from fastmcp.server.server import FastMCP
 
142
  fn=_proxy_passthrough,
143
  )
144
 
145
+ async def render(self, arguments: dict[str, Any]) -> list[Message]:
146
  async with self._client:
147
  result = await self._client.get_prompt(self.name, arguments)
148
+ return [Message(role=m.role, content=m.content) for m in result.messages]
149
 
150
 
151
  class FastMCPProxy(FastMCP):
src/fastmcp/server/server.py CHANGED
@@ -1,9 +1,7 @@
1
  """FastMCP - A more ergonomic interface for MCP servers."""
2
 
3
- import inspect
4
  import json
5
- import re
6
- from collections.abc import AsyncIterator, Callable
7
  from contextlib import (
8
  AbstractAsyncContextManager,
9
  AsyncExitStack,
@@ -43,8 +41,12 @@ import fastmcp
43
  import fastmcp.settings
44
  from fastmcp.exceptions import ResourceError
45
  from fastmcp.prompts import Prompt, PromptManager
46
- from fastmcp.resources import FunctionResource, Resource, ResourceManager
 
 
47
  from fastmcp.tools import ToolManager
 
 
48
  from fastmcp.utilities.logging import configure_logging, get_logger
49
  from fastmcp.utilities.types import Image
50
 
@@ -171,18 +173,27 @@ class FastMCP(Generic[LifespanResultT]):
171
 
172
  def _setup_handlers(self) -> None:
173
  """Set up core MCP protocol handlers."""
174
- self._mcp_server.list_tools()(self.list_tools)
175
  self._mcp_server.call_tool()(self.call_tool)
176
- self._mcp_server.list_resources()(self.list_resources)
177
- self._mcp_server.read_resource()(self.read_resource)
178
- self._mcp_server.list_prompts()(self.list_prompts)
179
- self._mcp_server.get_prompt()(self.get_prompt)
180
- self._mcp_server.list_resource_templates()(self.list_resource_templates)
181
 
182
- async def list_tools(self) -> list[MCPTool]:
183
- """List all available tools."""
 
 
 
 
 
 
 
 
 
 
184
 
185
- tools = self._tool_manager.list_tools()
186
  return [
187
  MCPTool(
188
  name=info.name,
@@ -215,10 +226,18 @@ class FastMCP(Generic[LifespanResultT]):
215
  converted_result = _convert_to_content(result)
216
  return converted_result
217
 
218
- async def list_resources(self) -> list[MCPResource]:
219
- """List all available resources."""
 
 
 
 
 
 
 
 
220
 
221
- resources = self._resource_manager.list_resources()
222
  return [
223
  MCPResource(
224
  uri=resource.uri,
@@ -229,8 +248,18 @@ class FastMCP(Generic[LifespanResultT]):
229
  for resource in resources
230
  ]
231
 
232
- async def list_resource_templates(self) -> list[MCPResourceTemplate]:
233
- templates = self._resource_manager.list_templates()
 
 
 
 
 
 
 
 
 
 
234
  return [
235
  MCPResourceTemplate(
236
  uriTemplate=template.uri_template,
@@ -240,15 +269,27 @@ class FastMCP(Generic[LifespanResultT]):
240
  for template in templates
241
  ]
242
 
243
- async def read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
244
  """Read a resource by URI."""
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  resource = await self._resource_manager.get_resource(uri)
247
  if not resource:
248
  raise ResourceError(f"Unknown resource: {uri}")
249
 
250
  try:
251
- content = await resource.read()
252
  return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
253
  except Exception as e:
254
  logger.error(f"Error reading resource {uri}: {e}")
@@ -308,6 +349,7 @@ class FastMCP(Generic[LifespanResultT]):
308
  await context.report_progress(50, 100)
309
  return str(x)
310
  """
 
311
  # Check if user passed function directly instead of calling decorator
312
  if callable(name):
313
  raise TypeError(
@@ -317,7 +359,7 @@ class FastMCP(Generic[LifespanResultT]):
317
 
318
  def decorator(fn: AnyFunction) -> AnyFunction:
319
  self.add_tool(fn, name=name, description=description, tags=tags)
320
- return fn
321
 
322
  return decorator
323
 
@@ -327,8 +369,40 @@ class FastMCP(Generic[LifespanResultT]):
327
  Args:
328
  resource: A Resource instance to add
329
  """
 
330
  self._resource_manager.add_resource(resource)
331
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  def resource(
333
  self,
334
  uri: str,
@@ -383,52 +457,36 @@ class FastMCP(Generic[LifespanResultT]):
383
  )
384
 
385
  def decorator(fn: AnyFunction) -> AnyFunction:
386
- # Check if this should be a template
387
- has_uri_params = "{" in uri and "}" in uri
388
- has_func_params = bool(inspect.signature(fn).parameters)
389
-
390
- if has_uri_params or has_func_params:
391
- # Validate that URI params match function params
392
- uri_params = set(re.findall(r"{(\w+)}", uri))
393
- func_params = set(inspect.signature(fn).parameters.keys())
394
-
395
- if uri_params != func_params:
396
- raise ValueError(
397
- f"Mismatch between URI parameters {uri_params} "
398
- f"and function parameters {func_params}"
399
- )
400
-
401
- # Register as template
402
- self._resource_manager.add_template_from_fn(
403
- fn=fn,
404
- uri_template=uri,
405
- name=name,
406
- description=description,
407
- mime_type=mime_type or "text/plain",
408
- tags=tags,
409
- )
410
- else:
411
- # Register as regular resource
412
- resource = FunctionResource(
413
- uri=AnyUrl(uri),
414
- name=name,
415
- description=description,
416
- mime_type=mime_type or "text/plain",
417
- fn=fn,
418
- tags=tags or set(), # Default to empty set if None
419
- )
420
- self.add_resource(resource)
421
- return fn
422
 
423
  return decorator
424
 
425
- def add_prompt(self, prompt: Prompt) -> None:
 
 
 
 
 
 
426
  """Add a prompt to the server.
427
 
428
  Args:
429
  prompt: A Prompt instance to add
430
  """
431
- self._prompt_manager.add_prompt(prompt)
 
 
 
 
 
432
 
433
  def prompt(
434
  self,
@@ -478,11 +536,8 @@ class FastMCP(Generic[LifespanResultT]):
478
  )
479
 
480
  def decorator(func: AnyFunction) -> AnyFunction:
481
- prompt = Prompt.from_function(
482
- func, name=name, description=description, tags=tags
483
- )
484
- self.add_prompt(prompt)
485
- return func
486
 
487
  return decorator
488
 
@@ -537,9 +592,20 @@ class FastMCP(Generic[LifespanResultT]):
537
  ],
538
  )
539
 
540
- async def list_prompts(self) -> list[MCPPrompt]:
541
- """List all available prompts."""
542
- prompts = self._prompt_manager.list_prompts()
 
 
 
 
 
 
 
 
 
 
 
543
  return [
544
  MCPPrompt(
545
  name=prompt.name,
@@ -558,10 +624,21 @@ class FastMCP(Generic[LifespanResultT]):
558
 
559
  async def get_prompt(
560
  self, name: str, arguments: dict[str, Any] | None = None
561
- ) -> GetPromptResult:
562
  """Get a prompt by name with arguments."""
 
 
 
 
 
 
 
 
 
 
 
563
  try:
564
- messages = await self._prompt_manager.render_prompt(name, arguments)
565
 
566
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
567
  except Exception as e:
 
1
  """FastMCP - A more ergonomic interface for MCP servers."""
2
 
 
3
  import json
4
+ from collections.abc import AsyncIterator, Awaitable, Callable
 
5
  from contextlib import (
6
  AbstractAsyncContextManager,
7
  AsyncExitStack,
 
41
  import fastmcp.settings
42
  from fastmcp.exceptions import ResourceError
43
  from fastmcp.prompts import Prompt, PromptManager
44
+ from fastmcp.prompts.prompt import Message, PromptResult
45
+ from fastmcp.resources import Resource, ResourceManager
46
+ from fastmcp.resources.template import ResourceTemplate
47
  from fastmcp.tools import ToolManager
48
+ from fastmcp.tools.tool import Tool
49
+ from fastmcp.utilities.decorators import DecoratedFunction
50
  from fastmcp.utilities.logging import configure_logging, get_logger
51
  from fastmcp.utilities.types import Image
52
 
 
173
 
174
  def _setup_handlers(self) -> None:
175
  """Set up core MCP protocol handlers."""
176
+ self._mcp_server.list_tools()(self._mcp_list_tools)
177
  self._mcp_server.call_tool()(self.call_tool)
178
+ self._mcp_server.list_resources()(self._mcp_list_resources)
179
+ self._mcp_server.read_resource()(self._mcp_read_resource)
180
+ self._mcp_server.list_prompts()(self._mcp_list_prompts)
181
+ self._mcp_server.get_prompt()(self._mcp_get_prompt)
182
+ self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
183
 
184
+ def list_tools(self) -> list[Tool]:
185
+ return self._tool_manager.list_tools()
186
+
187
+ async def _mcp_list_tools(self) -> list[MCPTool]:
188
+ """
189
+ List all available tools, in the format expected by the low-level MCP
190
+ server.
191
+
192
+ See `list_tools` for a more ergonomic way to list tools.
193
+ """
194
+
195
+ tools = self.list_tools()
196
 
 
197
  return [
198
  MCPTool(
199
  name=info.name,
 
226
  converted_result = _convert_to_content(result)
227
  return converted_result
228
 
229
+ def list_resources(self) -> list[Resource]:
230
+ return self._resource_manager.list_resources()
231
+
232
+ async def _mcp_list_resources(self) -> list[MCPResource]:
233
+ """
234
+ List all available resources, in the format expected by the low-level MCP
235
+ server.
236
+
237
+ See `list_resources` for a more ergonomic way to list resources.
238
+ """
239
 
240
+ resources = self.list_resources()
241
  return [
242
  MCPResource(
243
  uri=resource.uri,
 
248
  for resource in resources
249
  ]
250
 
251
+ def list_resource_templates(self) -> list[ResourceTemplate]:
252
+ return self._resource_manager.list_templates()
253
+
254
+ async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
255
+ """
256
+ List all available resource templates, in the format expected by the low-level
257
+ MCP server.
258
+
259
+ See `list_resource_templates` for a more ergonomic way to list resource
260
+ templates.
261
+ """
262
+ templates = self.list_resource_templates()
263
  return [
264
  MCPResourceTemplate(
265
  uriTemplate=template.uri_template,
 
269
  for template in templates
270
  ]
271
 
272
+ async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
273
  """Read a resource by URI."""
274
+ resource = await self._resource_manager.get_resource(uri)
275
+ if not resource:
276
+ raise ResourceError(f"Unknown resource: {uri}")
277
+ return await resource.read()
278
+
279
+ async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
280
+ """
281
+ Read a resource by URI, in the format expected by the low-level MCP
282
+ server.
283
+
284
+ See `read_resource` for a more ergonomic way to read resources.
285
+ """
286
 
287
  resource = await self._resource_manager.get_resource(uri)
288
  if not resource:
289
  raise ResourceError(f"Unknown resource: {uri}")
290
 
291
  try:
292
+ content = await self.read_resource(uri)
293
  return [ReadResourceContents(content=content, mime_type=resource.mime_type)]
294
  except Exception as e:
295
  logger.error(f"Error reading resource {uri}: {e}")
 
349
  await context.report_progress(50, 100)
350
  return str(x)
351
  """
352
+
353
  # Check if user passed function directly instead of calling decorator
354
  if callable(name):
355
  raise TypeError(
 
359
 
360
  def decorator(fn: AnyFunction) -> AnyFunction:
361
  self.add_tool(fn, name=name, description=description, tags=tags)
362
+ return DecoratedFunction(fn)
363
 
364
  return decorator
365
 
 
369
  Args:
370
  resource: A Resource instance to add
371
  """
372
+
373
  self._resource_manager.add_resource(resource)
374
 
375
+ def add_resource_from_fn(
376
+ self,
377
+ fn: AnyFunction,
378
+ uri: str,
379
+ name: str | None = None,
380
+ description: str | None = None,
381
+ mime_type: str | None = None,
382
+ tags: set[str] | None = None,
383
+ ) -> None:
384
+ """Add a resource or template to the server from a function.
385
+
386
+ If the URI contains parameters (e.g. "resource://{param}") or the function
387
+ has parameters, it will be registered as a template resource.
388
+
389
+ Args:
390
+ fn: The function to register as a resource
391
+ uri: The URI for the resource
392
+ name: Optional name for the resource
393
+ description: Optional description of the resource
394
+ mime_type: Optional MIME type for the resource
395
+ tags: Optional set of tags for categorizing the resource
396
+ """
397
+ self._resource_manager.add_resource_or_template_from_fn(
398
+ fn=fn,
399
+ uri=uri,
400
+ name=name,
401
+ description=description,
402
+ mime_type=mime_type,
403
+ tags=tags,
404
+ )
405
+
406
  def resource(
407
  self,
408
  uri: str,
 
457
  )
458
 
459
  def decorator(fn: AnyFunction) -> AnyFunction:
460
+ self._resource_manager.add_resource_or_template_from_fn(
461
+ fn=fn,
462
+ uri=uri,
463
+ name=name,
464
+ description=description,
465
+ mime_type=mime_type,
466
+ tags=tags,
467
+ )
468
+ return DecoratedFunction(fn)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
  return decorator
471
 
472
+ def add_prompt(
473
+ self,
474
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]],
475
+ name: str | None = None,
476
+ description: str | None = None,
477
+ tags: set[str] | None = None,
478
+ ) -> None:
479
  """Add a prompt to the server.
480
 
481
  Args:
482
  prompt: A Prompt instance to add
483
  """
484
+ self._prompt_manager.add_prompt_from_fn(
485
+ fn=fn,
486
+ name=name,
487
+ description=description,
488
+ tags=tags,
489
+ )
490
 
491
  def prompt(
492
  self,
 
536
  )
537
 
538
  def decorator(func: AnyFunction) -> AnyFunction:
539
+ self.add_prompt(func, name=name, description=description, tags=tags)
540
+ return DecoratedFunction(func)
 
 
 
541
 
542
  return decorator
543
 
 
592
  ],
593
  )
594
 
595
+ def list_prompts(self) -> list[Prompt]:
596
+ """
597
+ List all available prompts.
598
+ """
599
+ return self._prompt_manager.list_prompts()
600
+
601
+ async def _mcp_list_prompts(self) -> list[MCPPrompt]:
602
+ """
603
+ List all available prompts, in the format expected by the low-level MCP
604
+ server.
605
+
606
+ See `list_prompts` for a more ergonomic way to list prompts.
607
+ """
608
+ prompts = self.list_prompts()
609
  return [
610
  MCPPrompt(
611
  name=prompt.name,
 
624
 
625
  async def get_prompt(
626
  self, name: str, arguments: dict[str, Any] | None = None
627
+ ) -> list[Message]:
628
  """Get a prompt by name with arguments."""
629
+ return await self._prompt_manager.render_prompt(name, arguments)
630
+
631
+ async def _mcp_get_prompt(
632
+ self, name: str, arguments: dict[str, Any] | None = None
633
+ ) -> GetPromptResult:
634
+ """
635
+ Get a prompt by name with arguments, in the format expected by the low-level
636
+ MCP server.
637
+
638
+ See `get_prompt` for a more ergonomic way to get prompts.
639
+ """
640
  try:
641
+ messages = await self.get_prompt(name, arguments)
642
 
643
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
644
  except Exception as e:
src/fastmcp/tools/tool.py CHANGED
@@ -58,7 +58,10 @@ class Tool(BaseModel):
58
  is_async = inspect.iscoroutinefunction(fn)
59
 
60
  if context_kwarg is None:
61
- sig = inspect.signature(fn)
 
 
 
62
  for param_name, param in sig.parameters.items():
63
  if param.annotation is Context:
64
  context_kwarg = param_name
 
58
  is_async = inspect.iscoroutinefunction(fn)
59
 
60
  if context_kwarg is None:
61
+ if isinstance(fn, classmethod):
62
+ sig = inspect.signature(fn.__func__)
63
+ else:
64
+ sig = inspect.signature(fn)
65
  for param_name, param in sig.parameters.items():
66
  if param.annotation is Context:
67
  context_kwarg = param_name
src/fastmcp/utilities/decorators.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from typing import Generic, ParamSpec, TypeVar, cast, overload
4
+
5
+ from typing_extensions import Self
6
+
7
+ R = TypeVar("R")
8
+ P = ParamSpec("P")
9
+
10
+
11
+ class DecoratedFunction(Generic[P, R]):
12
+ """Descriptor for decorated functions.
13
+
14
+ You can return this object from a decorator to ensure that it works across
15
+ all types of functions: vanilla, instance methods, class methods, and static
16
+ methods; both synchronous and asynchronous.
17
+
18
+ This class is used to store the original function and metadata about how to
19
+ register it as a tool.
20
+
21
+ Example usage:
22
+
23
+ ```python
24
+ def my_decorator(fn: Callable[P, R]) -> DecoratedFunction[P, R]:
25
+ return DecoratedFunction(fn)
26
+ ```
27
+
28
+ On a function:
29
+ ```python
30
+ @my_decorator
31
+ def my_function(a: int, b: int) -> int:
32
+ return a + b
33
+ ```
34
+
35
+ On an instance method:
36
+ ```python
37
+ class Test:
38
+ @my_decorator
39
+ def my_function(self, a: int, b: int) -> int:
40
+ return a + b
41
+ ```
42
+
43
+ On a class method:
44
+ ```python
45
+ class Test:
46
+ @classmethod
47
+ @my_decorator
48
+ def my_function(cls, a: int, b: int) -> int:
49
+ return a + b
50
+ ```
51
+
52
+ Note that for classmethods, the decorator must be applied first, then
53
+ `@classmethod` on top.
54
+
55
+ On a static method:
56
+ ```python
57
+ class Test:
58
+ @staticmethod
59
+ @my_decorator
60
+ def my_function(a: int, b: int) -> int:
61
+ return a + b
62
+ ```
63
+ """
64
+
65
+ def __init__(self, fn: Callable[P, R]):
66
+ self.fn = fn
67
+
68
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
69
+ """Call the original function."""
70
+ try:
71
+ return self.fn(*args, **kwargs)
72
+ except TypeError as e:
73
+ if "'classmethod' object is not callable" in str(e):
74
+ raise TypeError(
75
+ "To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top."
76
+ )
77
+ raise
78
+
79
+ @overload
80
+ def __get__(self, instance: None, owner: type | None = None) -> Self: ...
81
+
82
+ @overload
83
+ def __get__(
84
+ self, instance: object, owner: type | None = None
85
+ ) -> Callable[P, R]: ...
86
+
87
+ def __get__(
88
+ self, instance: object | None, owner: type | None = None
89
+ ) -> Self | Callable[P, R]:
90
+ """Return the original function when accessed from an instance, or self when accessed from the class."""
91
+ if instance is None:
92
+ return self
93
+ # Return the original function bound to the instance
94
+ return cast(Callable[P, R], self.fn.__get__(instance, owner))
95
+
96
+ def __repr__(self) -> str:
97
+ """Return a representation that matches Python's function representation."""
98
+ module = getattr(self.fn, "__module__", "unknown")
99
+ qualname = getattr(self.fn, "__qualname__", str(self.fn))
100
+ sig_str = str(inspect.signature(self.fn))
101
+ return f"<function {module}.{qualname}{sig_str}>"
src/fastmcp/utilities/func_metadata.py CHANGED
@@ -125,7 +125,10 @@ def func_metadata(
125
  Returns:
126
  A pydantic model representing the function's signature.
127
  """
128
- sig = _get_typed_signature(func)
 
 
 
129
  params = sig.parameters
130
  dynamic_pydantic_model_params: dict[str, Any] = {}
131
  globalns = getattr(func, "__globals__", {})
 
125
  Returns:
126
  A pydantic model representing the function's signature.
127
  """
128
+ if isinstance(func, classmethod):
129
+ sig = _get_typed_signature(func.__func__)
130
+ else:
131
+ sig = _get_typed_signature(func)
132
  params = sig.parameters
133
  dynamic_pydantic_model_params: dict[str, Any] = {}
134
  globalns = getattr(func, "__globals__", {})
tests/prompts/test_prompt_manager.py CHANGED
@@ -1,5 +1,6 @@
1
  import pytest
2
 
 
3
  from fastmcp.prompts import Prompt
4
  from fastmcp.prompts.prompt import PromptArgument, TextContent, UserMessage
5
  from fastmcp.prompts.prompt_manager import PromptManager
@@ -97,7 +98,7 @@ class TestPromptManager:
97
  async def test_render_unknown_prompt(self):
98
  """Test rendering a non-existent prompt."""
99
  manager = PromptManager()
100
- with pytest.raises(ValueError, match="Unknown prompt: unknown"):
101
  await manager.render_prompt("unknown")
102
 
103
  @pytest.mark.anyio
 
1
  import pytest
2
 
3
+ from fastmcp.exceptions import PromptError
4
  from fastmcp.prompts import Prompt
5
  from fastmcp.prompts.prompt import PromptArgument, TextContent, UserMessage
6
  from fastmcp.prompts.prompt_manager import PromptManager
 
98
  async def test_render_unknown_prompt(self):
99
  """Test rendering a non-existent prompt."""
100
  manager = PromptManager()
101
+ with pytest.raises(PromptError, match="Unknown prompt: unknown"):
102
  await manager.render_prompt("unknown")
103
 
104
  @pytest.mark.anyio
tests/resources/test_resource_manager.py CHANGED
@@ -4,6 +4,7 @@ from tempfile import NamedTemporaryFile
4
  import pytest
5
  from pydantic import AnyUrl, FileUrl
6
 
 
7
  from fastmcp.resources import (
8
  FileResource,
9
  FunctionResource,
@@ -156,7 +157,7 @@ class TestResourceManager:
156
  async def test_get_unknown_resource(self):
157
  """Test getting a non-existent resource."""
158
  manager = ResourceManager()
159
- with pytest.raises(ValueError, match="Unknown resource"):
160
  await manager.get_resource(AnyUrl("unknown://test"))
161
 
162
  def test_list_resources(self, temp_file: Path):
 
4
  import pytest
5
  from pydantic import AnyUrl, FileUrl
6
 
7
+ from fastmcp.exceptions import ResourceError
8
  from fastmcp.resources import (
9
  FileResource,
10
  FunctionResource,
 
157
  async def test_get_unknown_resource(self):
158
  """Test getting a non-existent resource."""
159
  manager = ResourceManager()
160
+ with pytest.raises(ResourceError, match="Unknown resource"):
161
  await manager.get_resource(AnyUrl("unknown://test"))
162
 
163
  def test_list_resources(self, temp_file: Path):
tests/server/test_file_server.py CHANGED
@@ -75,7 +75,7 @@ def tools(mcp: FastMCP, test_dir: Path) -> FastMCP:
75
 
76
  @pytest.mark.anyio
77
  async def test_list_resources(mcp: FastMCP):
78
- resources = await mcp.list_resources()
79
  assert len(resources) == 4
80
 
81
  assert [str(r.uri) for r in resources] == [
@@ -88,7 +88,7 @@ async def test_list_resources(mcp: FastMCP):
88
 
89
  @pytest.mark.anyio
90
  async def test_read_resource_dir(mcp: FastMCP):
91
- res_iter = await mcp.read_resource("dir://test_dir")
92
  res_list = list(res_iter)
93
  assert len(res_list) == 1
94
  res = res_list[0]
@@ -105,7 +105,7 @@ async def test_read_resource_dir(mcp: FastMCP):
105
 
106
  @pytest.mark.anyio
107
  async def test_read_resource_file(mcp: FastMCP):
108
- res_iter = await mcp.read_resource("file://test_dir/example.py")
109
  res_list = list(res_iter)
110
  assert len(res_list) == 1
111
  res = res_list[0]
@@ -125,7 +125,7 @@ async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path):
125
  await mcp.call_tool(
126
  "delete_file", arguments=dict(path=str(test_dir / "example.py"))
127
  )
128
- res_iter = await mcp.read_resource("file://test_dir/example.py")
129
  res_list = list(res_iter)
130
  assert len(res_list) == 1
131
  res = res_list[0]
 
75
 
76
  @pytest.mark.anyio
77
  async def test_list_resources(mcp: FastMCP):
78
+ resources = await mcp._mcp_list_resources()
79
  assert len(resources) == 4
80
 
81
  assert [str(r.uri) for r in resources] == [
 
88
 
89
  @pytest.mark.anyio
90
  async def test_read_resource_dir(mcp: FastMCP):
91
+ res_iter = await mcp._mcp_read_resource("dir://test_dir")
92
  res_list = list(res_iter)
93
  assert len(res_list) == 1
94
  res = res_list[0]
 
105
 
106
  @pytest.mark.anyio
107
  async def test_read_resource_file(mcp: FastMCP):
108
+ res_iter = await mcp._mcp_read_resource("file://test_dir/example.py")
109
  res_list = list(res_iter)
110
  assert len(res_list) == 1
111
  res = res_list[0]
 
125
  await mcp.call_tool(
126
  "delete_file", arguments=dict(path=str(test_dir / "example.py"))
127
  )
128
+ res_iter = await mcp._mcp_read_resource("file://test_dir/example.py")
129
  res_list = list(res_iter)
130
  assert len(res_list) == 1
131
  res = res_list[0]
tests/server/test_openapi.py CHANGED
@@ -117,7 +117,7 @@ class TestTools:
117
  """
118
  By default, tools exclude GET methods
119
  """
120
- tools = await fastmcp_server.list_tools()
121
  assert len(tools) == 2
122
 
123
  assert tools[0].model_dump() == dict(
@@ -164,7 +164,7 @@ class TestTools:
164
  assert len(response.json()) == 4
165
 
166
  # Check that the user was created via MCP
167
- user_response = await fastmcp_server.read_resource(
168
  "resource://openapi/get_user_users__user_id__get/4"
169
  )
170
  user = user_response[0].content
@@ -186,7 +186,7 @@ class TestTools:
186
  assert dict(id=1, name="XYZ", active=True) in response.json()
187
 
188
  # Check that the user was updated via MCP
189
- user_response = await fastmcp_server.read_resource(
190
  "resource://openapi/get_user_users__user_id__get/1"
191
  )
192
  user = user_response[0].content
@@ -198,7 +198,7 @@ class TestResources:
198
  """
199
  By default, resources exclude GET methods without parameters
200
  """
201
- resources = await fastmcp_server.list_resources()
202
  assert len(resources) == 1
203
  assert resources[0].uri == AnyUrl("resource://openapi/get_users_users_get")
204
  assert resources[0].name == "get_users_users_get"
@@ -212,7 +212,7 @@ class TestResources:
212
  json_users = TypeAdapter(list[User]).dump_python(
213
  sorted(users_db.values(), key=lambda x: x.id)
214
  )
215
- resource_response = await fastmcp_server.read_resource(
216
  "resource://openapi/get_users_users_get"
217
  )
218
  resource = resource_response[0].content
@@ -226,7 +226,7 @@ class TestResourceTemplates:
226
  """
227
  By default, resource templates exclude GET methods without parameters
228
  """
229
- resource_templates = await fastmcp_server.list_resource_templates()
230
  assert len(resource_templates) == 1
231
  assert resource_templates[0].name == "get_user_users__user_id__get"
232
  assert (
@@ -241,7 +241,7 @@ class TestResourceTemplates:
241
  The resource template created by the OpenAPI server should be the same as the original
242
  """
243
  user_id = 2
244
- resource_response = await fastmcp_server.read_resource(
245
  f"resource://openapi/get_user_users__user_id__get/{user_id}"
246
  )
247
 
@@ -256,7 +256,7 @@ class TestPrompts:
256
  """
257
  By default, there are no prompts.
258
  """
259
- prompts = await fastmcp_server.list_prompts()
260
  assert len(prompts) == 0
261
 
262
 
 
117
  """
118
  By default, tools exclude GET methods
119
  """
120
+ tools = await fastmcp_server._mcp_list_tools()
121
  assert len(tools) == 2
122
 
123
  assert tools[0].model_dump() == dict(
 
164
  assert len(response.json()) == 4
165
 
166
  # Check that the user was created via MCP
167
+ user_response = await fastmcp_server._mcp_read_resource(
168
  "resource://openapi/get_user_users__user_id__get/4"
169
  )
170
  user = user_response[0].content
 
186
  assert dict(id=1, name="XYZ", active=True) in response.json()
187
 
188
  # Check that the user was updated via MCP
189
+ user_response = await fastmcp_server._mcp_read_resource(
190
  "resource://openapi/get_user_users__user_id__get/1"
191
  )
192
  user = user_response[0].content
 
198
  """
199
  By default, resources exclude GET methods without parameters
200
  """
201
+ resources = await fastmcp_server._mcp_list_resources()
202
  assert len(resources) == 1
203
  assert resources[0].uri == AnyUrl("resource://openapi/get_users_users_get")
204
  assert resources[0].name == "get_users_users_get"
 
212
  json_users = TypeAdapter(list[User]).dump_python(
213
  sorted(users_db.values(), key=lambda x: x.id)
214
  )
215
+ resource_response = await fastmcp_server._mcp_read_resource(
216
  "resource://openapi/get_users_users_get"
217
  )
218
  resource = resource_response[0].content
 
226
  """
227
  By default, resource templates exclude GET methods without parameters
228
  """
229
+ resource_templates = await fastmcp_server._mcp_list_resource_templates()
230
  assert len(resource_templates) == 1
231
  assert resource_templates[0].name == "get_user_users__user_id__get"
232
  assert (
 
241
  The resource template created by the OpenAPI server should be the same as the original
242
  """
243
  user_id = 2
244
+ resource_response = await fastmcp_server._mcp_read_resource(
245
  f"resource://openapi/get_user_users__user_id__get/{user_id}"
246
  )
247
 
 
256
  """
257
  By default, there are no prompts.
258
  """
259
+ prompts = await fastmcp_server._mcp_list_prompts()
260
  assert len(prompts) == 0
261
 
262
 
tests/server/test_proxy.py CHANGED
@@ -7,6 +7,7 @@ from dirty_equals import Contains
7
  from fastmcp import FastMCP
8
  from fastmcp.client import Client
9
  from fastmcp.client.transports import FastMCPTransport
 
10
  from fastmcp.server.proxy import FastMCPProxy
11
 
12
  USERS = [
@@ -80,11 +81,14 @@ async def test_create_proxy(fastmcp_server):
80
 
81
  class TestTools:
82
  async def test_list_tools(self, proxy_server):
83
- tools = await proxy_server.list_tools()
84
  assert [t.name for t in tools] == Contains("greet", "add", "error_tool")
85
 
86
  async def test_list_tools_same_as_original(self, fastmcp_server, proxy_server):
87
- assert await proxy_server.list_tools() == await fastmcp_server.list_tools()
 
 
 
88
 
89
  async def test_call_tool_result_same_as_original(
90
  self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy
@@ -106,19 +110,20 @@ class TestTools:
106
 
107
  class TestResources:
108
  async def test_list_resources(self, proxy_server):
109
- resources = await proxy_server.list_resources()
110
  assert [r.name for r in resources] == Contains(
111
  "data://users", "resource://wave"
112
  )
113
 
114
  async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server):
115
  assert (
116
- await proxy_server.list_resources() == await fastmcp_server.list_resources()
 
117
  )
118
 
119
  async def test_read_resource(self, proxy_server: FastMCPProxy):
120
  result = await proxy_server.read_resource("resource://wave")
121
- assert result[0].content == "👋" # type: ignore
122
 
123
  async def test_read_resource_same_as_original(self, fastmcp_server, proxy_server):
124
  result = await fastmcp_server.read_resource("resource://wave")
@@ -127,31 +132,31 @@ class TestResources:
127
 
128
  async def test_read_json_resource(self, proxy_server: FastMCPProxy):
129
  result = await proxy_server.read_resource("data://users")
130
- assert json.loads(result[0].content) == USERS # type: ignore
131
 
132
  async def test_read_resource_returns_none_if_not_found(self, proxy_server):
133
  with pytest.raises(
134
- ValueError, match="Unknown resource: resource://nonexistent"
135
  ):
136
  await proxy_server.read_resource("resource://nonexistent")
137
 
138
 
139
  class TestResourceTemplates:
140
  async def test_list_resource_templates(self, proxy_server):
141
- templates = await proxy_server.list_resource_templates()
142
  assert [t.name for t in templates] == Contains("get_user")
143
 
144
  async def test_list_resource_templates_same_as_original(
145
  self, fastmcp_server, proxy_server
146
  ):
147
- result = await fastmcp_server.list_resource_templates()
148
- proxy_result = await proxy_server.list_resource_templates()
149
  assert proxy_result == result
150
 
151
  @pytest.mark.parametrize("id", [1, 2, 3])
152
  async def test_read_resource_template(self, proxy_server: FastMCPProxy, id: int):
153
  result = await proxy_server.read_resource(f"data://user/{id}")
154
- assert json.loads(result[0].content) == USERS[id - 1] # type: ignore
155
 
156
  async def test_read_resource_template_same_as_original(
157
  self, fastmcp_server, proxy_server
@@ -163,11 +168,14 @@ class TestResourceTemplates:
163
 
164
  class TestPrompts:
165
  async def test_list_prompts(self, proxy_server):
166
- prompts = await proxy_server.list_prompts()
167
  assert [p.name for p in prompts] == Contains("welcome")
168
 
169
  async def test_list_prompts_same_as_original(self, fastmcp_server, proxy_server):
170
- assert await proxy_server.list_prompts() == await fastmcp_server.list_prompts()
 
 
 
171
 
172
  async def test_render_prompt_same_as_original(
173
  self, fastmcp_server: FastMCP, proxy_server
@@ -178,4 +186,4 @@ class TestPrompts:
178
 
179
  async def test_render_prompt_calls_prompt(self, proxy_server):
180
  result = await proxy_server.get_prompt("welcome", {"name": "Alice"})
181
- assert result.messages[0].content.text == "Welcome to FastMCP, Alice!"
 
7
  from fastmcp import FastMCP
8
  from fastmcp.client import Client
9
  from fastmcp.client.transports import FastMCPTransport
10
+ from fastmcp.exceptions import ResourceError
11
  from fastmcp.server.proxy import FastMCPProxy
12
 
13
  USERS = [
 
81
 
82
  class TestTools:
83
  async def test_list_tools(self, proxy_server):
84
+ tools = proxy_server.list_tools()
85
  assert [t.name for t in tools] == Contains("greet", "add", "error_tool")
86
 
87
  async def test_list_tools_same_as_original(self, fastmcp_server, proxy_server):
88
+ assert (
89
+ await proxy_server._mcp_list_tools()
90
+ == await fastmcp_server._mcp_list_tools()
91
+ )
92
 
93
  async def test_call_tool_result_same_as_original(
94
  self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy
 
110
 
111
  class TestResources:
112
  async def test_list_resources(self, proxy_server):
113
+ resources = proxy_server.list_resources()
114
  assert [r.name for r in resources] == Contains(
115
  "data://users", "resource://wave"
116
  )
117
 
118
  async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server):
119
  assert (
120
+ await proxy_server._mcp_list_resources()
121
+ == await fastmcp_server._mcp_list_resources()
122
  )
123
 
124
  async def test_read_resource(self, proxy_server: FastMCPProxy):
125
  result = await proxy_server.read_resource("resource://wave")
126
+ assert result == "👋"
127
 
128
  async def test_read_resource_same_as_original(self, fastmcp_server, proxy_server):
129
  result = await fastmcp_server.read_resource("resource://wave")
 
132
 
133
  async def test_read_json_resource(self, proxy_server: FastMCPProxy):
134
  result = await proxy_server.read_resource("data://users")
135
+ assert json.loads(result) == USERS
136
 
137
  async def test_read_resource_returns_none_if_not_found(self, proxy_server):
138
  with pytest.raises(
139
+ ResourceError, match="Unknown resource: resource://nonexistent"
140
  ):
141
  await proxy_server.read_resource("resource://nonexistent")
142
 
143
 
144
  class TestResourceTemplates:
145
  async def test_list_resource_templates(self, proxy_server):
146
+ templates = proxy_server.list_resource_templates()
147
  assert [t.name for t in templates] == Contains("get_user")
148
 
149
  async def test_list_resource_templates_same_as_original(
150
  self, fastmcp_server, proxy_server
151
  ):
152
+ result = await fastmcp_server._mcp_list_resource_templates()
153
+ proxy_result = await proxy_server._mcp_list_resource_templates()
154
  assert proxy_result == result
155
 
156
  @pytest.mark.parametrize("id", [1, 2, 3])
157
  async def test_read_resource_template(self, proxy_server: FastMCPProxy, id: int):
158
  result = await proxy_server.read_resource(f"data://user/{id}")
159
+ assert json.loads(result) == USERS[id - 1]
160
 
161
  async def test_read_resource_template_same_as_original(
162
  self, fastmcp_server, proxy_server
 
168
 
169
  class TestPrompts:
170
  async def test_list_prompts(self, proxy_server):
171
+ prompts = proxy_server.list_prompts()
172
  assert [p.name for p in prompts] == Contains("welcome")
173
 
174
  async def test_list_prompts_same_as_original(self, fastmcp_server, proxy_server):
175
+ assert (
176
+ await proxy_server._mcp_list_prompts()
177
+ == await fastmcp_server._mcp_list_prompts()
178
+ )
179
 
180
  async def test_render_prompt_same_as_original(
181
  self, fastmcp_server: FastMCP, proxy_server
 
186
 
187
  async def test_render_prompt_calls_prompt(self, proxy_server):
188
  result = await proxy_server.get_prompt("welcome", {"name": "Alice"})
189
+ assert result[0].content.text == "Welcome to FastMCP, Alice!"
tests/server/test_server.py CHANGED
@@ -14,7 +14,7 @@ from mcp.types import (
14
  from pydantic import AnyUrl, Field
15
 
16
  from fastmcp import Client, Context, FastMCP
17
- from fastmcp.exceptions import ToolError
18
  from fastmcp.prompts.prompt import EmbeddedResource, Message, UserMessage
19
  from fastmcp.resources import FileResource, FunctionResource
20
  from fastmcp.utilities.types import Image
@@ -56,16 +56,26 @@ class TestCreateServer:
56
  assert isinstance(content, TextContent)
57
  assert "¡Hola, 世界! 👋" == content.text
58
 
59
- async def test_add_tool_decorator(self):
 
 
 
 
 
 
 
 
60
  mcp = FastMCP()
61
 
62
  @mcp.tool()
63
  def add(x: int, y: int) -> int:
64
  return x + y
65
 
66
- assert len(mcp._tool_manager.list_tools()) == 1
 
 
67
 
68
- async def test_add_tool_decorator_incorrect_usage(self):
69
  mcp = FastMCP()
70
 
71
  with pytest.raises(TypeError, match="The @tool decorator was used incorrectly"):
@@ -74,16 +84,145 @@ class TestCreateServer:
74
  def add(x: int, y: int) -> int:
75
  return x + y
76
 
77
- async def test_add_resource_decorator(self):
78
  mcp = FastMCP()
79
 
80
- @mcp.resource("r://{x}")
81
- def get_data(x: str) -> str:
82
- return f"Data: {x}"
83
 
84
- assert len(mcp._resource_manager._templates) == 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- async def test_add_resource_decorator_incorrect_usage(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  mcp = FastMCP()
88
 
89
  with pytest.raises(
@@ -91,8 +230,386 @@ class TestCreateServer:
91
  ):
92
 
93
  @mcp.resource # Missing parentheses #type: ignore
94
- def get_data(x: str) -> str:
95
- return f"Data: {x}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
 
98
  @pytest.fixture
@@ -136,10 +653,10 @@ def tool_server():
136
 
137
  class TestServerTools:
138
  async def test_add_tool_exists(self, tool_server: FastMCP):
139
- assert "add" in [t.name for t in await tool_server.list_tools()]
140
 
141
  async def test_list_tools(self, tool_server: FastMCP):
142
- assert len(await tool_server.list_tools()) == 6
143
 
144
  async def test_call_tool(self, tool_server: FastMCP):
145
  result = await tool_server.call_tool("add", {"x": 1, "y": 2})
@@ -236,7 +753,7 @@ class TestServerTools:
236
  """A greeting tool"""
237
  return f"Hello {title} {name}"
238
 
239
- tools = await mcp.list_tools()
240
  assert len(tools) == 1
241
  tool = tools[0]
242
 
@@ -328,7 +845,7 @@ class TestServerResourceTemplates:
328
  parameters don't match"""
329
  mcp = FastMCP()
330
 
331
- with pytest.raises(ValueError, match="Mismatch between URI parameters"):
332
 
333
  @mcp.resource("resource://data")
334
  def get_data_fn(param: str) -> str:
@@ -338,7 +855,7 @@ class TestServerResourceTemplates:
338
  """Test that a resource with URI parameters is automatically a template"""
339
  mcp = FastMCP()
340
 
341
- with pytest.raises(ValueError, match="Mismatch between URI parameters"):
342
 
343
  @mcp.resource("resource://{param}")
344
  def get_data() -> str:
@@ -422,7 +939,7 @@ class TestServerResourceTemplates:
422
 
423
  # Should be registered as a template
424
  assert len(mcp._resource_manager._templates) == 1
425
- assert len(await mcp.list_resources()) == 0
426
 
427
  # When accessed, should create a concrete resource
428
  resource = await mcp._resource_manager.get_resource("resource://test/data")
 
14
  from pydantic import AnyUrl, Field
15
 
16
  from fastmcp import Client, Context, FastMCP
17
+ from fastmcp.exceptions import ResourceError, ToolError
18
  from fastmcp.prompts.prompt import EmbeddedResource, Message, UserMessage
19
  from fastmcp.resources import FileResource, FunctionResource
20
  from fastmcp.utilities.types import Image
 
56
  assert isinstance(content, TextContent)
57
  assert "¡Hola, 世界! 👋" == content.text
58
 
59
+
60
+ class TestToolDecorator:
61
+ async def test_no_tools_before_decorator(self):
62
+ mcp = FastMCP()
63
+
64
+ with pytest.raises(ToolError, match="Unknown tool: add"):
65
+ await mcp.call_tool("add", {"x": 1, "y": 2})
66
+
67
+ async def test_tool_decorator(self):
68
  mcp = FastMCP()
69
 
70
  @mcp.tool()
71
  def add(x: int, y: int) -> int:
72
  return x + y
73
 
74
+ result = await mcp.call_tool("add", {"x": 1, "y": 2})
75
+ assert isinstance(result[0], TextContent)
76
+ assert result[0].text == "3"
77
 
78
+ async def test_tool_decorator_incorrect_usage(self):
79
  mcp = FastMCP()
80
 
81
  with pytest.raises(TypeError, match="The @tool decorator was used incorrectly"):
 
84
  def add(x: int, y: int) -> int:
85
  return x + y
86
 
87
+ async def test_tool_decorator_with_name(self):
88
  mcp = FastMCP()
89
 
90
+ @mcp.tool(name="custom-add")
91
+ def add(x: int, y: int) -> int:
92
+ return x + y
93
 
94
+ result = await mcp.call_tool("custom-add", {"x": 1, "y": 2})
95
+ assert isinstance(result[0], TextContent)
96
+ assert result[0].text == "3"
97
+
98
+ async def test_tool_decorator_with_description(self):
99
+ mcp = FastMCP()
100
+
101
+ @mcp.tool(description="Add two numbers")
102
+ def add(x: int, y: int) -> int:
103
+ return x + y
104
+
105
+ tools = await mcp._mcp_list_tools()
106
+ assert len(tools) == 1
107
+ tool = tools[0]
108
+ assert tool.description == "Add two numbers"
109
+
110
+ async def test_tool_decorator_instance_method(self):
111
+ mcp = FastMCP()
112
+
113
+ class MyClass:
114
+ def __init__(self, x: int):
115
+ self.x = x
116
+
117
+ @mcp.tool()
118
+ def add(self, y: int) -> int:
119
+ return self.x + y
120
+
121
+ obj = MyClass(10)
122
+ mcp.add_tool(obj.add)
123
+ result = await mcp.call_tool("add", {"y": 2})
124
+ assert isinstance(result[0], TextContent)
125
+ assert result[0].text == "12"
126
+
127
+ async def test_tool_decorator_classmethod(self):
128
+ mcp = FastMCP()
129
+
130
+ class MyClass:
131
+ x: int = 10
132
+
133
+ @classmethod
134
+ def add(cls, y: int) -> int:
135
+ return cls.x + y
136
+
137
+ mcp.add_tool(MyClass.add)
138
+ result = await mcp.call_tool("add", {"y": 2})
139
+ assert isinstance(result[0], TextContent)
140
+ assert result[0].text == "12"
141
+
142
+ async def test_tool_decorator_staticmethod(self):
143
+ mcp = FastMCP()
144
+
145
+ class MyClass:
146
+ @staticmethod
147
+ @mcp.tool()
148
+ def add(x: int, y: int) -> int:
149
+ return x + y
150
+
151
+ result = await mcp.call_tool("add", {"x": 1, "y": 2})
152
+ assert isinstance(result[0], TextContent)
153
+ assert result[0].text == "3"
154
+
155
+ async def test_tool_decorator_async_function(self):
156
+ mcp = FastMCP()
157
+
158
+ @mcp.tool()
159
+ async def add(x: int, y: int) -> int:
160
+ return x + y
161
+
162
+ result = await mcp.call_tool("add", {"x": 1, "y": 2})
163
+ assert isinstance(result[0], TextContent)
164
+ assert result[0].text == "3"
165
+
166
+ async def test_tool_decorator_classmethod_async_function(self):
167
+ mcp = FastMCP()
168
+
169
+ class MyClass:
170
+ x = 10
171
+
172
+ @classmethod
173
+ async def add(cls, y: int) -> int:
174
+ return cls.x + y
175
 
176
+ mcp.add_tool(MyClass.add)
177
+ result = await mcp.call_tool("add", {"y": 2})
178
+ assert isinstance(result[0], TextContent)
179
+ assert result[0].text == "12"
180
+
181
+ async def test_tool_decorator_staticmethod_async_function(self):
182
+ mcp = FastMCP()
183
+
184
+ class MyClass:
185
+ @staticmethod
186
+ async def add(x: int, y: int) -> int:
187
+ return x + y
188
+
189
+ mcp.add_tool(MyClass.add)
190
+ result = await mcp.call_tool("add", {"x": 1, "y": 2})
191
+ assert isinstance(result[0], TextContent)
192
+ assert result[0].text == "3"
193
+
194
+ async def test_tool_decorator_with_tags(self):
195
+ """Test that the tool decorator properly sets tags."""
196
+ mcp = FastMCP()
197
+
198
+ @mcp.tool(tags={"example", "test-tag"})
199
+ def sample_tool(x: int) -> int:
200
+ return x * 2
201
+
202
+ # Verify the tags were set correctly
203
+ tools = mcp._tool_manager.list_tools()
204
+ assert len(tools) == 1
205
+ assert tools[0].tags == {"example", "test-tag"}
206
+
207
+
208
+ class TestResourceDecorator:
209
+ async def test_no_resources_before_decorator(self):
210
+ mcp = FastMCP()
211
+
212
+ with pytest.raises(ResourceError, match="Unknown resource"):
213
+ await mcp.read_resource("resource://data")
214
+
215
+ async def test_resource_decorator(self):
216
+ mcp = FastMCP()
217
+
218
+ @mcp.resource("resource://data")
219
+ def get_data() -> str:
220
+ return "Hello, world!"
221
+
222
+ result = await mcp.read_resource("resource://data")
223
+ assert result == "Hello, world!"
224
+
225
+ async def test_resource_decorator_incorrect_usage(self):
226
  mcp = FastMCP()
227
 
228
  with pytest.raises(
 
230
  ):
231
 
232
  @mcp.resource # Missing parentheses #type: ignore
233
+ def get_data() -> str:
234
+ return "Hello, world!"
235
+
236
+ async def test_resource_decorator_with_name(self):
237
+ mcp = FastMCP()
238
+
239
+ @mcp.resource("resource://data", name="custom-data")
240
+ def get_data() -> str:
241
+ return "Hello, world!"
242
+
243
+ resources = mcp.list_resources()
244
+ assert len(resources) == 1
245
+ assert resources[0].name == "custom-data"
246
+
247
+ result = await mcp.read_resource("resource://data")
248
+ assert result == "Hello, world!"
249
+
250
+ async def test_resource_decorator_with_description(self):
251
+ mcp = FastMCP()
252
+
253
+ @mcp.resource("resource://data", description="Data resource")
254
+ def get_data() -> str:
255
+ return "Hello, world!"
256
+
257
+ resources = mcp.list_resources()
258
+ assert len(resources) == 1
259
+ assert resources[0].description == "Data resource"
260
+
261
+ async def test_resource_decorator_instance_method(self):
262
+ mcp = FastMCP()
263
+
264
+ class MyClass:
265
+ def __init__(self, prefix: str):
266
+ self.prefix = prefix
267
+
268
+ def get_data(self) -> str:
269
+ return f"{self.prefix} Hello, world!"
270
+
271
+ obj = MyClass("My prefix:")
272
+ mcp.add_resource_from_fn(
273
+ obj.get_data, uri="resource://data", name="instance-resource"
274
+ )
275
+
276
+ result = await mcp.read_resource("resource://data")
277
+ assert result == "My prefix: Hello, world!"
278
+
279
+ async def test_resource_decorator_classmethod(self):
280
+ mcp = FastMCP()
281
+
282
+ class MyClass:
283
+ prefix = "Class prefix:"
284
+
285
+ @classmethod
286
+ def get_data(cls) -> str:
287
+ return f"{cls.prefix} Hello, world!"
288
+
289
+ mcp.add_resource_from_fn(
290
+ MyClass.get_data, uri="resource://data", name="class-resource"
291
+ )
292
+
293
+ result = await mcp.read_resource("resource://data")
294
+ assert result == "Class prefix: Hello, world!"
295
+
296
+ async def test_resource_decorator_staticmethod(self):
297
+ mcp = FastMCP()
298
+
299
+ class MyClass:
300
+ @staticmethod
301
+ @mcp.resource("resource://data")
302
+ def get_data() -> str:
303
+ return "Static Hello, world!"
304
+
305
+ result = await mcp.read_resource("resource://data")
306
+ assert result == "Static Hello, world!"
307
+
308
+ async def test_resource_decorator_async_function(self):
309
+ mcp = FastMCP()
310
+
311
+ @mcp.resource("resource://data")
312
+ async def get_data() -> str:
313
+ return "Async Hello, world!"
314
+
315
+ result = await mcp.read_resource("resource://data")
316
+ assert result == "Async Hello, world!"
317
+
318
+ async def test_resource_decorator_with_tags(self):
319
+ mcp = FastMCP()
320
+
321
+ @mcp.resource("resource://data", tags={"example", "test-tag"})
322
+ def get_data() -> str:
323
+ return "Hello, world!"
324
+
325
+ resources = mcp.list_resources()
326
+ assert len(resources) == 1
327
+ assert resources[0].tags == {"example", "test-tag"}
328
+
329
+
330
+ class TestTemplateDecorator:
331
+ async def test_template_decorator(self):
332
+ mcp = FastMCP()
333
+
334
+ @mcp.resource("resource://{name}/data")
335
+ def get_data(name: str) -> str:
336
+ return f"Data for {name}"
337
+
338
+ templates = mcp.list_resource_templates()
339
+ assert len(templates) == 1
340
+ assert templates[0].uri_template == "resource://{name}/data"
341
+
342
+ result = await mcp.read_resource("resource://test/data")
343
+ assert result == "Data for test"
344
+
345
+ async def test_template_decorator_incorrect_usage(self):
346
+ mcp = FastMCP()
347
+
348
+ with pytest.raises(
349
+ TypeError, match="The @resource decorator was used incorrectly"
350
+ ):
351
+
352
+ @mcp.resource # Missing parentheses #type: ignore
353
+ def get_data(name: str) -> str:
354
+ return f"Data for {name}"
355
+
356
+ async def test_template_decorator_with_name(self):
357
+ mcp = FastMCP()
358
+
359
+ @mcp.resource("resource://{name}/data", name="custom-template")
360
+ def get_data(name: str) -> str:
361
+ return f"Data for {name}"
362
+
363
+ templates = mcp.list_resource_templates()
364
+ assert len(templates) == 1
365
+ assert templates[0].name == "custom-template"
366
+
367
+ result = await mcp.read_resource("resource://test/data")
368
+ assert result == "Data for test"
369
+
370
+ async def test_template_decorator_with_description(self):
371
+ mcp = FastMCP()
372
+
373
+ @mcp.resource("resource://{name}/data", description="Template description")
374
+ def get_data(name: str) -> str:
375
+ return f"Data for {name}"
376
+
377
+ templates = mcp.list_resource_templates()
378
+ assert len(templates) == 1
379
+ assert templates[0].description == "Template description"
380
+
381
+ async def test_template_decorator_instance_method(self):
382
+ mcp = FastMCP()
383
+
384
+ class MyClass:
385
+ def __init__(self, prefix: str):
386
+ self.prefix = prefix
387
+
388
+ def get_data(self, name: str) -> str:
389
+ return f"{self.prefix} Data for {name}"
390
+
391
+ obj = MyClass("My prefix:")
392
+
393
+ mcp.add_resource_from_fn(
394
+ obj.get_data, uri="resource://{name}/data", name="instance-template"
395
+ )
396
+
397
+ result = await mcp.read_resource("resource://test/data")
398
+ assert result == "My prefix: Data for test"
399
+
400
+ async def test_template_decorator_classmethod(self):
401
+ mcp = FastMCP()
402
+
403
+ class MyClass:
404
+ prefix = "Class prefix:"
405
+
406
+ @classmethod
407
+ def get_data(cls, name: str) -> str:
408
+ return f"{cls.prefix} Data for {name}"
409
+
410
+ mcp.add_resource_from_fn(
411
+ MyClass.get_data, uri="resource://{name}/data", name="class-template"
412
+ )
413
+
414
+ result = await mcp.read_resource("resource://test/data")
415
+ assert result == "Class prefix: Data for test"
416
+
417
+ async def test_template_decorator_staticmethod(self):
418
+ mcp = FastMCP()
419
+
420
+ class MyClass:
421
+ @staticmethod
422
+ @mcp.resource("resource://{name}/data")
423
+ def get_data(name: str) -> str:
424
+ return f"Static Data for {name}"
425
+
426
+ result = await mcp.read_resource("resource://test/data")
427
+ assert result == "Static Data for test"
428
+
429
+ async def test_template_decorator_async_function(self):
430
+ mcp = FastMCP()
431
+
432
+ @mcp.resource("resource://{name}/data")
433
+ async def get_data(name: str) -> str:
434
+ return f"Async Data for {name}"
435
+
436
+ result = await mcp.read_resource("resource://test/data")
437
+ assert result == "Async Data for test"
438
+
439
+ async def test_template_decorator_with_tags(self):
440
+ mcp = FastMCP()
441
+
442
+ @mcp.resource("resource://{name}/data", tags={"template", "test-tag"})
443
+ def get_data(name: str) -> str:
444
+ return f"Data for {name}"
445
+
446
+ templates = mcp.list_resource_templates()
447
+ assert len(templates) == 1
448
+ assert templates[0].tags == {"template", "test-tag"}
449
+
450
+
451
+ class TestPromptDecorator:
452
+ async def test_prompt_decorator(self):
453
+ mcp = FastMCP()
454
+
455
+ @mcp.prompt()
456
+ def test_prompt() -> str:
457
+ return "Hello, world!"
458
+
459
+ prompts = mcp.list_prompts()
460
+ assert len(prompts) == 1
461
+ assert prompts[0].name == "test_prompt"
462
+
463
+ result = await mcp.get_prompt("test_prompt")
464
+ assert len(result) == 1
465
+ message = result[0]
466
+ assert isinstance(message.content, TextContent)
467
+ assert message.content.text == "Hello, world!"
468
+
469
+ async def test_prompt_decorator_incorrect_usage(self):
470
+ mcp = FastMCP()
471
+
472
+ with pytest.raises(
473
+ TypeError, match="The @prompt decorator was used incorrectly"
474
+ ):
475
+
476
+ @mcp.prompt # Missing parentheses #type: ignore
477
+ def test_prompt() -> str:
478
+ return "Hello, world!"
479
+
480
+ async def test_prompt_decorator_with_name(self):
481
+ mcp = FastMCP()
482
+
483
+ @mcp.prompt(name="custom-prompt")
484
+ def test_prompt() -> str:
485
+ return "Hello, world!"
486
+
487
+ prompts = mcp.list_prompts()
488
+ assert len(prompts) == 1
489
+ assert prompts[0].name == "custom-prompt"
490
+
491
+ result = await mcp.get_prompt("custom-prompt")
492
+ assert len(result) == 1
493
+ message = result[0]
494
+ assert isinstance(message.content, TextContent)
495
+ assert message.content.text == "Hello, world!"
496
+
497
+ async def test_prompt_decorator_with_description(self):
498
+ mcp = FastMCP()
499
+
500
+ @mcp.prompt(description="Test prompt description")
501
+ def test_prompt() -> str:
502
+ return "Hello, world!"
503
+
504
+ prompts = mcp.list_prompts()
505
+ assert len(prompts) == 1
506
+ assert prompts[0].description == "Test prompt description"
507
+
508
+ async def test_prompt_decorator_with_parameters(self):
509
+ mcp = FastMCP()
510
+
511
+ @mcp.prompt()
512
+ def test_prompt(name: str, greeting: str = "Hello") -> str:
513
+ return f"{greeting}, {name}!"
514
+
515
+ prompts = mcp.list_prompts()
516
+ assert len(prompts) == 1
517
+ assert prompts[0].arguments is not None
518
+ assert len(prompts[0].arguments) == 2
519
+ assert prompts[0].arguments[0].name == "name"
520
+ assert prompts[0].arguments[0].required is True
521
+ assert prompts[0].arguments[1].name == "greeting"
522
+ assert prompts[0].arguments[1].required is False
523
+
524
+ result = await mcp.get_prompt("test_prompt", {"name": "World"})
525
+ assert len(result) == 1
526
+ message = result[0]
527
+ assert isinstance(message.content, TextContent)
528
+ assert message.content.text == "Hello, World!"
529
+
530
+ result = await mcp.get_prompt(
531
+ "test_prompt", {"name": "World", "greeting": "Hi"}
532
+ )
533
+ assert len(result) == 1
534
+ message = result[0]
535
+ assert isinstance(message.content, TextContent)
536
+ assert message.content.text == "Hi, World!"
537
+
538
+ async def test_prompt_decorator_instance_method(self):
539
+ mcp = FastMCP()
540
+
541
+ class MyClass:
542
+ def __init__(self, prefix: str):
543
+ self.prefix = prefix
544
+
545
+ def test_prompt(self) -> str:
546
+ return f"{self.prefix} Hello, world!"
547
+
548
+ obj = MyClass("My prefix:")
549
+ mcp.add_prompt(obj.test_prompt, name="test_prompt")
550
+
551
+ result = await mcp.get_prompt("test_prompt")
552
+ assert len(result) == 1
553
+ message = result[0]
554
+ assert isinstance(message.content, TextContent)
555
+ assert message.content.text == "My prefix: Hello, world!"
556
+
557
+ async def test_prompt_decorator_classmethod(self):
558
+ mcp = FastMCP()
559
+
560
+ class MyClass:
561
+ prefix = "Class prefix:"
562
+
563
+ @classmethod
564
+ def test_prompt(cls) -> str:
565
+ return f"{cls.prefix} Hello, world!"
566
+
567
+ mcp.add_prompt(MyClass.test_prompt, name="test_prompt")
568
+
569
+ result = await mcp.get_prompt("test_prompt")
570
+ assert len(result) == 1
571
+ message = result[0]
572
+ assert isinstance(message.content, TextContent)
573
+ assert message.content.text == "Class prefix: Hello, world!"
574
+
575
+ async def test_prompt_decorator_staticmethod(self):
576
+ mcp = FastMCP()
577
+
578
+ class MyClass:
579
+ @staticmethod
580
+ @mcp.prompt()
581
+ def test_prompt() -> str:
582
+ return "Static Hello, world!"
583
+
584
+ result = await mcp.get_prompt("test_prompt")
585
+ assert len(result) == 1
586
+ message = result[0]
587
+ assert isinstance(message.content, TextContent)
588
+ assert message.content.text == "Static Hello, world!"
589
+
590
+ async def test_prompt_decorator_async_function(self):
591
+ mcp = FastMCP()
592
+
593
+ @mcp.prompt()
594
+ async def test_prompt() -> str:
595
+ return "Async Hello, world!"
596
+
597
+ result = await mcp.get_prompt("test_prompt")
598
+ assert len(result) == 1
599
+ message = result[0]
600
+ assert isinstance(message.content, TextContent)
601
+ assert message.content.text == "Async Hello, world!"
602
+
603
+ async def test_prompt_decorator_with_tags(self):
604
+ mcp = FastMCP()
605
+
606
+ @mcp.prompt(tags={"example", "test-tag"})
607
+ def test_prompt() -> str:
608
+ return "Hello, world!"
609
+
610
+ prompts = mcp.list_prompts()
611
+ assert len(prompts) == 1
612
+ assert prompts[0].tags == {"example", "test-tag"}
613
 
614
 
615
  @pytest.fixture
 
653
 
654
  class TestServerTools:
655
  async def test_add_tool_exists(self, tool_server: FastMCP):
656
+ assert "add" in [t.name for t in await tool_server._mcp_list_tools()]
657
 
658
  async def test_list_tools(self, tool_server: FastMCP):
659
+ assert len(await tool_server._mcp_list_tools()) == 6
660
 
661
  async def test_call_tool(self, tool_server: FastMCP):
662
  result = await tool_server.call_tool("add", {"x": 1, "y": 2})
 
753
  """A greeting tool"""
754
  return f"Hello {title} {name}"
755
 
756
+ tools = await mcp._mcp_list_tools()
757
  assert len(tools) == 1
758
  tool = tools[0]
759
 
 
845
  parameters don't match"""
846
  mcp = FastMCP()
847
 
848
+ with pytest.raises(ValueError, match="mismatch between URI parameters"):
849
 
850
  @mcp.resource("resource://data")
851
  def get_data_fn(param: str) -> str:
 
855
  """Test that a resource with URI parameters is automatically a template"""
856
  mcp = FastMCP()
857
 
858
+ with pytest.raises(ValueError, match="mismatch between URI parameters"):
859
 
860
  @mcp.resource("resource://{param}")
861
  def get_data() -> str:
 
939
 
940
  # Should be registered as a template
941
  assert len(mcp._resource_manager._templates) == 1
942
+ assert len(await mcp._mcp_list_resources()) == 0
943
 
944
  # When accessed, should create a concrete resource
945
  resource = await mcp._resource_manager.get_resource("resource://test/data")
tests/utilities/test_decorated_function.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+ from collections.abc import Callable
3
+ from typing import Any
4
+
5
+ import pytest
6
+
7
+ from fastmcp.utilities.decorators import DecoratedFunction
8
+
9
+ DECORATOR_CALLED = []
10
+
11
+
12
+ def decorator(fn: Callable[..., Any]) -> DecoratedFunction[..., Any]:
13
+ @functools.wraps(fn)
14
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
15
+ DECORATOR_CALLED.append((args, kwargs))
16
+ return fn(*args, **kwargs)
17
+
18
+ return DecoratedFunction(wrapper)
19
+
20
+
21
+ @pytest.fixture(autouse=True)
22
+ def reset_decorator_called():
23
+ DECORATOR_CALLED.clear()
24
+ yield
25
+ DECORATOR_CALLED.clear()
26
+
27
+
28
+ @decorator
29
+ def add(a: int, b: int) -> int:
30
+ return a + b
31
+
32
+
33
+ @decorator
34
+ async def add_async(a: int, b: int) -> int:
35
+ return a + b
36
+
37
+
38
+ class DecoratedClass:
39
+ def __init__(self, x: int):
40
+ self.x = x
41
+
42
+ @decorator
43
+ def add(self, a: int, b: int) -> int:
44
+ return a + b + self.x
45
+
46
+ @decorator
47
+ async def add_async(self, a: int, b: int) -> int:
48
+ return a + b + self.x
49
+
50
+ @classmethod
51
+ @decorator
52
+ def add_classmethod(cls, a: int, b: int) -> int:
53
+ return a + b
54
+
55
+ @staticmethod
56
+ @decorator
57
+ def add_staticmethod(a: int, b: int) -> int:
58
+ return a + b
59
+
60
+ @classmethod
61
+ @decorator
62
+ async def add_classmethod_async(cls, a: int, b: int) -> int:
63
+ return a + b
64
+
65
+ @staticmethod
66
+ @decorator
67
+ async def add_staticmethod_async(a: int, b: int) -> int:
68
+ return a + b
69
+
70
+ @decorator
71
+ @classmethod
72
+ def add_classmethod_reverse_decorator_order(cls, a: int, b: int) -> int:
73
+ return a + b
74
+
75
+ @decorator
76
+ @staticmethod
77
+ def add_staticmethod_reverse_decorator_order(a: int, b: int) -> int:
78
+ return a + b
79
+
80
+ @decorator
81
+ @classmethod
82
+ async def add_classmethod_async_reverse_decorator_order(cls, a: int, b: int) -> int:
83
+ return a + b
84
+
85
+ @decorator
86
+ @staticmethod
87
+ async def add_staticmethod_async_reverse_decorator_order(a: int, b: int) -> int:
88
+ return a + b
89
+
90
+
91
+ def test_add():
92
+ assert add(1, 2) == 3
93
+ assert DECORATOR_CALLED == [((1, 2), {})]
94
+ DECORATOR_CALLED.clear()
95
+
96
+ # Test with keyword arguments
97
+ assert add(a=3, b=4) == 7
98
+ assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
99
+
100
+
101
+ async def test_add_async():
102
+ assert await add_async(1, 2) == 3
103
+ assert DECORATOR_CALLED == [((1, 2), {})]
104
+ DECORATOR_CALLED.clear()
105
+
106
+ # Test with keyword arguments
107
+ assert await add_async(a=3, b=4) == 7
108
+ assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
109
+
110
+
111
+ def test_instance_method():
112
+ obj = DecoratedClass(10)
113
+ assert obj.add(2, 3) == 15
114
+ assert DECORATOR_CALLED == [((obj, 2, 3), {})]
115
+ DECORATOR_CALLED.clear()
116
+
117
+ # Test with keyword arguments
118
+ assert obj.add(a=4, b=5) == 19
119
+ assert DECORATOR_CALLED == [((obj,), {"a": 4, "b": 5})]
120
+
121
+
122
+ async def test_instance_method_async():
123
+ obj = DecoratedClass(10)
124
+ assert await obj.add_async(2, 3) == 15
125
+ assert DECORATOR_CALLED == [((obj, 2, 3), {})]
126
+ DECORATOR_CALLED.clear()
127
+
128
+ # Test with keyword arguments
129
+ assert await obj.add_async(a=4, b=5) == 19
130
+ assert DECORATOR_CALLED == [((obj,), {"a": 4, "b": 5})]
131
+
132
+
133
+ def test_classmethod():
134
+ assert DecoratedClass.add_classmethod(1, 2) == 3
135
+ assert DECORATOR_CALLED == [((DecoratedClass, 1, 2), {})]
136
+ DECORATOR_CALLED.clear()
137
+
138
+ # Test with keyword arguments
139
+ assert DecoratedClass.add_classmethod(a=3, b=4) == 7
140
+ assert DECORATOR_CALLED == [((DecoratedClass,), {"a": 3, "b": 4})]
141
+ DECORATOR_CALLED.clear()
142
+
143
+ # Test via instance
144
+ obj = DecoratedClass(10)
145
+ assert obj.add_classmethod(5, 6) == 11
146
+ assert DECORATOR_CALLED == [((DecoratedClass, 5, 6), {})]
147
+
148
+
149
+ async def test_classmethod_async():
150
+ assert await DecoratedClass.add_classmethod_async(1, 2) == 3
151
+ assert DECORATOR_CALLED == [((DecoratedClass, 1, 2), {})]
152
+ DECORATOR_CALLED.clear()
153
+
154
+ # Test with keyword arguments
155
+ assert await DecoratedClass.add_classmethod_async(a=3, b=4) == 7
156
+ assert DECORATOR_CALLED == [((DecoratedClass,), {"a": 3, "b": 4})]
157
+ DECORATOR_CALLED.clear()
158
+
159
+ # Test via instance
160
+ obj = DecoratedClass(10)
161
+ assert await obj.add_classmethod_async(5, 6) == 11
162
+ assert DECORATOR_CALLED == [((DecoratedClass, 5, 6), {})]
163
+
164
+
165
+ def test_classmethod_wrong_order():
166
+ with pytest.raises(
167
+ TypeError,
168
+ match="To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top.",
169
+ ):
170
+ DecoratedClass.add_classmethod_reverse_decorator_order(1, 2)
171
+
172
+
173
+ async def test_classmethod_async_wrong_order():
174
+ with pytest.raises(
175
+ TypeError,
176
+ match="To apply this decorator to a classmethod, apply the decorator first, then @classmethod on top.",
177
+ ):
178
+ await DecoratedClass.add_classmethod_async_reverse_decorator_order(1, 2)
179
+
180
+
181
+ def test_staticmethod():
182
+ assert DecoratedClass.add_staticmethod(1, 2) == 3
183
+ assert DECORATOR_CALLED == [((1, 2), {})]
184
+ DECORATOR_CALLED.clear()
185
+
186
+ # Test with keyword arguments
187
+ assert DecoratedClass.add_staticmethod(a=3, b=4) == 7
188
+ assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
189
+ DECORATOR_CALLED.clear()
190
+
191
+ # Test via instance
192
+ obj = DecoratedClass(10)
193
+ assert obj.add_staticmethod(5, 6) == 11
194
+ assert DECORATOR_CALLED == [((5, 6), {})]
195
+
196
+
197
+ async def test_staticmethod_async():
198
+ assert await DecoratedClass.add_staticmethod_async(1, 2) == 3
199
+ assert DECORATOR_CALLED == [((1, 2), {})]
200
+ DECORATOR_CALLED.clear()
201
+
202
+ # Test with keyword arguments
203
+ assert await DecoratedClass.add_staticmethod_async(a=3, b=4) == 7
204
+ assert DECORATOR_CALLED == [((), {"a": 3, "b": 4})]
205
+ DECORATOR_CALLED.clear()
206
+
207
+ # Test via instance
208
+ obj = DecoratedClass(10)
209
+ assert await obj.add_staticmethod_async(5, 6) == 11
210
+ assert DECORATOR_CALLED == [((5, 6), {})]
211
+
212
+
213
+ def test_staticmethod_wrong_order():
214
+ assert DecoratedClass.add_staticmethod_reverse_decorator_order(1, 2) == 3
215
+ assert DECORATOR_CALLED == [((1, 2), {})]
216
+
217
+
218
+ async def test_staticmethod_async_wrong_order():
219
+ assert (
220
+ await DecoratedClass.add_staticmethod_async_reverse_decorator_order(1, 2) == 3
221
+ )
222
+ assert DECORATOR_CALLED == [((1, 2), {})]