Jeremiah Lowin commited on
Commit
ec6bd43
·
1 Parent(s): 5ee8f66

Ensure methods work/are documented

Browse files
docs/patterns/decorating-methods.mdx CHANGED
@@ -16,11 +16,36 @@ When you apply a FastMCP decorator like `@tool`, `@resource()`, or `@prompt()` t
16
 
17
  This means directly decorating these methods doesn't work as expected. In practice, the LLM would see parameters like `self` or `cls` that it cannot provide values for.
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  ## Recommended Patterns
20
 
21
  ### Instance Methods
22
 
23
- **Don't do this** (it doesn't work properly):
 
24
 
25
  ```python
26
  from fastmcp import FastMCP
@@ -28,17 +53,14 @@ from fastmcp import FastMCP
28
  mcp = FastMCP()
29
 
30
  class MyClass:
31
- @mcp.tool() # This won't work correctly
32
  def add(self, x, y):
33
  return x + y
34
-
35
- @mcp.resource("resource://{param}") # This won't work correctly
36
- def get_resource(self, param: str):
37
- return f"Resource data for {param}"
38
  ```
39
-
40
  When the decorator is applied this way, it captures the unbound method. When the LLM later tries to use this component, it will see `self` as a required parameter, but it won't know what to provide for it, causing errors or unexpected behavior.
41
 
 
42
  **Do this instead**:
43
 
44
  ```python
@@ -49,21 +71,15 @@ mcp = FastMCP()
49
  class MyClass:
50
  def add(self, x, y):
51
  return x + y
52
-
53
- def get_resource(self, param: str):
54
- return f"Resource data for {param}"
55
 
56
- # Create an instance first, then add the bound methods
57
  obj = MyClass()
58
- mcp.add_tool(obj.add)
59
- mcp.add_resource_fn(obj.get_resource, uri="resource://{param}") # For resources or templates
60
-
61
- # Note: FastMCP provides add_resource() for adding Resource objects directly and
62
- # add_resource_fn() for adding functions that generate resources or templates
63
 
64
  # Now you can call it without 'self' showing up as a parameter
65
- await mcp.call_tool('add', {'x': 1, 'y': 2}) # Returns 3
66
  ```
 
67
 
68
  This approach works because:
69
  1. You first create an instance of the class (`obj`)
@@ -72,9 +88,10 @@ This approach works because:
72
 
73
  ### Class Methods
74
 
75
- Similar to instance methods, decorating class methods directly doesn't work properly:
76
 
77
- **Don't do this**:
 
78
 
79
  ```python
80
  from fastmcp import FastMCP
@@ -83,13 +100,21 @@ mcp = FastMCP()
83
 
84
  class MyClass:
85
  @classmethod
86
- @mcp.tool() # This won't work correctly
87
- def from_string(cls, s):
 
 
 
 
 
88
  return cls(s)
89
  ```
 
90
 
91
- The problem here is that the FastMCP decorator is applied before the `@classmethod` decorator (Python applies decorators bottom-to-top). So it captures the function before it's transformed into a class method, leading to incorrect behavior.
 
92
 
 
93
  **Do this instead**:
94
 
95
  ```python
@@ -102,9 +127,10 @@ class MyClass:
102
  def from_string(cls, s):
103
  return cls(s)
104
 
105
- # Add the class method after the class is defined
106
- mcp.add_tool(MyClass.from_string)
107
  ```
 
108
 
109
  This works because:
110
  1. The `@classmethod` decorator is applied properly during class definition
@@ -113,7 +139,10 @@ This works because:
113
 
114
  ### Static Methods
115
 
116
- Unlike instance and class methods, static methods work fine with FastMCP decorators:
 
 
 
117
 
118
  ```python
119
  from fastmcp import FastMCP
@@ -121,23 +150,17 @@ from fastmcp import FastMCP
121
  mcp = FastMCP()
122
 
123
  class MyClass:
 
124
  @staticmethod
125
- @mcp.tool() # This works!
126
  def utility(x, y):
127
  return x + y
128
-
129
- @staticmethod
130
- @mcp.resource("resource://data") # This works too!
131
- def get_data():
132
- return "Static resource data"
133
  ```
 
134
 
135
- This approach works because:
136
- 1. The `@staticmethod` decorator is applied first (executed last), transforming the method into a regular function
137
- 2. When the FastMCP decorator is applied, it's capturing what is effectively just a regular function
138
- 3. A static method doesn't have any binding requirements - it doesn't receive a `self` or `cls` parameter
139
 
140
- Alternatively, you can use the same pattern as the other methods:
 
141
 
142
  ```python
143
  from fastmcp import FastMCP
@@ -150,10 +173,9 @@ class MyClass:
150
  return x + y
151
 
152
  # This also works
153
- mcp.add_tool(MyClass.utility)
154
  ```
155
-
156
- This works for the same reason - a static method is essentially just a function in a class namespace.
157
 
158
  ## Additional Patterns
159
 
@@ -169,8 +191,8 @@ mcp = FastMCP()
169
  class ComponentProvider:
170
  def __init__(self, mcp_instance):
171
  # Register methods
172
- mcp_instance.add_tool(self.tool_method)
173
- mcp_instance.add_resource_fn(self.resource_method, uri="resource://data")
174
 
175
  def tool_method(self, x):
176
  return x * 2
@@ -191,11 +213,13 @@ The class automatically registers its methods during initialization, ensuring th
191
 
192
  ## Summary
193
 
194
- While FastMCP's decorator pattern works seamlessly with regular functions and static methods, for instance methods and class methods, you should add them after creating the instance or class. This ensures that the methods are properly bound before being registered.
 
 
 
 
 
 
195
 
196
- These patterns apply to all FastMCP decorators and registration methods:
197
- - `@tool()` and `add_tool`
198
- - `@resource()` and `add_resource_fn()`
199
- - `@prompt()` and `add_prompt()`
200
 
201
- Understanding these patterns allows you to effectively organize your components into classes while maintaining proper method binding, giving you the benefits of object-oriented design without sacrificing the simplicity of FastMCP's decorator system.
 
16
 
17
  This means directly decorating these methods doesn't work as expected. In practice, the LLM would see parameters like `self` or `cls` that it cannot provide values for.
18
 
19
+ Additionally, **FastMCP decorators return objects (Tool, Resource, or Prompt instances) rather than the original function**. This means that when you decorate a method directly, the method becomes the returned object and is no longer callable by your code:
20
+
21
+ <Warning>
22
+ **Don't do this!**
23
+
24
+ The method will no longer be callable from Python, and the tool won't be callable by LLMs.
25
+
26
+ ```python
27
+
28
+ from fastmcp import FastMCP
29
+ mcp = FastMCP()
30
+
31
+ class MyClass:
32
+ @mcp.tool()
33
+ def my_method(self, x: int) -> int:
34
+ return x * 2
35
+
36
+ obj = MyClass()
37
+ obj.my_method(5) # Fails - my_method is a Tool, not a function
38
+ ```
39
+ </Warning>
40
+
41
+ This is another important reason to register methods functionally after defining the class.
42
+
43
  ## Recommended Patterns
44
 
45
  ### Instance Methods
46
 
47
+ <Warning>
48
+ **Don't do this!**
49
 
50
  ```python
51
  from fastmcp import FastMCP
 
53
  mcp = FastMCP()
54
 
55
  class MyClass:
56
+ @mcp.tool() # This won't work correctly
57
  def add(self, x, y):
58
  return x + y
 
 
 
 
59
  ```
60
+ </Warning>
61
  When the decorator is applied this way, it captures the unbound method. When the LLM later tries to use this component, it will see `self` as a required parameter, but it won't know what to provide for it, causing errors or unexpected behavior.
62
 
63
+ <Check>
64
  **Do this instead**:
65
 
66
  ```python
 
71
  class MyClass:
72
  def add(self, x, y):
73
  return x + y
 
 
 
74
 
75
+ # Create an instance first, then register the bound methods
76
  obj = MyClass()
77
+ mcp.tool(obj.add)
 
 
 
 
78
 
79
  # Now you can call it without 'self' showing up as a parameter
80
+ await mcp._mcp_call_tool('add', {'x': 1, 'y': 2}) # Returns 3
81
  ```
82
+ </Check>
83
 
84
  This approach works because:
85
  1. You first create an instance of the class (`obj`)
 
88
 
89
  ### Class Methods
90
 
91
+ The behavior of decorating class methods depends on the order of decorators:
92
 
93
+ <Warning>
94
+ **Don't do this** (decorator order matters):
95
 
96
  ```python
97
  from fastmcp import FastMCP
 
100
 
101
  class MyClass:
102
  @classmethod
103
+ @mcp.tool() # This won't work but won't raise an error
104
+ def from_string_v1(cls, s):
105
+ return cls(s)
106
+
107
+ @mcp.tool()
108
+ @classmethod # This will raise a helpful ValueError
109
+ def from_string_v2(cls, s):
110
  return cls(s)
111
  ```
112
+ </Warning>
113
 
114
+ - If `@classmethod` comes first, then `@mcp.tool()`: No error is raised, but it won't work correctly
115
+ - If `@mcp.tool()` comes first, then `@classmethod`: FastMCP will detect this and raise a helpful `ValueError` with guidance
116
 
117
+ <Check>
118
  **Do this instead**:
119
 
120
  ```python
 
127
  def from_string(cls, s):
128
  return cls(s)
129
 
130
+ # Register the class method after the class is defined
131
+ mcp.tool(MyClass.from_string)
132
  ```
133
+ </Check>
134
 
135
  This works because:
136
  1. The `@classmethod` decorator is applied properly during class definition
 
139
 
140
  ### Static Methods
141
 
142
+ Static methods "work" with FastMCP decorators, but this is not recommended because the FastMCP decorator will not return a callable method. Therefore, you should register static methods the same way as other methods.
143
+
144
+ <Warning>
145
+ **This is not recommended, though it will work.**
146
 
147
  ```python
148
  from fastmcp import FastMCP
 
150
  mcp = FastMCP()
151
 
152
  class MyClass:
153
+ @mcp.tool()
154
  @staticmethod
 
155
  def utility(x, y):
156
  return x + y
 
 
 
 
 
157
  ```
158
+ </Warning>
159
 
160
+ This works because `@staticmethod` converts the method to a regular function, which the FastMCP decorator can then properly process. However, this is not recommended because the FastMCP decorator will not return a callable staticmethod. Therefore, you should register static methods the same way as other methods.
 
 
 
161
 
162
+ <Check>
163
+ **Prefer this pattern:**
164
 
165
  ```python
166
  from fastmcp import FastMCP
 
173
  return x + y
174
 
175
  # This also works
176
+ mcp.tool(MyClass.utility)
177
  ```
178
+ </Check>
 
179
 
180
  ## Additional Patterns
181
 
 
191
  class ComponentProvider:
192
  def __init__(self, mcp_instance):
193
  # Register methods
194
+ mcp_instance.tool(self.tool_method)
195
+ mcp_instance.resource("resource://data")(self.resource_method)
196
 
197
  def tool_method(self, x):
198
  return x * 2
 
213
 
214
  ## Summary
215
 
216
+ The current behavior of FastMCP decorators with methods is:
217
+
218
+ - **Static methods**: Can be decorated directly and work perfectly with all FastMCP decorators
219
+ - **Class methods**: Cannot be decorated directly and will raise a helpful `ValueError` with guidance
220
+ - **Instance methods**: Should be registered after creating an instance using the decorator calls
221
+
222
+ For class and instance methods, you should register them after creating the instance or class to ensure proper method binding. This ensures that the methods are properly bound before being registered.
223
 
 
 
 
 
224
 
225
+ Understanding these patterns allows you to effectively organize your components into classes while maintaining proper method binding, giving you the benefits of object-oriented design without sacrificing the simplicity of FastMCP's decorator system.
src/fastmcp/prompts/prompt.py CHANGED
@@ -171,6 +171,9 @@ class FunctionPrompt(Prompt):
171
  # if the fn is a callable class, we need to get the __call__ method from here out
172
  if not inspect.isroutine(fn):
173
  fn = fn.__call__
 
 
 
174
 
175
  type_adapter = get_cached_typeadapter(fn)
176
  parameters = type_adapter.json_schema()
 
171
  # if the fn is a callable class, we need to get the __call__ method from here out
172
  if not inspect.isroutine(fn):
173
  fn = fn.__call__
174
+ # if the fn is a staticmethod, we need to work with the underlying function
175
+ if isinstance(fn, staticmethod):
176
+ fn = fn.__func__
177
 
178
  type_adapter = get_cached_typeadapter(fn)
179
  parameters = type_adapter.json_schema()
src/fastmcp/resources/template.py CHANGED
@@ -225,8 +225,12 @@ class FunctionResourceTemplate(ResourceTemplate):
225
 
226
  description = description or fn.__doc__
227
 
 
228
  if not inspect.isroutine(fn):
229
  fn = fn.__call__
 
 
 
230
 
231
  type_adapter = get_cached_typeadapter(fn)
232
  parameters = type_adapter.json_schema()
 
225
 
226
  description = description or fn.__doc__
227
 
228
+ # if the fn is a callable class, we need to get the __call__ method from here out
229
  if not inspect.isroutine(fn):
230
  fn = fn.__call__
231
+ # if the fn is a staticmethod, we need to work with the underlying function
232
+ if isinstance(fn, staticmethod):
233
+ fn = fn.__func__
234
 
235
  type_adapter = get_cached_typeadapter(fn)
236
  parameters = type_adapter.json_schema()
src/fastmcp/server/server.py CHANGED
@@ -589,8 +589,20 @@ class FastMCP(Generic[LifespanResultT]):
589
  if isinstance(annotations, dict):
590
  annotations = ToolAnnotations(**annotations)
591
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  # Determine the actual name and function based on the calling pattern
593
- if callable(name_or_fn):
594
  # Case 1: @tool (without parens) - function passed directly
595
  # Case 2: direct call like tool(fn, name="something")
596
  fn = name_or_fn
@@ -747,7 +759,7 @@ class FastMCP(Generic[LifespanResultT]):
747
  return f"Weather for {city}: {data}"
748
  """
749
  # Check if user passed function directly instead of calling decorator
750
- if callable(uri):
751
  raise TypeError(
752
  "The @resource decorator was used incorrectly. "
753
  "Did you forget to call it? Use @resource('uri') instead of @resource"
@@ -756,6 +768,18 @@ class FastMCP(Generic[LifespanResultT]):
756
  def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
757
  from fastmcp.server.context import Context
758
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  # Check if this should be a template
760
  has_uri_params = "{" in uri and "}" in uri
761
  # check if the function has any parameters (other than injected context)
@@ -896,8 +920,21 @@ class FastMCP(Generic[LifespanResultT]):
896
  # Direct function call
897
  server.prompt(my_function, name="custom_name")
898
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
899
  # Determine the actual name and function based on the calling pattern
900
- if callable(name_or_fn):
901
  # Case 1: @prompt (without parens) - function passed directly as decorator
902
  # Case 2: direct call like prompt(fn, name="something")
903
  fn = name_or_fn
 
589
  if isinstance(annotations, dict):
590
  annotations = ToolAnnotations(**annotations)
591
 
592
+ if isinstance(name_or_fn, classmethod):
593
+ raise ValueError(
594
+ inspect.cleandoc(
595
+ """
596
+ To decorate a classmethod, first define the method and then call
597
+ tool() directly on the method instead of using it as a
598
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
599
+ for examples and more information.
600
+ """
601
+ )
602
+ )
603
+
604
  # Determine the actual name and function based on the calling pattern
605
+ if inspect.isroutine(name_or_fn):
606
  # Case 1: @tool (without parens) - function passed directly
607
  # Case 2: direct call like tool(fn, name="something")
608
  fn = name_or_fn
 
759
  return f"Weather for {city}: {data}"
760
  """
761
  # Check if user passed function directly instead of calling decorator
762
+ if inspect.isroutine(uri):
763
  raise TypeError(
764
  "The @resource decorator was used incorrectly. "
765
  "Did you forget to call it? Use @resource('uri') instead of @resource"
 
768
  def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
769
  from fastmcp.server.context import Context
770
 
771
+ if isinstance(fn, classmethod):
772
+ raise ValueError(
773
+ inspect.cleandoc(
774
+ """
775
+ To decorate a classmethod, first define the method and then call
776
+ resource() directly on the method instead of using it as a
777
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
778
+ for examples and more information.
779
+ """
780
+ )
781
+ )
782
+
783
  # Check if this should be a template
784
  has_uri_params = "{" in uri and "}" in uri
785
  # check if the function has any parameters (other than injected context)
 
920
  # Direct function call
921
  server.prompt(my_function, name="custom_name")
922
  """
923
+
924
+ if isinstance(name_or_fn, classmethod):
925
+ raise ValueError(
926
+ inspect.cleandoc(
927
+ """
928
+ To decorate a classmethod, first define the method and then call
929
+ prompt() directly on the method instead of using it as a
930
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
931
+ for examples and more information.
932
+ """
933
+ )
934
+ )
935
+
936
  # Determine the actual name and function based on the calling pattern
937
+ if inspect.isroutine(name_or_fn):
938
  # Case 1: @prompt (without parens) - function passed directly as decorator
939
  # Case 2: direct call like prompt(fn, name="something")
940
  fn = name_or_fn
src/fastmcp/tools/tool.py CHANGED
@@ -146,6 +146,9 @@ class FunctionTool(Tool):
146
  # if the fn is a callable class, we need to get the __call__ method from here out
147
  if not inspect.isroutine(fn):
148
  fn = fn.__call__
 
 
 
149
 
150
  type_adapter = get_cached_typeadapter(fn)
151
  schema = type_adapter.json_schema()
 
146
  # if the fn is a callable class, we need to get the __call__ method from here out
147
  if not inspect.isroutine(fn):
148
  fn = fn.__call__
149
+ # if the fn is a staticmethod, we need to work with the underlying function
150
+ if isinstance(fn, staticmethod):
151
+ fn = fn.__func__
152
 
153
  type_adapter = get_cached_typeadapter(fn)
154
  schema = type_adapter.json_schema()
tests/server/test_server.py CHANGED
@@ -205,8 +205,8 @@ class TestToolDecorator:
205
  mcp = FastMCP()
206
 
207
  class MyClass:
208
- @staticmethod
209
  @mcp.tool
 
210
  def add(x: int, y: int) -> int:
211
  return x + y
212
 
@@ -223,6 +223,17 @@ class TestToolDecorator:
223
  result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
224
  assert result[0].text == "3" # type: ignore[attr-defined]
225
 
 
 
 
 
 
 
 
 
 
 
 
226
  async def test_tool_decorator_classmethod_async_function(self):
227
  mcp = FastMCP()
228
 
@@ -249,6 +260,20 @@ class TestToolDecorator:
249
  result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
250
  assert result[0].text == "3" # type: ignore[attr-defined]
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  async def test_tool_decorator_with_tags(self):
253
  """Test that the tool decorator properly sets tags."""
254
  mcp = FastMCP()
@@ -480,12 +505,23 @@ class TestResourceDecorator:
480
  result = await client.read_resource("resource://data")
481
  assert result[0].text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
482
 
 
 
 
 
 
 
 
 
 
 
 
483
  async def test_resource_decorator_staticmethod(self):
484
  mcp = FastMCP()
485
 
486
  class MyClass:
487
- @staticmethod
488
  @mcp.resource("resource://data")
 
489
  def get_data() -> str:
490
  return "Static Hello, world!"
491
 
@@ -504,6 +540,20 @@ class TestResourceDecorator:
504
  result = await client.read_resource("resource://data")
505
  assert result[0].text == "Async Hello, world!" # type: ignore[attr-defined]
506
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
 
508
  class TestTemplateDecorator:
509
  async def test_template_decorator(self):
@@ -609,8 +659,8 @@ class TestTemplateDecorator:
609
  mcp = FastMCP()
610
 
611
  class MyClass:
612
- @staticmethod
613
  @mcp.resource("resource://{name}/data")
 
614
  def get_data(name: str) -> str:
615
  return f"Static Data for {name}"
616
 
@@ -783,12 +833,23 @@ class TestPromptDecorator:
783
  message = result.messages[0]
784
  assert message.content.text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
785
 
 
 
 
 
 
 
 
 
 
 
 
786
  async def test_prompt_decorator_staticmethod(self):
787
  mcp = FastMCP()
788
 
789
  class MyClass:
790
- @staticmethod
791
  @mcp.prompt
 
792
  def test_prompt() -> str:
793
  return "Static Hello, world!"
794
 
@@ -881,6 +942,22 @@ class TestPromptDecorator:
881
  def my_function() -> str:
882
  return "Hello, world!"
883
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
 
885
  class TestResourcePrefixHelpers:
886
  @pytest.mark.parametrize(
 
205
  mcp = FastMCP()
206
 
207
  class MyClass:
 
208
  @mcp.tool
209
+ @staticmethod
210
  def add(x: int, y: int) -> int:
211
  return x + y
212
 
 
223
  result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
224
  assert result[0].text == "3" # type: ignore[attr-defined]
225
 
226
+ async def test_tool_decorator_classmethod_error(self):
227
+ mcp = FastMCP()
228
+
229
+ with pytest.raises(ValueError, match="To decorate a classmethod"):
230
+
231
+ class MyClass:
232
+ @mcp.tool
233
+ @classmethod
234
+ def add(cls, y: int) -> None:
235
+ pass
236
+
237
  async def test_tool_decorator_classmethod_async_function(self):
238
  mcp = FastMCP()
239
 
 
260
  result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
261
  assert result[0].text == "3" # type: ignore[attr-defined]
262
 
263
+ async def test_tool_decorator_staticmethod_order(self):
264
+ """Test that the recommended decorator order works for static methods"""
265
+ mcp = FastMCP()
266
+
267
+ class MyClass:
268
+ @staticmethod
269
+ @mcp.tool
270
+ def add_v1(x: int, y: int) -> int:
271
+ return x + y
272
+
273
+ # Test that the recommended order works
274
+ result = await mcp._mcp_call_tool("add_v1", {"x": 1, "y": 2})
275
+ assert result[0].text == "3" # type: ignore[attr-defined]
276
+
277
  async def test_tool_decorator_with_tags(self):
278
  """Test that the tool decorator properly sets tags."""
279
  mcp = FastMCP()
 
505
  result = await client.read_resource("resource://data")
506
  assert result[0].text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
507
 
508
+ async def test_resource_decorator_classmethod_error(self):
509
+ mcp = FastMCP()
510
+
511
+ with pytest.raises(ValueError, match="To decorate a classmethod"):
512
+
513
+ class MyClass:
514
+ @mcp.resource("resource://data")
515
+ @classmethod
516
+ def get_data(cls) -> None:
517
+ pass
518
+
519
  async def test_resource_decorator_staticmethod(self):
520
  mcp = FastMCP()
521
 
522
  class MyClass:
 
523
  @mcp.resource("resource://data")
524
+ @staticmethod
525
  def get_data() -> str:
526
  return "Static Hello, world!"
527
 
 
540
  result = await client.read_resource("resource://data")
541
  assert result[0].text == "Async Hello, world!" # type: ignore[attr-defined]
542
 
543
+ async def test_resource_decorator_staticmethod_order(self):
544
+ """Test that both decorator orders work for static methods"""
545
+ mcp = FastMCP()
546
+
547
+ class MyClass:
548
+ @mcp.resource("resource://data") # type: ignore[misc] # Type checker warns but runtime works
549
+ @staticmethod
550
+ def get_data() -> str:
551
+ return "Static Hello, world!"
552
+
553
+ async with Client(mcp) as client:
554
+ result = await client.read_resource("resource://data")
555
+ assert result[0].text == "Static Hello, world!" # type: ignore[attr-defined]
556
+
557
 
558
  class TestTemplateDecorator:
559
  async def test_template_decorator(self):
 
659
  mcp = FastMCP()
660
 
661
  class MyClass:
 
662
  @mcp.resource("resource://{name}/data")
663
+ @staticmethod
664
  def get_data(name: str) -> str:
665
  return f"Static Data for {name}"
666
 
 
833
  message = result.messages[0]
834
  assert message.content.text == "Class prefix: Hello, world!" # type: ignore[attr-defined]
835
 
836
+ async def test_prompt_decorator_classmethod_error(self):
837
+ mcp = FastMCP()
838
+
839
+ with pytest.raises(ValueError, match="To decorate a classmethod"):
840
+
841
+ class MyClass:
842
+ @mcp.prompt
843
+ @classmethod
844
+ def test_prompt(cls) -> None:
845
+ pass
846
+
847
  async def test_prompt_decorator_staticmethod(self):
848
  mcp = FastMCP()
849
 
850
  class MyClass:
 
851
  @mcp.prompt
852
+ @staticmethod
853
  def test_prompt() -> str:
854
  return "Static Hello, world!"
855
 
 
942
  def my_function() -> str:
943
  return "Hello, world!"
944
 
945
+ async def test_prompt_decorator_staticmethod_order(self):
946
+ """Test that both decorator orders work for static methods"""
947
+ mcp = FastMCP()
948
+
949
+ class MyClass:
950
+ @mcp.prompt # type: ignore[misc] # Type checker warns but runtime works
951
+ @staticmethod
952
+ def test_prompt() -> str:
953
+ return "Static Hello, world!"
954
+
955
+ async with Client(mcp) as client:
956
+ result = await client.get_prompt("test_prompt")
957
+ assert len(result.messages) == 1
958
+ message = result.messages[0]
959
+ assert message.content.text == "Static Hello, world!" # type: ignore[attr-defined]
960
+
961
 
962
  class TestResourcePrefixHelpers:
963
  @pytest.mark.parametrize(