Jeremiah Lowin commited on
Commit
feb341e
·
unverified ·
2 Parent(s): 9866fde5221da7

Merge pull request #706 from jlowin/object-functions

Browse files

Support flexible @tool decorator call patterns

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +4 -4
  2. docs/clients/transports.mdx +1 -1
  3. docs/deployment/asgi.mdx +1 -1
  4. docs/deployment/running-server.mdx +2 -2
  5. docs/getting-started/quickstart.mdx +3 -3
  6. docs/getting-started/welcome.mdx +1 -1
  7. docs/integrations/anthropic.mdx +2 -2
  8. docs/integrations/claude-desktop.mdx +1 -1
  9. docs/integrations/gemini.mdx +1 -1
  10. docs/integrations/openai.mdx +2 -2
  11. docs/patterns/cli.mdx +1 -1
  12. docs/patterns/decorating-methods.mdx +6 -6
  13. docs/patterns/http-requests.mdx +2 -2
  14. docs/patterns/testing.mdx +1 -1
  15. docs/servers/auth/bearer.mdx +1 -1
  16. docs/servers/composition.mdx +3 -3
  17. docs/servers/context.mdx +10 -10
  18. docs/servers/fastmcp.mdx +4 -4
  19. docs/servers/proxy.mdx +1 -1
  20. docs/servers/tools.mdx +29 -29
  21. examples/complex_inputs.py +1 -1
  22. examples/config_server.py +2 -2
  23. examples/desktop.py +1 -1
  24. examples/echo.py +1 -1
  25. examples/memory.py +2 -2
  26. examples/mount_example.py +3 -3
  27. examples/sampling.py +1 -1
  28. examples/screenshot.py +1 -1
  29. examples/serializer.py +1 -1
  30. examples/simple_echo.py +1 -1
  31. examples/smart_home/src/smart_home/hub.py +1 -1
  32. examples/smart_home/src/smart_home/lights/server.py +9 -9
  33. src/fastmcp/contrib/bulk_tool_caller/example.py +1 -1
  34. src/fastmcp/server/context.py +1 -1
  35. src/fastmcp/server/server.py +59 -19
  36. tests/auth/providers/test_bearer.py +1 -1
  37. tests/auth/test_oauth_client.py +1 -1
  38. tests/client/test_client.py +7 -7
  39. tests/client/test_logs.py +2 -2
  40. tests/client/test_progress.py +1 -1
  41. tests/client/test_roots.py +1 -1
  42. tests/client/test_sampling.py +3 -3
  43. tests/client/test_sse.py +3 -3
  44. tests/client/test_stdio.py +1 -1
  45. tests/client/test_streamable_http.py +3 -3
  46. tests/deprecated/test_deprecated.py +1 -1
  47. tests/deprecated/test_mount_separators.py +1 -1
  48. tests/server/http/test_http_dependencies.py +1 -1
  49. tests/server/test_file_server.py +1 -1
  50. tests/server/test_import_server.py +6 -6
README.md CHANGED
@@ -31,7 +31,7 @@ from fastmcp import FastMCP
31
 
32
  mcp = FastMCP("Demo 🚀")
33
 
34
- @mcp.tool()
35
  def add(a: int, b: int) -> int:
36
  """Add two numbers"""
37
  return a + b
@@ -144,7 +144,7 @@ Learn more in the [**FastMCP Server Documentation**](https://gofastmcp.com/serve
144
  Tools allow LLMs to perform actions by executing your Python functions (sync or async). Ideal for computations, API calls, or side effects (like `POST`/`PUT`). FastMCP handles schema generation from type hints and docstrings. Tools can return various types, including text, JSON-serializable objects, and even images using the [`fastmcp.Image`](https://gofastmcp.com/servers/tools#return-values) helper.
145
 
146
  ```python
147
- @mcp.tool()
148
  def multiply(a: float, b: float) -> float:
149
  """Multiplies two numbers."""
150
  return a * b
@@ -201,7 +201,7 @@ from fastmcp import FastMCP, Context
201
 
202
  mcp = FastMCP("My MCP Server")
203
 
204
- @mcp.tool()
205
  async def process_data(uri: str, ctx: Context):
206
  # Log a message to the client
207
  await ctx.info(f"Processing {uri}...")
@@ -321,7 +321,7 @@ from fastmcp import FastMCP
321
 
322
  mcp = FastMCP("Demo 🚀")
323
 
324
- @mcp.tool()
325
  def hello(name: str) -> str:
326
  return f"Hello, {name}!"
327
 
 
31
 
32
  mcp = FastMCP("Demo 🚀")
33
 
34
+ @mcp.tool
35
  def add(a: int, b: int) -> int:
36
  """Add two numbers"""
37
  return a + b
 
144
  Tools allow LLMs to perform actions by executing your Python functions (sync or async). Ideal for computations, API calls, or side effects (like `POST`/`PUT`). FastMCP handles schema generation from type hints and docstrings. Tools can return various types, including text, JSON-serializable objects, and even images using the [`fastmcp.Image`](https://gofastmcp.com/servers/tools#return-values) helper.
145
 
146
  ```python
147
+ @mcp.tool
148
  def multiply(a: float, b: float) -> float:
149
  """Multiplies two numbers."""
150
  return a * b
 
201
 
202
  mcp = FastMCP("My MCP Server")
203
 
204
+ @mcp.tool
205
  async def process_data(uri: str, ctx: Context):
206
  # Log a message to the client
207
  await ctx.info(f"Processing {uri}...")
 
321
 
322
  mcp = FastMCP("Demo 🚀")
323
 
324
+ @mcp.tool
325
  def hello(name: str) -> str:
326
  return f"Hello, {name}!"
327
 
docs/clients/transports.mdx CHANGED
@@ -359,7 +359,7 @@ import asyncio
359
  # 1. Create your FastMCP server instance
360
  server = FastMCP(name="InMemoryServer")
361
 
362
- @server.tool()
363
  def ping():
364
  return "pong"
365
 
 
359
  # 1. Create your FastMCP server instance
360
  server = FastMCP(name="InMemoryServer")
361
 
362
+ @server.tool
363
  def ping():
364
  return "pong"
365
 
docs/deployment/asgi.mdx CHANGED
@@ -32,7 +32,7 @@ from fastmcp import FastMCP
32
 
33
  mcp = FastMCP("MyServer")
34
 
35
- @mcp.tool()
36
  def hello(name: str) -> str:
37
  return f"Hello, {name}!"
38
 
 
32
 
33
  mcp = FastMCP("MyServer")
34
 
35
+ @mcp.tool
36
  def hello(name: str) -> str:
37
  return f"Hello, {name}!"
38
 
docs/deployment/running-server.mdx CHANGED
@@ -22,7 +22,7 @@ from fastmcp import FastMCP
22
 
23
  mcp = FastMCP(name="MyServer")
24
 
25
- @mcp.tool()
26
  def hello(name: str) -> str:
27
  return f"Hello, {name}!"
28
 
@@ -244,7 +244,7 @@ import asyncio
244
 
245
  mcp = FastMCP(name="MyServer")
246
 
247
- @mcp.tool()
248
  def hello(name: str) -> str:
249
  return f"Hello, {name}!"
250
 
 
22
 
23
  mcp = FastMCP(name="MyServer")
24
 
25
+ @mcp.tool
26
  def hello(name: str) -> str:
27
  return f"Hello, {name}!"
28
 
 
244
 
245
  mcp = FastMCP(name="MyServer")
246
 
247
+ @mcp.tool
248
  def hello(name: str) -> str:
249
  return f"Hello, {name}!"
250
 
docs/getting-started/quickstart.mdx CHANGED
@@ -32,7 +32,7 @@ from fastmcp import FastMCP
32
 
33
  mcp = FastMCP("My MCP Server")
34
 
35
- @mcp.tool()
36
  def greet(name: str) -> str:
37
  return f"Hello, {name}!"
38
  ```
@@ -49,7 +49,7 @@ from fastmcp import FastMCP, Client
49
 
50
  mcp = FastMCP("My MCP Server")
51
 
52
- @mcp.tool()
53
  def greet(name: str) -> str:
54
  return f"Hello, {name}!"
55
 
@@ -76,7 +76,7 @@ from fastmcp import FastMCP
76
 
77
  mcp = FastMCP("My MCP Server")
78
 
79
- @mcp.tool()
80
  def greet(name: str) -> str:
81
  return f"Hello, {name}!"
82
 
 
32
 
33
  mcp = FastMCP("My MCP Server")
34
 
35
+ @mcp.tool
36
  def greet(name: str) -> str:
37
  return f"Hello, {name}!"
38
  ```
 
49
 
50
  mcp = FastMCP("My MCP Server")
51
 
52
+ @mcp.tool
53
  def greet(name: str) -> str:
54
  return f"Hello, {name}!"
55
 
 
76
 
77
  mcp = FastMCP("My MCP Server")
78
 
79
+ @mcp.tool
80
  def greet(name: str) -> str:
81
  return f"Hello, {name}!"
82
 
docs/getting-started/welcome.mdx CHANGED
@@ -14,7 +14,7 @@ from fastmcp import FastMCP
14
 
15
  mcp = FastMCP("Demo 🚀")
16
 
17
- @mcp.tool()
18
  def add(a: int, b: int) -> int:
19
  """Add two numbers"""
20
  return a + b
 
14
 
15
  mcp = FastMCP("Demo 🚀")
16
 
17
+ @mcp.tool
18
  def add(a: int, b: int) -> int:
19
  """Add two numbers"""
20
  return a + b
docs/integrations/anthropic.mdx CHANGED
@@ -27,7 +27,7 @@ from fastmcp import FastMCP
27
 
28
  mcp = FastMCP(name="Dice Roller")
29
 
30
- @mcp.tool()
31
  def roll_dice(n_dice: int) -> list[int]:
32
  """Roll `n_dice` 6-sided dice and return the results."""
33
  return [random.randint(1, 6) for _ in range(n_dice)]
@@ -170,7 +170,7 @@ auth = BearerAuthProvider(
170
 
171
  mcp = FastMCP(name="Dice Roller", auth=auth)
172
 
173
- @mcp.tool()
174
  def roll_dice(n_dice: int) -> list[int]:
175
  """Roll `n_dice` 6-sided dice and return the results."""
176
  return [random.randint(1, 6) for _ in range(n_dice)]
 
27
 
28
  mcp = FastMCP(name="Dice Roller")
29
 
30
+ @mcp.tool
31
  def roll_dice(n_dice: int) -> list[int]:
32
  """Roll `n_dice` 6-sided dice and return the results."""
33
  return [random.randint(1, 6) for _ in range(n_dice)]
 
170
 
171
  mcp = FastMCP(name="Dice Roller", auth=auth)
172
 
173
+ @mcp.tool
174
  def roll_dice(n_dice: int) -> list[int]:
175
  """Roll `n_dice` 6-sided dice and return the results."""
176
  return [random.randint(1, 6) for _ in range(n_dice)]
docs/integrations/claude-desktop.mdx CHANGED
@@ -31,7 +31,7 @@ from fastmcp import FastMCP
31
 
32
  mcp = FastMCP(name="Dice Roller")
33
 
34
- @mcp.tool()
35
  def roll_dice(n_dice: int) -> list[int]:
36
  """Roll `n_dice` 6-sided dice and return the results."""
37
  return [random.randint(1, 6) for _ in range(n_dice)]
 
31
 
32
  mcp = FastMCP(name="Dice Roller")
33
 
34
+ @mcp.tool
35
  def roll_dice(n_dice: int) -> list[int]:
36
  """Roll `n_dice` 6-sided dice and return the results."""
37
  return [random.randint(1, 6) for _ in range(n_dice)]
docs/integrations/gemini.mdx CHANGED
@@ -31,7 +31,7 @@ from fastmcp import FastMCP
31
 
32
  mcp = FastMCP(name="Dice Roller")
33
 
34
- @mcp.tool()
35
  def roll_dice(n_dice: int) -> list[int]:
36
  """Roll `n_dice` 6-sided dice and return the results."""
37
  return [random.randint(1, 6) for _ in range(n_dice)]
 
31
 
32
  mcp = FastMCP(name="Dice Roller")
33
 
34
+ @mcp.tool
35
  def roll_dice(n_dice: int) -> list[int]:
36
  """Roll `n_dice` 6-sided dice and return the results."""
37
  return [random.randint(1, 6) for _ in range(n_dice)]
docs/integrations/openai.mdx CHANGED
@@ -32,7 +32,7 @@ from fastmcp import FastMCP
32
 
33
  mcp = FastMCP(name="Dice Roller")
34
 
35
- @mcp.tool()
36
  def roll_dice(n_dice: int) -> list[int]:
37
  """Roll `n_dice` 6-sided dice and return the results."""
38
  return [random.randint(1, 6) for _ in range(n_dice)]
@@ -165,7 +165,7 @@ auth = BearerAuthProvider(
165
 
166
  mcp = FastMCP(name="Dice Roller", auth=auth)
167
 
168
- @mcp.tool()
169
  def roll_dice(n_dice: int) -> list[int]:
170
  """Roll `n_dice` 6-sided dice and return the results."""
171
  return [random.randint(1, 6) for _ in range(n_dice)]
 
32
 
33
  mcp = FastMCP(name="Dice Roller")
34
 
35
+ @mcp.tool
36
  def roll_dice(n_dice: int) -> list[int]:
37
  """Roll `n_dice` 6-sided dice and return the results."""
38
  return [random.randint(1, 6) for _ in range(n_dice)]
 
165
 
166
  mcp = FastMCP(name="Dice Roller", auth=auth)
167
 
168
+ @mcp.tool
169
  def roll_dice(n_dice: int) -> list[int]:
170
  """Roll `n_dice` 6-sided dice and return the results."""
171
  return [random.randint(1, 6) for _ in range(n_dice)]
docs/patterns/cli.mdx CHANGED
@@ -66,7 +66,7 @@ from fastmcp import FastMCP
66
 
67
  mcp = FastMCP("MyServer")
68
 
69
- @mcp.tool()
70
  def hello(name: str) -> str:
71
  return f"Hello, {name}!"
72
 
 
66
 
67
  mcp = FastMCP("MyServer")
68
 
69
+ @mcp.tool
70
  def hello(name: str) -> str:
71
  return f"Hello, {name}!"
72
 
docs/patterns/decorating-methods.mdx CHANGED
@@ -5,11 +5,11 @@ description: Properly use instance methods, class methods, and static methods wi
5
  icon: at
6
  ---
7
 
8
- FastMCP's decorator system is designed to work with functions, but you may see unexpected behavior if you try to decorate an instance or class method. This guide explains the correct approach for using methods with all FastMCP decorators (`@tool()`, `@resource()`, and `@prompt()`).
9
 
10
  ## Why Are Methods Hard?
11
 
12
- When you apply a FastMCP decorator like `@tool()`, `@resource()`, or `@prompt()` to a method, the decorator captures the function at decoration time. For instance methods and class methods, this poses a challenge because:
13
 
14
  1. For instance methods: The decorator gets the unbound method before any instance exists
15
  2. For class methods: The decorator gets the function before it's bound to the class
@@ -28,7 +28,7 @@ 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
 
@@ -83,7 +83,7 @@ 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
  ```
@@ -122,7 +122,7 @@ mcp = FastMCP()
122
 
123
  class MyClass:
124
  @staticmethod
125
- @mcp.tool() # This works!
126
  def utility(x, y):
127
  return x + y
128
 
@@ -194,7 +194,7 @@ The class automatically registers its methods during initialization, ensuring th
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
 
 
5
  icon: at
6
  ---
7
 
8
+ FastMCP's decorator system is designed to work with functions, but you may see unexpected behavior if you try to decorate an instance or class method. This guide explains the correct approach for using methods with all FastMCP decorators (`@tool`, `@resource()`, and `@prompt()`).
9
 
10
  ## Why Are Methods Hard?
11
 
12
+ When you apply a FastMCP decorator like `@tool`, `@resource()`, or `@prompt()` to a method, the decorator captures the function at decoration time. For instance methods and class methods, this poses a challenge because:
13
 
14
  1. For instance methods: The decorator gets the unbound method before any instance exists
15
  2. For class methods: The decorator gets the function before it's bound to the class
 
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
 
 
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
  ```
 
122
 
123
  class MyClass:
124
  @staticmethod
125
+ @mcp.tool # This works!
126
  def utility(x, y):
127
  return x + y
128
 
 
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
 
docs/patterns/http-requests.mdx CHANGED
@@ -25,7 +25,7 @@ from starlette.requests import Request
25
 
26
  mcp = FastMCP(name="HTTP Request Demo")
27
 
28
- @mcp.tool()
29
  async def user_agent_info() -> dict:
30
  """Return information about the user agent."""
31
  # Get the HTTP request
@@ -58,7 +58,7 @@ from fastmcp.server.dependencies import get_http_headers
58
 
59
  mcp = FastMCP(name="Headers Demo")
60
 
61
- @mcp.tool()
62
  async def safe_header_info() -> dict:
63
  """Safely get header information without raising errors."""
64
  # Get headers (returns empty dict if no request context)
 
25
 
26
  mcp = FastMCP(name="HTTP Request Demo")
27
 
28
+ @mcp.tool
29
  async def user_agent_info() -> dict:
30
  """Return information about the user agent."""
31
  # Get the HTTP request
 
58
 
59
  mcp = FastMCP(name="Headers Demo")
60
 
61
+ @mcp.tool
62
  async def safe_header_info() -> dict:
63
  """Safely get header information without raising errors."""
64
  # Get headers (returns empty dict if no request context)
docs/patterns/testing.mdx CHANGED
@@ -22,7 +22,7 @@ from fastmcp import FastMCP, Client
22
  def mcp_server():
23
  server = FastMCP("TestServer")
24
 
25
- @server.tool()
26
  def greet(name: str) -> str:
27
  return f"Hello, {name}!"
28
 
 
22
  def mcp_server():
23
  server = FastMCP("TestServer")
24
 
25
+ @server.tool
26
  def greet(name: str) -> str:
27
  return f"Hello, {name}!"
28
 
docs/servers/auth/bearer.mdx CHANGED
@@ -159,7 +159,7 @@ Once authenticated, your tools, resources, or prompts can access token informati
159
  from fastmcp import FastMCP, Context, ToolError
160
  from fastmcp.server.dependencies import get_access_token, AccessToken
161
 
162
- @mcp.tool()
163
  async def get_my_data(ctx: Context) -> dict:
164
  access_token: AccessToken = get_access_token()
165
 
 
159
  from fastmcp import FastMCP, Context, ToolError
160
  from fastmcp.server.dependencies import get_access_token, AccessToken
161
 
162
+ @mcp.tool
163
  async def get_my_data(ctx: Context) -> dict:
164
  access_token: AccessToken = get_access_token()
165
 
docs/servers/composition.mdx CHANGED
@@ -50,7 +50,7 @@ import asyncio
50
  # Define subservers
51
  weather_mcp = FastMCP(name="WeatherService")
52
 
53
- @weather_mcp.tool()
54
  def get_forecast(city: str) -> dict:
55
  """Get weather forecast."""
56
  return {"city": city, "forecast": "Sunny"}
@@ -102,7 +102,7 @@ from fastmcp import FastMCP, Client
102
  # Define subserver
103
  dynamic_mcp = FastMCP(name="DynamicService")
104
 
105
- @dynamic_mcp.tool()
106
  def initial_tool():
107
  """Initial tool demonstration."""
108
  return "Initial Tool Exists"
@@ -112,7 +112,7 @@ main_mcp = FastMCP(name="MainAppLive")
112
  main_mcp.mount("dynamic", dynamic_mcp)
113
 
114
  # Add a tool AFTER mounting - it will be accessible through main_mcp
115
- @dynamic_mcp.tool()
116
  def added_later():
117
  """Tool added after mounting."""
118
  return "Tool Added Dynamically!"
 
50
  # Define subservers
51
  weather_mcp = FastMCP(name="WeatherService")
52
 
53
+ @weather_mcp.tool
54
  def get_forecast(city: str) -> dict:
55
  """Get weather forecast."""
56
  return {"city": city, "forecast": "Sunny"}
 
102
  # Define subserver
103
  dynamic_mcp = FastMCP(name="DynamicService")
104
 
105
+ @dynamic_mcp.tool
106
  def initial_tool():
107
  """Initial tool demonstration."""
108
  return "Initial Tool Exists"
 
112
  main_mcp.mount("dynamic", dynamic_mcp)
113
 
114
  # Add a tool AFTER mounting - it will be accessible through main_mcp
115
+ @dynamic_mcp.tool
116
  def added_later():
117
  """Tool added after mounting."""
118
  return "Tool Added Dynamically!"
docs/servers/context.mdx CHANGED
@@ -41,7 +41,7 @@ from fastmcp import FastMCP, Context
41
 
42
  mcp = FastMCP(name="ContextDemo")
43
 
44
- @mcp.tool()
45
  async def process_file(file_uri: str, ctx: Context) -> str:
46
  """Processes a file, using context for logging and resource access."""
47
  # Context is available as the ctx parameter
@@ -99,7 +99,7 @@ async def process_data(data: list[float]) -> dict:
99
  ctx = get_context()
100
  await ctx.info(f"Processing {len(data)} data points")
101
 
102
- @mcp.tool()
103
  async def analyze_dataset(dataset_name: str) -> dict:
104
  # Call utility function that uses context internally
105
  data = load_data(dataset_name)
@@ -118,7 +118,7 @@ async def analyze_dataset(dataset_name: str) -> dict:
118
  Send log messages back to the MCP client. This is useful for debugging and providing visibility into function execution during a request.
119
 
120
  ```python
121
- @mcp.tool()
122
  async def analyze_data(data: list[float], ctx: Context) -> dict:
123
  """Analyze numerical data with logging."""
124
  await ctx.debug("Starting analysis of numerical data")
@@ -149,7 +149,7 @@ async def analyze_data(data: list[float], ctx: Context) -> dict:
149
  For long-running operations, notify the client about the progress. This allows clients to display progress indicators and provide a better user experience.
150
 
151
  ```python
152
- @mcp.tool()
153
  async def process_items(items: list[str], ctx: Context) -> dict:
154
  """Process a list of items with progress updates."""
155
  total = len(items)
@@ -182,7 +182,7 @@ Progress reporting requires the client to have sent a `progressToken` in the ini
182
  Read data from resources registered with your FastMCP server. This allows functions to access files, configuration, or dynamically generated content.
183
 
184
  ```python
185
- @mcp.tool()
186
  async def summarize_document(document_uri: str, ctx: Context) -> str:
187
  """Summarize a document by its resource URI."""
188
  # Read the document content
@@ -222,7 +222,7 @@ The returned content is typically accessed via `content_list[0].content` and can
222
  Request the client's LLM to generate text based on provided messages. This is useful when your function needs to leverage the LLM's capabilities to process data or generate responses.
223
 
224
  ```python
225
- @mcp.tool()
226
  async def analyze_sentiment(text: str, ctx: Context) -> dict:
227
  """Analyze the sentiment of a text using the client's LLM."""
228
  # Create a sampling prompt asking for sentiment analysis
@@ -258,7 +258,7 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
258
  When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles.
259
 
260
  ```python
261
- @mcp.tool()
262
  async def generate_example(concept: str, ctx: Context) -> str:
263
  """Generate a Python code example for a given concept."""
264
  # Using a system prompt and a user message
@@ -280,7 +280,7 @@ See [Client Sampling](/clients/client#llm-sampling) for more details on how clie
280
  Access metadata about the current request and client.
281
 
282
  ```python
283
- @mcp.tool()
284
  async def request_info(ctx: Context) -> dict:
285
  """Return information about the current request."""
286
  return {
@@ -300,7 +300,7 @@ async def request_info(ctx: Context) -> dict:
300
  #### FastMCP Server and Sessions
301
 
302
  ```python
303
- @mcp.tool()
304
  async def advanced_tool(ctx: Context) -> str:
305
  """Demonstrate advanced context access."""
306
  # Access the FastMCP server instance
@@ -326,7 +326,7 @@ See the [HTTP Requests pattern](/patterns/http-requests) for more details.
326
  For web applications, you can access the underlying HTTP request:
327
 
328
  ```python
329
- @mcp.tool()
330
  async def handle_web_request(ctx: Context) -> dict:
331
  """Access HTTP request information from the Starlette request."""
332
  request = ctx.get_http_request()
 
41
 
42
  mcp = FastMCP(name="ContextDemo")
43
 
44
+ @mcp.tool
45
  async def process_file(file_uri: str, ctx: Context) -> str:
46
  """Processes a file, using context for logging and resource access."""
47
  # Context is available as the ctx parameter
 
99
  ctx = get_context()
100
  await ctx.info(f"Processing {len(data)} data points")
101
 
102
+ @mcp.tool
103
  async def analyze_dataset(dataset_name: str) -> dict:
104
  # Call utility function that uses context internally
105
  data = load_data(dataset_name)
 
118
  Send log messages back to the MCP client. This is useful for debugging and providing visibility into function execution during a request.
119
 
120
  ```python
121
+ @mcp.tool
122
  async def analyze_data(data: list[float], ctx: Context) -> dict:
123
  """Analyze numerical data with logging."""
124
  await ctx.debug("Starting analysis of numerical data")
 
149
  For long-running operations, notify the client about the progress. This allows clients to display progress indicators and provide a better user experience.
150
 
151
  ```python
152
+ @mcp.tool
153
  async def process_items(items: list[str], ctx: Context) -> dict:
154
  """Process a list of items with progress updates."""
155
  total = len(items)
 
182
  Read data from resources registered with your FastMCP server. This allows functions to access files, configuration, or dynamically generated content.
183
 
184
  ```python
185
+ @mcp.tool
186
  async def summarize_document(document_uri: str, ctx: Context) -> str:
187
  """Summarize a document by its resource URI."""
188
  # Read the document content
 
222
  Request the client's LLM to generate text based on provided messages. This is useful when your function needs to leverage the LLM's capabilities to process data or generate responses.
223
 
224
  ```python
225
+ @mcp.tool
226
  async def analyze_sentiment(text: str, ctx: Context) -> dict:
227
  """Analyze the sentiment of a text using the client's LLM."""
228
  # Create a sampling prompt asking for sentiment analysis
 
258
  When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles.
259
 
260
  ```python
261
+ @mcp.tool
262
  async def generate_example(concept: str, ctx: Context) -> str:
263
  """Generate a Python code example for a given concept."""
264
  # Using a system prompt and a user message
 
280
  Access metadata about the current request and client.
281
 
282
  ```python
283
+ @mcp.tool
284
  async def request_info(ctx: Context) -> dict:
285
  """Return information about the current request."""
286
  return {
 
300
  #### FastMCP Server and Sessions
301
 
302
  ```python
303
+ @mcp.tool
304
  async def advanced_tool(ctx: Context) -> str:
305
  """Demonstrate advanced context access."""
306
  # Access the FastMCP server instance
 
326
  For web applications, you can access the underlying HTTP request:
327
 
328
  ```python
329
+ @mcp.tool
330
  async def handle_web_request(ctx: Context) -> dict:
331
  """Access HTTP request information from the Starlette request."""
332
  request = ctx.get_http_request()
docs/servers/fastmcp.mdx CHANGED
@@ -47,7 +47,7 @@ FastMCP servers expose several types of components to the client:
47
  Tools are functions that the client can call to perform actions or access external systems.
48
 
49
  ```python
50
- @mcp.tool()
51
  def multiply(a: float, b: float) -> float:
52
  """Multiplies two numbers together."""
53
  return a * b
@@ -106,7 +106,7 @@ from fastmcp import FastMCP
106
 
107
  mcp = FastMCP(name="MyServer")
108
 
109
- @mcp.tool()
110
  def greet(name: str) -> str:
111
  """Greet a user by name."""
112
  return f"Hello, {name}!"
@@ -145,7 +145,7 @@ import asyncio
145
  main = FastMCP(name="Main")
146
  sub = FastMCP(name="Sub")
147
 
148
- @sub.tool()
149
  def hello():
150
  return "hi"
151
 
@@ -216,7 +216,7 @@ def yaml_serializer(data):
216
  # Create a server with the custom serializer
217
  mcp = FastMCP(name="MyServer", tool_serializer=yaml_serializer)
218
 
219
- @mcp.tool()
220
  def get_config():
221
  """Returns configuration in YAML format."""
222
  return {"api_key": "abc123", "debug": True, "rate_limit": 100}
 
47
  Tools are functions that the client can call to perform actions or access external systems.
48
 
49
  ```python
50
+ @mcp.tool
51
  def multiply(a: float, b: float) -> float:
52
  """Multiplies two numbers together."""
53
  return a * b
 
106
 
107
  mcp = FastMCP(name="MyServer")
108
 
109
+ @mcp.tool
110
  def greet(name: str) -> str:
111
  """Greet a user by name."""
112
  return f"Hello, {name}!"
 
145
  main = FastMCP(name="Main")
146
  sub = FastMCP(name="Sub")
147
 
148
+ @sub.tool
149
  def hello():
150
  return "hi"
151
 
 
216
  # Create a server with the custom serializer
217
  mcp = FastMCP(name="MyServer", tool_serializer=yaml_serializer)
218
 
219
+ @mcp.tool
220
  def get_config():
221
  """Returns configuration in YAML format."""
222
  return {"api_key": "abc123", "debug": True, "rate_limit": 100}
docs/servers/proxy.mdx CHANGED
@@ -90,7 +90,7 @@ from fastmcp import FastMCP
90
  # Original server
91
  original_server = FastMCP(name="Original")
92
 
93
- @original_server.tool()
94
  def tool_a() -> str:
95
  return "A"
96
 
 
90
  # Original server
91
  original_server = FastMCP(name="Original")
92
 
93
+ @original_server.tool
94
  def tool_a() -> str:
95
  return "A"
96
 
docs/servers/tools.mdx CHANGED
@@ -24,14 +24,14 @@ This allows LLMs to perform tasks like querying databases, calling APIs, making
24
 
25
  ### The `@tool` Decorator
26
 
27
- Creating a tool is as simple as decorating a Python function with `@mcp.tool()`:
28
 
29
  ```python
30
  from fastmcp import FastMCP
31
 
32
  mcp = FastMCP(name="CalculatorServer")
33
 
34
- @mcp.tool()
35
  def add(a: int, b: int) -> int:
36
  """Adds two integer numbers together."""
37
  return a + b
@@ -61,7 +61,7 @@ Type annotations for parameters are essential for proper tool functionality. The
61
  Use standard Python type annotations for parameters:
62
 
63
  ```python
64
- @mcp.tool()
65
  def analyze_text(
66
  text: str,
67
  max_tokens: int = 100,
@@ -79,7 +79,7 @@ You can provide additional metadata about parameters using Pydantic's `Field` cl
79
  from typing import Annotated
80
  from pydantic import Field
81
 
82
- @mcp.tool()
83
  def process_image(
84
  image_url: Annotated[str, Field(description="URL of the image to process")],
85
  resize: Annotated[bool, Field(description="Whether to resize the image")] = False,
@@ -97,7 +97,7 @@ def process_image(
97
  You can also use the Field as a default value, though the Annotated approach is preferred:
98
 
99
  ```python
100
- @mcp.tool()
101
  def search_database(
102
  query: str = Field(description="Search query string"),
103
  limit: int = Field(10, description="Maximum number of results", ge=1, le=100)
@@ -137,7 +137,7 @@ For additional type annotations not listed here, see the [Parameter Types](#para
137
  FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.
138
 
139
  ```python
140
- @mcp.tool()
141
  def search_products(
142
  query: str, # Required - no default value
143
  max_results: int = 10, # Optional - has default value
@@ -197,14 +197,14 @@ FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`)
197
 
198
  ```python
199
  # Synchronous tool (suitable for CPU-bound or quick tasks)
200
- @mcp.tool()
201
  def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
202
  """Calculate the distance between two coordinates."""
203
  # Implementation...
204
  return 42.5
205
 
206
  # Asynchronous tool (ideal for I/O-bound operations)
207
- @mcp.tool()
208
  async def fetch_weather(city: str) -> dict:
209
  """Retrieve current weather conditions for a city."""
210
  # Use 'async def' for operations involving network calls, file I/O, etc.
@@ -244,7 +244,7 @@ except ImportError:
244
 
245
  mcp = FastMCP("Image Demo")
246
 
247
- @mcp.tool()
248
  def generate_image(width: int, height: int, color: str) -> Image:
249
  """Generates a solid color image."""
250
  # Create image using Pillow
@@ -258,7 +258,7 @@ def generate_image(width: int, height: int, color: str) -> Image:
258
  # Return using FastMCP's Image helper
259
  return Image(data=img_bytes, format="png")
260
 
261
- @mcp.tool()
262
  def do_nothing() -> None:
263
  """This tool performs an action but returns no data."""
264
  print("Performing a side effect...")
@@ -285,7 +285,7 @@ mcp = FastMCP(name="SecureServer", mask_error_details=True)
285
  from fastmcp import FastMCP
286
  from fastmcp.exceptions import ToolError
287
 
288
- @mcp.tool()
289
  def divide(a: float, b: float) -> float:
290
  """Divide a by b."""
291
 
@@ -315,7 +315,7 @@ Annotations serve several purposes in client applications:
315
  - Describing the safety profile of tools (destructive vs. non-destructive)
316
  - Signaling if tools interact with external systems
317
 
318
- You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool()` decorator:
319
 
320
  ```python
321
  @mcp.tool(
@@ -351,7 +351,7 @@ from fastmcp import FastMCP, Context
351
 
352
  mcp = FastMCP(name="ContextDemo")
353
 
354
- @mcp.tool()
355
  async def process_data(data_uri: str, ctx: Context) -> dict:
356
  """Process data from a resource with progress reporting."""
357
  await ctx.info(f"Processing data from {data_uri}")
@@ -396,7 +396,7 @@ FastMCP supports **type coercion** when possible. This means that if a client se
396
  The most common parameter types are Python's built-in scalar types:
397
 
398
  ```python
399
- @mcp.tool()
400
  def process_values(
401
  name: str, # Text data
402
  count: int, # Integer numbers
@@ -416,7 +416,7 @@ FastMCP supports various date and time types from the `datetime` module:
416
  ```python
417
  from datetime import datetime, date, timedelta
418
 
419
- @mcp.tool()
420
  def process_date_time(
421
  event_date: date, # ISO format date string or date object
422
  event_time: datetime, # ISO format datetime string or datetime object
@@ -440,7 +440,7 @@ def process_date_time(
440
  FastMCP supports all standard Python collection types:
441
 
442
  ```python
443
- @mcp.tool()
444
  def analyze_data(
445
  values: list[float], # List of numbers
446
  properties: dict[str, str], # Dictionary with string keys and values
@@ -465,7 +465,7 @@ Collection types can be nested and combined to represent complex data structures
465
  For parameters that can accept multiple types or may be omitted:
466
 
467
  ```python
468
- @mcp.tool()
469
  def flexible_search(
470
  query: str | int, # Can be either string or integer
471
  filters: dict[str, str] | None = None, # Optional dictionary
@@ -488,7 +488,7 @@ Literals constrain parameters to a specific set of values:
488
  ```python
489
  from typing import Literal
490
 
491
- @mcp.tool()
492
  def sort_data(
493
  data: list[float],
494
  order: Literal["ascending", "descending"] = "ascending",
@@ -516,7 +516,7 @@ class Color(Enum):
516
  GREEN = "green"
517
  BLUE = "blue"
518
 
519
- @mcp.tool()
520
  def process_image(
521
  image_path: str,
522
  color_filter: Color = Color.RED
@@ -539,7 +539,7 @@ There are two approaches to handling binary data in tool parameters:
539
  #### Bytes
540
 
541
  ```python
542
- @mcp.tool()
543
  def process_binary(data: bytes):
544
  """Process binary data directly.
545
 
@@ -563,7 +563,7 @@ FastMCP does not automatically decode base64-encoded strings for bytes parameter
563
  from typing import Annotated
564
  from pydantic import Field
565
 
566
- @mcp.tool()
567
  def process_image_data(
568
  image_data: Annotated[str, Field(description="Base64-encoded image data")]
569
  ):
@@ -587,7 +587,7 @@ The `Path` type from the `pathlib` module can be used for file system paths:
587
  ```python
588
  from pathlib import Path
589
 
590
- @mcp.tool()
591
  def process_file(path: Path) -> str:
592
  """Process a file at the given path."""
593
  assert isinstance(path, Path) # Path is properly converted
@@ -603,7 +603,7 @@ The `UUID` type from the `uuid` module can be used for unique identifiers:
603
  ```python
604
  import uuid
605
 
606
- @mcp.tool()
607
  def process_item(
608
  item_id: uuid.UUID # String UUID or UUID object
609
  ) -> str:
@@ -628,7 +628,7 @@ class User(BaseModel):
628
  age: int | None = None
629
  is_active: bool = True
630
 
631
- @mcp.tool()
632
  def create_user(user: User):
633
  """Create a new user in the system."""
634
  # The input is automatically validated against the User model
@@ -657,7 +657,7 @@ Note that fields can be used *outside* Pydantic models to provide metadata and v
657
  from typing import Annotated
658
  from pydantic import Field
659
 
660
- @mcp.tool()
661
  def analyze_metrics(
662
  # Numbers with range constraints
663
  count: Annotated[int, Field(ge=0, le=100)], # 0 <= count <= 100
@@ -682,7 +682,7 @@ def analyze_metrics(
682
  You can also use `Field` as a default value, though the `Annotated` approach is preferred:
683
 
684
  ```python
685
- @mcp.tool()
686
  def validate_data(
687
  # Value constraints
688
  age: int = Field(ge=0, lt=120), # 0 <= age < 120
@@ -727,12 +727,12 @@ mcp = FastMCP(
727
  on_duplicate_tools="error"
728
  )
729
 
730
- @mcp.tool()
731
  def my_tool(): return "Version 1"
732
 
733
  # This will now raise a ValueError because 'my_tool' already exists
734
  # and on_duplicate_tools is set to "error".
735
- # @mcp.tool()
736
  # def my_tool(): return "Version 2"
737
  ```
738
 
@@ -754,7 +754,7 @@ from fastmcp import FastMCP
754
 
755
  mcp = FastMCP(name="DynamicToolServer")
756
 
757
- @mcp.tool()
758
  def calculate_sum(a: int, b: int) -> int:
759
  """Add two numbers together."""
760
  return a + b
 
24
 
25
  ### The `@tool` Decorator
26
 
27
+ Creating a tool is as simple as decorating a Python function with `@mcp.tool`:
28
 
29
  ```python
30
  from fastmcp import FastMCP
31
 
32
  mcp = FastMCP(name="CalculatorServer")
33
 
34
+ @mcp.tool
35
  def add(a: int, b: int) -> int:
36
  """Adds two integer numbers together."""
37
  return a + b
 
61
  Use standard Python type annotations for parameters:
62
 
63
  ```python
64
+ @mcp.tool
65
  def analyze_text(
66
  text: str,
67
  max_tokens: int = 100,
 
79
  from typing import Annotated
80
  from pydantic import Field
81
 
82
+ @mcp.tool
83
  def process_image(
84
  image_url: Annotated[str, Field(description="URL of the image to process")],
85
  resize: Annotated[bool, Field(description="Whether to resize the image")] = False,
 
97
  You can also use the Field as a default value, though the Annotated approach is preferred:
98
 
99
  ```python
100
+ @mcp.tool
101
  def search_database(
102
  query: str = Field(description="Search query string"),
103
  limit: int = Field(10, description="Maximum number of results", ge=1, le=100)
 
137
  FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.
138
 
139
  ```python
140
+ @mcp.tool
141
  def search_products(
142
  query: str, # Required - no default value
143
  max_results: int = 10, # Optional - has default value
 
197
 
198
  ```python
199
  # Synchronous tool (suitable for CPU-bound or quick tasks)
200
+ @mcp.tool
201
  def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
202
  """Calculate the distance between two coordinates."""
203
  # Implementation...
204
  return 42.5
205
 
206
  # Asynchronous tool (ideal for I/O-bound operations)
207
+ @mcp.tool
208
  async def fetch_weather(city: str) -> dict:
209
  """Retrieve current weather conditions for a city."""
210
  # Use 'async def' for operations involving network calls, file I/O, etc.
 
244
 
245
  mcp = FastMCP("Image Demo")
246
 
247
+ @mcp.tool
248
  def generate_image(width: int, height: int, color: str) -> Image:
249
  """Generates a solid color image."""
250
  # Create image using Pillow
 
258
  # Return using FastMCP's Image helper
259
  return Image(data=img_bytes, format="png")
260
 
261
+ @mcp.tool
262
  def do_nothing() -> None:
263
  """This tool performs an action but returns no data."""
264
  print("Performing a side effect...")
 
285
  from fastmcp import FastMCP
286
  from fastmcp.exceptions import ToolError
287
 
288
+ @mcp.tool
289
  def divide(a: float, b: float) -> float:
290
  """Divide a by b."""
291
 
 
315
  - Describing the safety profile of tools (destructive vs. non-destructive)
316
  - Signaling if tools interact with external systems
317
 
318
+ You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator:
319
 
320
  ```python
321
  @mcp.tool(
 
351
 
352
  mcp = FastMCP(name="ContextDemo")
353
 
354
+ @mcp.tool
355
  async def process_data(data_uri: str, ctx: Context) -> dict:
356
  """Process data from a resource with progress reporting."""
357
  await ctx.info(f"Processing data from {data_uri}")
 
396
  The most common parameter types are Python's built-in scalar types:
397
 
398
  ```python
399
+ @mcp.tool
400
  def process_values(
401
  name: str, # Text data
402
  count: int, # Integer numbers
 
416
  ```python
417
  from datetime import datetime, date, timedelta
418
 
419
+ @mcp.tool
420
  def process_date_time(
421
  event_date: date, # ISO format date string or date object
422
  event_time: datetime, # ISO format datetime string or datetime object
 
440
  FastMCP supports all standard Python collection types:
441
 
442
  ```python
443
+ @mcp.tool
444
  def analyze_data(
445
  values: list[float], # List of numbers
446
  properties: dict[str, str], # Dictionary with string keys and values
 
465
  For parameters that can accept multiple types or may be omitted:
466
 
467
  ```python
468
+ @mcp.tool
469
  def flexible_search(
470
  query: str | int, # Can be either string or integer
471
  filters: dict[str, str] | None = None, # Optional dictionary
 
488
  ```python
489
  from typing import Literal
490
 
491
+ @mcp.tool
492
  def sort_data(
493
  data: list[float],
494
  order: Literal["ascending", "descending"] = "ascending",
 
516
  GREEN = "green"
517
  BLUE = "blue"
518
 
519
+ @mcp.tool
520
  def process_image(
521
  image_path: str,
522
  color_filter: Color = Color.RED
 
539
  #### Bytes
540
 
541
  ```python
542
+ @mcp.tool
543
  def process_binary(data: bytes):
544
  """Process binary data directly.
545
 
 
563
  from typing import Annotated
564
  from pydantic import Field
565
 
566
+ @mcp.tool
567
  def process_image_data(
568
  image_data: Annotated[str, Field(description="Base64-encoded image data")]
569
  ):
 
587
  ```python
588
  from pathlib import Path
589
 
590
+ @mcp.tool
591
  def process_file(path: Path) -> str:
592
  """Process a file at the given path."""
593
  assert isinstance(path, Path) # Path is properly converted
 
603
  ```python
604
  import uuid
605
 
606
+ @mcp.tool
607
  def process_item(
608
  item_id: uuid.UUID # String UUID or UUID object
609
  ) -> str:
 
628
  age: int | None = None
629
  is_active: bool = True
630
 
631
+ @mcp.tool
632
  def create_user(user: User):
633
  """Create a new user in the system."""
634
  # The input is automatically validated against the User model
 
657
  from typing import Annotated
658
  from pydantic import Field
659
 
660
+ @mcp.tool
661
  def analyze_metrics(
662
  # Numbers with range constraints
663
  count: Annotated[int, Field(ge=0, le=100)], # 0 <= count <= 100
 
682
  You can also use `Field` as a default value, though the `Annotated` approach is preferred:
683
 
684
  ```python
685
+ @mcp.tool
686
  def validate_data(
687
  # Value constraints
688
  age: int = Field(ge=0, lt=120), # 0 <= age < 120
 
727
  on_duplicate_tools="error"
728
  )
729
 
730
+ @mcp.tool
731
  def my_tool(): return "Version 1"
732
 
733
  # This will now raise a ValueError because 'my_tool' already exists
734
  # and on_duplicate_tools is set to "error".
735
+ # @mcp.tool
736
  # def my_tool(): return "Version 2"
737
  ```
738
 
 
754
 
755
  mcp = FastMCP(name="DynamicToolServer")
756
 
757
+ @mcp.tool
758
  def calculate_sum(a: int, b: int) -> int:
759
  """Add two numbers together."""
760
  return a + b
examples/complex_inputs.py CHANGED
@@ -20,7 +20,7 @@ class ShrimpTank(BaseModel):
20
  shrimp: list[Shrimp]
21
 
22
 
23
- @mcp.tool()
24
  def name_shrimp(
25
  tank: ShrimpTank,
26
  # You can use pydantic Field in function signatures for validation.
 
20
  shrimp: list[Shrimp]
21
 
22
 
23
+ @mcp.tool
24
  def name_shrimp(
25
  tank: ShrimpTank,
26
  # You can use pydantic Field in function signatures for validation.
examples/config_server.py CHANGED
@@ -24,7 +24,7 @@ if args.debug:
24
  mcp = FastMCP(server_name)
25
 
26
 
27
- @mcp.tool()
28
  def get_status() -> dict[str, str | bool]:
29
  """Get the current server configuration and status."""
30
  return {
@@ -34,7 +34,7 @@ def get_status() -> dict[str, str | bool]:
34
  }
35
 
36
 
37
- @mcp.tool()
38
  def echo_message(message: str) -> str:
39
  """Echo a message, with debug info if debug mode is enabled."""
40
  if args.debug:
 
24
  mcp = FastMCP(server_name)
25
 
26
 
27
+ @mcp.tool
28
  def get_status() -> dict[str, str | bool]:
29
  """Get the current server configuration and status."""
30
  return {
 
34
  }
35
 
36
 
37
+ @mcp.tool
38
  def echo_message(message: str) -> str:
39
  """Echo a message, with debug info if debug mode is enabled."""
40
  if args.debug:
examples/desktop.py CHANGED
@@ -26,7 +26,7 @@ def get_greeting(name: str) -> str:
26
  return f"Hello, {name}!"
27
 
28
 
29
- @mcp.tool()
30
  def add(a: int, b: int) -> int:
31
  """Add two numbers"""
32
  return a + b
 
26
  return f"Hello, {name}!"
27
 
28
 
29
+ @mcp.tool
30
  def add(a: int, b: int) -> int:
31
  """Add two numbers"""
32
  return a + b
examples/echo.py CHANGED
@@ -8,7 +8,7 @@ from fastmcp import FastMCP
8
  mcp = FastMCP("Echo Server")
9
 
10
 
11
- @mcp.tool()
12
  def echo_tool(text: str) -> str:
13
  """Echo the input text"""
14
  return text
 
8
  mcp = FastMCP("Echo Server")
9
 
10
 
11
+ @mcp.tool
12
  def echo_tool(text: str) -> str:
13
  """Echo the input text"""
14
  return text
examples/memory.py CHANGED
@@ -279,7 +279,7 @@ async def display_memory_tree(deps: Deps) -> str:
279
  return result
280
 
281
 
282
- @mcp.tool()
283
  async def remember(
284
  contents: list[str] = Field(
285
  description="List of observations or memories to store"
@@ -294,7 +294,7 @@ async def remember(
294
  await deps.pool.close()
295
 
296
 
297
- @mcp.tool()
298
  async def read_profile() -> str:
299
  deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())
300
  profile = await display_memory_tree(deps)
 
279
  return result
280
 
281
 
282
+ @mcp.tool
283
  async def remember(
284
  contents: list[str] = Field(
285
  description="List of observations or memories to store"
 
294
  await deps.pool.close()
295
 
296
 
297
+ @mcp.tool
298
  async def read_profile() -> str:
299
  deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())
300
  profile = await display_memory_tree(deps)
examples/mount_example.py CHANGED
@@ -16,7 +16,7 @@ from fastmcp import FastMCP
16
  weather_app = FastMCP("Weather App")
17
 
18
 
19
- @weather_app.tool()
20
  def get_weather_forecast(location: str) -> str:
21
  """Get the weather forecast for a location."""
22
  return f"Sunny skies for {location} today!"
@@ -32,7 +32,7 @@ async def weather_data():
32
  news_app = FastMCP("News App")
33
 
34
 
35
- @news_app.tool()
36
  def get_news_headlines() -> list[str]:
37
  """Get the latest news headlines."""
38
  return [
@@ -58,7 +58,7 @@ app = FastMCP(
58
  )
59
 
60
 
61
- @app.tool()
62
  def check_app_status() -> dict[str, str]:
63
  """Check the status of the main application."""
64
  return {"status": "running", "version": "1.0.0", "uptime": "3h 24m"}
 
16
  weather_app = FastMCP("Weather App")
17
 
18
 
19
+ @weather_app.tool
20
  def get_weather_forecast(location: str) -> str:
21
  """Get the weather forecast for a location."""
22
  return f"Sunny skies for {location} today!"
 
32
  news_app = FastMCP("News App")
33
 
34
 
35
+ @news_app.tool
36
  def get_news_headlines() -> list[str]:
37
  """Get the latest news headlines."""
38
  return [
 
58
  )
59
 
60
 
61
+ @app.tool
62
  def check_app_status() -> dict[str, str]:
63
  """Check the status of the main application."""
64
  return {"status": "running", "version": "1.0.0", "uptime": "3h 24m"}
examples/sampling.py CHANGED
@@ -15,7 +15,7 @@ from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingPar
15
  mcp = FastMCP("Sampling Example")
16
 
17
 
18
- @mcp.tool()
19
  async def example_tool(prompt: str, context: Context) -> str:
20
  """Sample a completion from the LLM."""
21
  response = await context.sample(
 
15
  mcp = FastMCP("Sampling Example")
16
 
17
 
18
+ @mcp.tool
19
  async def example_tool(prompt: str, context: Context) -> str:
20
  """Sample a completion from the LLM."""
21
  response = await context.sample(
examples/screenshot.py CHANGED
@@ -12,7 +12,7 @@ from fastmcp import FastMCP, Image
12
  mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"])
13
 
14
 
15
- @mcp.tool()
16
  def take_screenshot() -> Image:
17
  """
18
  Take a screenshot of the user's screen and return it as an image. Use
 
12
  mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"])
13
 
14
 
15
+ @mcp.tool
16
  def take_screenshot() -> Image:
17
  """
18
  Take a screenshot of the user's screen and return it as an image. Use
examples/serializer.py CHANGED
@@ -14,7 +14,7 @@ def custom_dict_serializer(data: Any) -> str:
14
  server = FastMCP(name="CustomSerializerExample", tool_serializer=custom_dict_serializer)
15
 
16
 
17
- @server.tool()
18
  def get_example_data() -> dict:
19
  """Returns some example data."""
20
  return {"name": "Test", "value": 123, "status": True}
 
14
  server = FastMCP(name="CustomSerializerExample", tool_serializer=custom_dict_serializer)
15
 
16
 
17
+ @server.tool
18
  def get_example_data() -> dict:
19
  """Returns some example data."""
20
  return {"name": "Test", "value": 123, "status": True}
examples/simple_echo.py CHANGED
@@ -8,7 +8,7 @@ from fastmcp import FastMCP
8
  mcp = FastMCP("Echo Server")
9
 
10
 
11
- @mcp.tool()
12
  def echo(text: str) -> str:
13
  """Echo the input text"""
14
  return text
 
8
  mcp = FastMCP("Echo Server")
9
 
10
 
11
+ @mcp.tool
12
  def echo(text: str) -> str:
13
  """Echo the input text"""
14
  return text
examples/smart_home/src/smart_home/hub.py CHANGED
@@ -16,7 +16,7 @@ hub_mcp.mount("hue", lights_mcp)
16
 
17
 
18
  # Add a status check for the hub
19
- @hub_mcp.tool()
20
  def hub_status() -> str:
21
  """Checks the status of the main hub and connections."""
22
  try:
 
16
 
17
 
18
  # Add a status check for the hub
19
+ @hub_mcp.tool
20
  def hub_status() -> str:
21
  """Checks the status of the main hub and connections."""
22
  try:
examples/smart_home/src/smart_home/lights/server.py CHANGED
@@ -43,7 +43,7 @@ lights_mcp = FastMCP(
43
  )
44
 
45
 
46
- @lights_mcp.tool()
47
  def read_all_lights() -> list[str]:
48
  """Lists the names of all available Hue lights using phue2."""
49
  if not (bridge := _get_bridge()):
@@ -59,7 +59,7 @@ def read_all_lights() -> list[str]:
59
  # --- Tools ---
60
 
61
 
62
- @lights_mcp.tool()
63
  def toggle_light(light_name: str, state: bool) -> dict[str, Any]:
64
  """Turns a specific light on (true) or off (false) using phue2."""
65
  if not (bridge := _get_bridge()):
@@ -76,7 +76,7 @@ def toggle_light(light_name: str, state: bool) -> dict[str, Any]:
76
  return handle_phue_error(light_name, "toggle_light", e)
77
 
78
 
79
- @lights_mcp.tool()
80
  def set_brightness(light_name: str, brightness: int) -> dict[str, Any]:
81
  """Sets the brightness of a specific light (0-254) using phue2."""
82
  if not (bridge := _get_bridge()):
@@ -100,7 +100,7 @@ def set_brightness(light_name: str, brightness: int) -> dict[str, Any]:
100
  return handle_phue_error(light_name, "set_brightness", e)
101
 
102
 
103
- @lights_mcp.tool()
104
  def list_groups() -> list[str]:
105
  """Lists the names of all available Hue light groups."""
106
  if not (bridge := _get_bridge()):
@@ -113,7 +113,7 @@ def list_groups() -> list[str]:
113
  return [f"Error listing groups: {e}"]
114
 
115
 
116
- @lights_mcp.tool()
117
  def list_scenes() -> dict[str, list[str]] | list[str]:
118
  """Lists Hue scenes, grouped by the light group they belong to.
119
 
@@ -154,7 +154,7 @@ def list_scenes() -> dict[str, list[str]] | list[str]:
154
  return [f"Error listing scenes by group: {e}"]
155
 
156
 
157
- @lights_mcp.tool()
158
  def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]:
159
  """Activates a specific scene within a specified light group, verifying the scene belongs to the group."""
160
  if not (bridge := _get_bridge()):
@@ -215,7 +215,7 @@ def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]:
215
  return handle_phue_error(f"{group_name}/{scene_name}", "activate_scene", e)
216
 
217
 
218
- @lights_mcp.tool()
219
  def set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str, Any]:
220
  """Sets multiple attributes (e.g., hue, sat, bri, ct, xy, transitiontime) for a specific light."""
221
  if not (bridge := _get_bridge()):
@@ -242,7 +242,7 @@ def set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str
242
  return handle_phue_error(light_name, "set_light_attributes", e)
243
 
244
 
245
- @lights_mcp.tool()
246
  def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str, Any]:
247
  """Sets multiple attributes for all lights within a specific group."""
248
  if not (bridge := _get_bridge()):
@@ -267,7 +267,7 @@ def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str
267
  return handle_phue_error(group_name, "set_group_attributes", e)
268
 
269
 
270
- @lights_mcp.tool()
271
  def list_lights_by_group() -> dict[str, list[str]] | list[str]:
272
  """Lists Hue lights, grouped by the room/group they belong to.
273
 
 
43
  )
44
 
45
 
46
+ @lights_mcp.tool
47
  def read_all_lights() -> list[str]:
48
  """Lists the names of all available Hue lights using phue2."""
49
  if not (bridge := _get_bridge()):
 
59
  # --- Tools ---
60
 
61
 
62
+ @lights_mcp.tool
63
  def toggle_light(light_name: str, state: bool) -> dict[str, Any]:
64
  """Turns a specific light on (true) or off (false) using phue2."""
65
  if not (bridge := _get_bridge()):
 
76
  return handle_phue_error(light_name, "toggle_light", e)
77
 
78
 
79
+ @lights_mcp.tool
80
  def set_brightness(light_name: str, brightness: int) -> dict[str, Any]:
81
  """Sets the brightness of a specific light (0-254) using phue2."""
82
  if not (bridge := _get_bridge()):
 
100
  return handle_phue_error(light_name, "set_brightness", e)
101
 
102
 
103
+ @lights_mcp.tool
104
  def list_groups() -> list[str]:
105
  """Lists the names of all available Hue light groups."""
106
  if not (bridge := _get_bridge()):
 
113
  return [f"Error listing groups: {e}"]
114
 
115
 
116
+ @lights_mcp.tool
117
  def list_scenes() -> dict[str, list[str]] | list[str]:
118
  """Lists Hue scenes, grouped by the light group they belong to.
119
 
 
154
  return [f"Error listing scenes by group: {e}"]
155
 
156
 
157
+ @lights_mcp.tool
158
  def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]:
159
  """Activates a specific scene within a specified light group, verifying the scene belongs to the group."""
160
  if not (bridge := _get_bridge()):
 
215
  return handle_phue_error(f"{group_name}/{scene_name}", "activate_scene", e)
216
 
217
 
218
+ @lights_mcp.tool
219
  def set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str, Any]:
220
  """Sets multiple attributes (e.g., hue, sat, bri, ct, xy, transitiontime) for a specific light."""
221
  if not (bridge := _get_bridge()):
 
242
  return handle_phue_error(light_name, "set_light_attributes", e)
243
 
244
 
245
+ @lights_mcp.tool
246
  def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str, Any]:
247
  """Sets multiple attributes for all lights within a specific group."""
248
  if not (bridge := _get_bridge()):
 
267
  return handle_phue_error(group_name, "set_group_attributes", e)
268
 
269
 
270
+ @lights_mcp.tool
271
  def list_lights_by_group() -> dict[str, list[str]] | list[str]:
272
  """Lists Hue lights, grouped by the room/group they belong to.
273
 
src/fastmcp/contrib/bulk_tool_caller/example.py CHANGED
@@ -6,7 +6,7 @@ from fastmcp.contrib.bulk_tool_caller import BulkToolCaller
6
  mcp = FastMCP()
7
 
8
 
9
- @mcp.tool()
10
  def echo_tool(text: str) -> str:
11
  """Echo the input text"""
12
  return text
 
6
  mcp = FastMCP()
7
 
8
 
9
+ @mcp.tool
10
  def echo_tool(text: str) -> str:
11
  """Echo the input text"""
12
  return text
src/fastmcp/server/context.py CHANGED
@@ -49,7 +49,7 @@ class Context:
49
  To use context in a tool function, add a parameter with the Context type annotation:
50
 
51
  ```python
52
- @server.tool()
53
  def my_tool(x: int, ctx: Context) -> str:
54
  # Log messages to the client
55
  ctx.info(f"Processing {x}")
 
49
  To use context in a tool function, add a parameter with the Context type annotation:
50
 
51
  ```python
52
+ @server.tool
53
  def my_tool(x: int, ctx: Context) -> str:
54
  # Log messages to the client
55
  ctx.info(f"Processing {x}")
src/fastmcp/server/server.py CHANGED
@@ -513,53 +513,69 @@ class FastMCP(Generic[LifespanResultT]):
513
 
514
  def tool(
515
  self,
 
 
516
  name: str | None = None,
517
  description: str | None = None,
518
  tags: set[str] | None = None,
519
  annotations: ToolAnnotations | dict[str, Any] | None = None,
520
  exclude_args: list[str] | None = None,
521
- ) -> Callable[[AnyFunction], AnyFunction]:
522
  """Decorator to register a tool.
523
 
524
  Tools can optionally request a Context object by adding a parameter with the
525
  Context type annotation. The context provides access to MCP capabilities like
526
  logging, progress reporting, and resource access.
527
 
 
 
 
 
 
 
 
528
  Args:
529
- name: Optional name for the tool (defaults to function name)
530
  description: Optional description of what the tool does
531
  tags: Optional set of tags for categorizing the tool
532
  annotations: Optional annotations about the tool's behavior
 
 
533
 
534
  Example:
535
- @server.tool()
536
  def my_tool(x: int) -> str:
537
  return str(x)
538
 
539
- @server.tool()
540
- def tool_with_context(x: int, ctx: Context) -> str:
541
- ctx.info(f"Processing {x}")
542
  return str(x)
543
 
544
- @server.tool()
545
- async def async_tool(x: int, context: Context) -> str:
546
- await context.report_progress(50, 100)
547
  return str(x)
548
- """
549
 
550
- # Check if user passed function directly instead of calling decorator
551
- if callable(name):
552
- raise TypeError(
553
- "The @tool decorator was used incorrectly. "
554
- "Did you forget to call it? Use @tool() instead of @tool"
555
- )
 
556
  if isinstance(annotations, dict):
557
  annotations = ToolAnnotations(**annotations)
558
 
559
- def decorator(fn: AnyFunction) -> AnyFunction:
 
 
 
 
 
 
 
560
  tool = Tool.from_function(
561
  fn,
562
- name=name,
563
  description=description,
564
  tags=tags,
565
  annotations=annotations,
@@ -569,7 +585,31 @@ class FastMCP(Generic[LifespanResultT]):
569
  self.add_tool(tool)
570
  return fn
571
 
572
- return decorator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
 
574
  def add_resource(self, resource: Resource, key: str | None = None) -> None:
575
  """Add a resource to the server.
 
513
 
514
  def tool(
515
  self,
516
+ name_or_fn: str | AnyFunction | None = None,
517
+ *,
518
  name: str | None = None,
519
  description: str | None = None,
520
  tags: set[str] | None = None,
521
  annotations: ToolAnnotations | dict[str, Any] | None = None,
522
  exclude_args: list[str] | None = None,
523
+ ) -> Callable[[AnyFunction], AnyFunction] | AnyFunction:
524
  """Decorator to register a tool.
525
 
526
  Tools can optionally request a Context object by adding a parameter with the
527
  Context type annotation. The context provides access to MCP capabilities like
528
  logging, progress reporting, and resource access.
529
 
530
+ This decorator supports multiple calling patterns:
531
+ - @server.tool (without parentheses)
532
+ - @server.tool (with empty parentheses)
533
+ - @server.tool("custom_name") (with name as first argument)
534
+ - @server.tool(name="custom_name") (with name as keyword argument)
535
+ - server.tool(function, name="custom_name") (direct function call)
536
+
537
  Args:
538
+ name_or_fn: Either a function (when used as @tool), a string name, or None
539
  description: Optional description of what the tool does
540
  tags: Optional set of tags for categorizing the tool
541
  annotations: Optional annotations about the tool's behavior
542
+ exclude_args: Optional list of argument names to exclude from the tool schema
543
+ name: Optional name for the tool (keyword-only, alternative to name_or_fn)
544
 
545
  Example:
546
+ @server.tool
547
  def my_tool(x: int) -> str:
548
  return str(x)
549
 
550
+ @server.tool
551
+ def my_tool(x: int) -> str:
 
552
  return str(x)
553
 
554
+ @server.tool("custom_name")
555
+ def my_tool(x: int) -> str:
 
556
  return str(x)
 
557
 
558
+ @server.tool(name="custom_name")
559
+ def my_tool(x: int) -> str:
560
+ return str(x)
561
+
562
+ # Direct function call
563
+ server.tool(my_function, name="custom_name")
564
+ """
565
  if isinstance(annotations, dict):
566
  annotations = ToolAnnotations(**annotations)
567
 
568
+ # Determine the actual name and function based on the calling pattern
569
+ if callable(name_or_fn):
570
+ # Case 1: @tool (without parens) - function passed directly
571
+ # Case 2: direct call like tool(fn, name="something")
572
+ fn = name_or_fn
573
+ tool_name = name # Use keyword name if provided, otherwise None
574
+
575
+ # Register the tool immediately and return the function
576
  tool = Tool.from_function(
577
  fn,
578
+ name=tool_name,
579
  description=description,
580
  tags=tags,
581
  annotations=annotations,
 
585
  self.add_tool(tool)
586
  return fn
587
 
588
+ elif isinstance(name_or_fn, str):
589
+ # Case 3: @tool("custom_name") - name passed as first argument
590
+ if name is not None:
591
+ raise TypeError(
592
+ "Cannot specify both a name as first argument and as keyword argument. "
593
+ f"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both."
594
+ )
595
+ tool_name = name_or_fn
596
+ elif name_or_fn is None:
597
+ # Case 4: @tool or @tool(name="something") - use keyword name
598
+ tool_name = name
599
+ else:
600
+ raise TypeError(
601
+ f"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}"
602
+ )
603
+
604
+ # Return partial for cases where we need to wait for the function
605
+ return partial(
606
+ self.tool,
607
+ name=tool_name,
608
+ description=description,
609
+ tags=tags,
610
+ annotations=annotations,
611
+ exclude_args=exclude_args,
612
+ )
613
 
614
  def add_resource(self, resource: Resource, key: str | None = None) -> None:
615
  """Add a resource to the server.
tests/auth/providers/test_bearer.py CHANGED
@@ -53,7 +53,7 @@ def run_mcp_server(
53
  )
54
  )
55
 
56
- @mcp.tool()
57
  def add(a: int, b: int) -> int:
58
  return a + b
59
 
 
53
  )
54
  )
55
 
56
+ @mcp.tool
57
  def add(a: int, b: int) -> int:
58
  return a + b
59
 
tests/auth/test_oauth_client.py CHANGED
@@ -24,7 +24,7 @@ def fastmcp_server(issuer_url: str):
24
  ),
25
  )
26
 
27
- @server.tool()
28
  def add(a: int, b: int) -> int:
29
  """Add two numbers together."""
30
  return a + b
 
24
  ),
25
  )
26
 
27
+ @server.tool
28
  def add(a: int, b: int) -> int:
29
  """Add two numbers together."""
30
  return a + b
tests/client/test_client.py CHANGED
@@ -25,18 +25,18 @@ def fastmcp_server():
25
  server = FastMCP("TestServer")
26
 
27
  # Add a tool
28
- @server.tool()
29
  def greet(name: str) -> str:
30
  """Greet someone by name."""
31
  return f"Hello, {name}!"
32
 
33
  # Add a second tool
34
- @server.tool()
35
  def add(a: int, b: int) -> int:
36
  """Add two numbers together."""
37
  return a + b
38
 
39
- @server.tool()
40
  async def sleep(seconds: float) -> str:
41
  """Sleep for a given number of seconds."""
42
  await asyncio.sleep(seconds)
@@ -347,7 +347,7 @@ async def test_concurrent_client_context_managers():
347
  # Create a simple server
348
  server = FastMCP("Test Server")
349
 
350
- @server.tool()
351
  def echo(text: str) -> str:
352
  """Echo tool"""
353
  return text
@@ -510,7 +510,7 @@ class TestErrorHandling:
510
  async def test_general_tool_exceptions_are_not_masked_by_default(self):
511
  mcp = FastMCP("TestServer")
512
 
513
- @mcp.tool()
514
  def error_tool():
515
  raise ValueError("This is a test error (abc)")
516
 
@@ -525,7 +525,7 @@ class TestErrorHandling:
525
  async def test_general_tool_exceptions_are_masked_when_enabled(self):
526
  mcp = FastMCP("TestServer", mask_error_details=True)
527
 
528
- @mcp.tool()
529
  def error_tool():
530
  raise ValueError("This is a test error (abc)")
531
 
@@ -540,7 +540,7 @@ class TestErrorHandling:
540
  async def test_specific_tool_errors_are_sent_to_client(self):
541
  mcp = FastMCP("TestServer")
542
 
543
- @mcp.tool()
544
  def custom_error_tool():
545
  raise ToolError("This is a test error (abc)")
546
 
 
25
  server = FastMCP("TestServer")
26
 
27
  # Add a tool
28
+ @server.tool
29
  def greet(name: str) -> str:
30
  """Greet someone by name."""
31
  return f"Hello, {name}!"
32
 
33
  # Add a second tool
34
+ @server.tool
35
  def add(a: int, b: int) -> int:
36
  """Add two numbers together."""
37
  return a + b
38
 
39
+ @server.tool
40
  async def sleep(seconds: float) -> str:
41
  """Sleep for a given number of seconds."""
42
  await asyncio.sleep(seconds)
 
347
  # Create a simple server
348
  server = FastMCP("Test Server")
349
 
350
+ @server.tool
351
  def echo(text: str) -> str:
352
  """Echo tool"""
353
  return text
 
510
  async def test_general_tool_exceptions_are_not_masked_by_default(self):
511
  mcp = FastMCP("TestServer")
512
 
513
+ @mcp.tool
514
  def error_tool():
515
  raise ValueError("This is a test error (abc)")
516
 
 
525
  async def test_general_tool_exceptions_are_masked_when_enabled(self):
526
  mcp = FastMCP("TestServer", mask_error_details=True)
527
 
528
+ @mcp.tool
529
  def error_tool():
530
  raise ValueError("This is a test error (abc)")
531
 
 
540
  async def test_specific_tool_errors_are_sent_to_client(self):
541
  mcp = FastMCP("TestServer")
542
 
543
+ @mcp.tool
544
  def custom_error_tool():
545
  raise ToolError("This is a test error (abc)")
546
 
tests/client/test_logs.py CHANGED
@@ -17,11 +17,11 @@ class LogHandler:
17
  def fastmcp_server():
18
  mcp = FastMCP()
19
 
20
- @mcp.tool()
21
  async def log(context: Context) -> None:
22
  await context.info(message="hello?")
23
 
24
- @mcp.tool()
25
  async def echo_log(
26
  message: str,
27
  context: Context,
 
17
  def fastmcp_server():
18
  mcp = FastMCP()
19
 
20
+ @mcp.tool
21
  async def log(context: Context) -> None:
22
  await context.info(message="hello?")
23
 
24
+ @mcp.tool
25
  async def echo_log(
26
  message: str,
27
  context: Context,
tests/client/test_progress.py CHANGED
@@ -16,7 +16,7 @@ def clear_progress_messages():
16
  def fastmcp_server():
17
  mcp = FastMCP()
18
 
19
- @mcp.tool()
20
  async def progress_tool(context: Context) -> int:
21
  for i in range(3):
22
  await context.report_progress(
 
16
  def fastmcp_server():
17
  mcp = FastMCP()
18
 
19
+ @mcp.tool
20
  async def progress_tool(context: Context) -> int:
21
  for i in range(3):
22
  await context.report_progress(
tests/client/test_roots.py CHANGED
@@ -9,7 +9,7 @@ from fastmcp import Client, Context, FastMCP
9
  def fastmcp_server():
10
  mcp = FastMCP()
11
 
12
- @mcp.tool()
13
  async def list_roots(context: Context) -> list[str]:
14
  roots = await context.list_roots()
15
  return [str(r.uri) for r in roots]
 
9
  def fastmcp_server():
10
  mcp = FastMCP()
11
 
12
+ @mcp.tool
13
  async def list_roots(context: Context) -> list[str]:
14
  roots = await context.list_roots()
15
  return [str(r.uri) for r in roots]
tests/client/test_sampling.py CHANGED
@@ -11,17 +11,17 @@ from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingPar
11
  def fastmcp_server():
12
  mcp = FastMCP()
13
 
14
- @mcp.tool()
15
  async def simple_sample(message: str, context: Context) -> str:
16
  result = await context.sample("Hello, world!")
17
  return cast(TextContent, result).text
18
 
19
- @mcp.tool()
20
  async def sample_with_system_prompt(message: str, context: Context) -> str:
21
  result = await context.sample("Hello, world!", system_prompt="You love FastMCP")
22
  return cast(TextContent, result).text
23
 
24
- @mcp.tool()
25
  async def sample_with_messages(message: str, context: Context) -> str:
26
  result = await context.sample(
27
  [
 
11
  def fastmcp_server():
12
  mcp = FastMCP()
13
 
14
+ @mcp.tool
15
  async def simple_sample(message: str, context: Context) -> str:
16
  result = await context.sample("Hello, world!")
17
  return cast(TextContent, result).text
18
 
19
+ @mcp.tool
20
  async def sample_with_system_prompt(message: str, context: Context) -> str:
21
  result = await context.sample("Hello, world!", system_prompt="You love FastMCP")
22
  return cast(TextContent, result).text
23
 
24
+ @mcp.tool
25
  async def sample_with_messages(message: str, context: Context) -> str:
26
  result = await context.sample(
27
  [
tests/client/test_sse.py CHANGED
@@ -21,18 +21,18 @@ def fastmcp_server():
21
  server = FastMCP("TestServer")
22
 
23
  # Add a tool
24
- @server.tool()
25
  def greet(name: str) -> str:
26
  """Greet someone by name."""
27
  return f"Hello, {name}!"
28
 
29
  # Add a second tool
30
- @server.tool()
31
  def add(a: int, b: int) -> int:
32
  """Add two numbers together."""
33
  return a + b
34
 
35
- @server.tool()
36
  async def sleep(seconds: float) -> str:
37
  """Sleep for a given number of seconds."""
38
  await asyncio.sleep(seconds)
 
21
  server = FastMCP("TestServer")
22
 
23
  # Add a tool
24
+ @server.tool
25
  def greet(name: str) -> str:
26
  """Greet someone by name."""
27
  return f"Hello, {name}!"
28
 
29
  # Add a second tool
30
+ @server.tool
31
  def add(a: int, b: int) -> int:
32
  """Add two numbers together."""
33
  return a + b
34
 
35
+ @server.tool
36
  async def sleep(seconds: float) -> str:
37
  """Sleep for a given number of seconds."""
38
  await asyncio.sleep(seconds)
tests/client/test_stdio.py CHANGED
@@ -17,7 +17,7 @@ class TestKeepAlive:
17
 
18
  mcp = FastMCP()
19
 
20
- @mcp.tool()
21
  def pid() -> int:
22
  """Gets PID of server"""
23
  return os.getpid()
 
17
 
18
  mcp = FastMCP()
19
 
20
+ @mcp.tool
21
  def pid() -> int:
22
  """Gets PID of server"""
23
  return os.getpid()
tests/client/test_streamable_http.py CHANGED
@@ -21,18 +21,18 @@ def fastmcp_server():
21
  server = FastMCP("TestServer")
22
 
23
  # Add a tool
24
- @server.tool()
25
  def greet(name: str) -> str:
26
  """Greet someone by name."""
27
  return f"Hello, {name}!"
28
 
29
  # Add a second tool
30
- @server.tool()
31
  def add(a: int, b: int) -> int:
32
  """Add two numbers together."""
33
  return a + b
34
 
35
- @server.tool()
36
  async def sleep(seconds: float) -> str:
37
  """Sleep for a given number of seconds."""
38
  await asyncio.sleep(seconds)
 
21
  server = FastMCP("TestServer")
22
 
23
  # Add a tool
24
+ @server.tool
25
  def greet(name: str) -> str:
26
  """Greet someone by name."""
27
  return f"Hello, {name}!"
28
 
29
  # Add a second tool
30
+ @server.tool
31
  def add(a: int, b: int) -> int:
32
  """Add two numbers together."""
33
  return a + b
34
 
35
+ @server.tool
36
  async def sleep(seconds: float) -> str:
37
  """Sleep for a given number of seconds."""
38
  await asyncio.sleep(seconds)
tests/deprecated/test_deprecated.py CHANGED
@@ -100,7 +100,7 @@ def test_mount_tool_separator_deprecation_warning():
100
  main_app.mount("sub", sub_app, tool_separator="-")
101
 
102
  # Verify the separator is ignored and the default is used
103
- @sub_app.tool()
104
  def test_tool():
105
  return "test"
106
 
 
100
  main_app.mount("sub", sub_app, tool_separator="-")
101
 
102
  # Verify the separator is ignored and the default is used
103
+ @sub_app.tool
104
  def test_tool():
105
  return "test"
106
 
tests/deprecated/test_mount_separators.py CHANGED
@@ -20,7 +20,7 @@ def test_mount_tool_separator_deprecation_warning():
20
  main_app.mount("sub", sub_app, tool_separator="-")
21
 
22
  # Verify the separator is ignored and the default is used
23
- @sub_app.tool()
24
  def test_tool():
25
  return "test"
26
 
 
20
  main_app.mount("sub", sub_app, tool_separator="-")
21
 
22
  # Verify the separator is ignored and the default is used
23
+ @sub_app.tool
24
  def test_tool():
25
  return "test"
26
 
tests/server/http/test_http_dependencies.py CHANGED
@@ -14,7 +14,7 @@ def fastmcp_server():
14
  server = FastMCP()
15
 
16
  # Add a tool
17
- @server.tool()
18
  def get_headers_tool() -> dict[str, str]:
19
  """Get the HTTP headers from the request."""
20
  request = get_http_request()
 
14
  server = FastMCP()
15
 
16
  # Add a tool
17
+ @server.tool
18
  def get_headers_tool() -> dict[str, str]:
19
  """Get the HTTP headers from the request."""
20
  request = get_http_request()
tests/server/test_file_server.py CHANGED
@@ -62,7 +62,7 @@ def resources(mcp: FastMCP, test_dir: Path) -> FastMCP:
62
 
63
  @pytest.fixture(autouse=True)
64
  def tools(mcp: FastMCP, test_dir: Path) -> FastMCP:
65
- @mcp.tool()
66
  def delete_file(path: str) -> bool:
67
  # ensure path is in test_dir
68
  if Path(path).resolve().parent != test_dir:
 
62
 
63
  @pytest.fixture(autouse=True)
64
  def tools(mcp: FastMCP, test_dir: Path) -> FastMCP:
65
+ @mcp.tool
66
  def delete_file(path: str) -> bool:
67
  # ensure path is in test_dir
68
  if Path(path).resolve().parent != test_dir:
tests/server/test_import_server.py CHANGED
@@ -13,7 +13,7 @@ async def test_import_basic_functionality():
13
  sub_app = FastMCP("SubApp")
14
 
15
  # Add a tool to the sub-app
16
- @sub_app.tool()
17
  def sub_tool() -> str:
18
  return "This is from the sub app"
19
 
@@ -40,11 +40,11 @@ async def test_import_multiple_apps():
40
  news_app = FastMCP("NewsApp")
41
 
42
  # Add tools to each sub-app
43
- @weather_app.tool()
44
  def get_forecast() -> str:
45
  return "Weather forecast"
46
 
47
- @news_app.tool()
48
  def get_headlines() -> str:
49
  return "News headlines"
50
 
@@ -65,11 +65,11 @@ async def test_import_combines_tools():
65
  second_app = FastMCP("SecondApp")
66
 
67
  # Add tools to each sub-app
68
- @first_app.tool()
69
  def first_tool() -> str:
70
  return "First app tool"
71
 
72
- @second_app.tool()
73
  def second_tool() -> str:
74
  return "Second app tool"
75
 
@@ -294,7 +294,7 @@ async def test_import_with_proxy_tools():
294
  main_app = FastMCP("MainApp")
295
  api_app = FastMCP("APIApp")
296
 
297
- @api_app.tool()
298
  def get_data(query: str) -> str:
299
  return f"Data for query: {query}"
300
 
 
13
  sub_app = FastMCP("SubApp")
14
 
15
  # Add a tool to the sub-app
16
+ @sub_app.tool
17
  def sub_tool() -> str:
18
  return "This is from the sub app"
19
 
 
40
  news_app = FastMCP("NewsApp")
41
 
42
  # Add tools to each sub-app
43
+ @weather_app.tool
44
  def get_forecast() -> str:
45
  return "Weather forecast"
46
 
47
+ @news_app.tool
48
  def get_headlines() -> str:
49
  return "News headlines"
50
 
 
65
  second_app = FastMCP("SecondApp")
66
 
67
  # Add tools to each sub-app
68
+ @first_app.tool
69
  def first_tool() -> str:
70
  return "First app tool"
71
 
72
+ @second_app.tool
73
  def second_tool() -> str:
74
  return "Second app tool"
75
 
 
294
  main_app = FastMCP("MainApp")
295
  api_app = FastMCP("APIApp")
296
 
297
+ @api_app.tool
298
  def get_data(query: str) -> str:
299
  return f"Data for query: {query}"
300