Jeremiah Lowin commited on
Commit
4757dcb
·
1 Parent(s): a853a78

Ensure tool serializer is applied

Browse files
src/fastmcp/server/server.py CHANGED
@@ -154,7 +154,6 @@ class FastMCP(Generic[LifespanResultT]):
154
  self._additional_http_routes: list[BaseRoute] = []
155
  self._tool_manager = ToolManager(
156
  duplicate_behavior=on_duplicate_tools,
157
- serializer=tool_serializer,
158
  mask_error_details=self.settings.mask_error_details,
159
  )
160
  self._resource_manager = ResourceManager(
@@ -165,6 +164,7 @@ class FastMCP(Generic[LifespanResultT]):
165
  duplicate_behavior=on_duplicate_prompts,
166
  mask_error_details=self.settings.mask_error_details,
167
  )
 
168
 
169
  if lifespan is None:
170
  self._has_lifespan = False
@@ -184,7 +184,7 @@ class FastMCP(Generic[LifespanResultT]):
184
  if tools:
185
  for tool in tools:
186
  if not isinstance(tool, Tool):
187
- tool = Tool.from_function(tool)
188
  self.add_tool(tool)
189
 
190
  # Set up MCP protocol handlers
@@ -564,6 +564,7 @@ class FastMCP(Generic[LifespanResultT]):
564
  tags=tags,
565
  annotations=annotations,
566
  exclude_args=exclude_args,
 
567
  )
568
  self.add_tool(tool)
569
  return fn
 
154
  self._additional_http_routes: list[BaseRoute] = []
155
  self._tool_manager = ToolManager(
156
  duplicate_behavior=on_duplicate_tools,
 
157
  mask_error_details=self.settings.mask_error_details,
158
  )
159
  self._resource_manager = ResourceManager(
 
164
  duplicate_behavior=on_duplicate_prompts,
165
  mask_error_details=self.settings.mask_error_details,
166
  )
167
+ self._tool_serializer = tool_serializer
168
 
169
  if lifespan is None:
170
  self._has_lifespan = False
 
184
  if tools:
185
  for tool in tools:
186
  if not isinstance(tool, Tool):
187
+ tool = Tool.from_function(tool, serializer=self._tool_serializer)
188
  self.add_tool(tool)
189
 
190
  # Set up MCP protocol handlers
 
564
  tags=tags,
565
  annotations=annotations,
566
  exclude_args=exclude_args,
567
+ serializer=self._tool_serializer,
568
  )
569
  self.add_tool(tool)
570
  return fn
src/fastmcp/tools/tool_manager.py CHANGED
@@ -23,11 +23,9 @@ class ToolManager:
23
  def __init__(
24
  self,
25
  duplicate_behavior: DuplicateBehavior | None = None,
26
- serializer: Callable[[Any], str] | None = None,
27
  mask_error_details: bool = False,
28
  ):
29
  self._tools: dict[str, Tool] = {}
30
- self._serializer = serializer
31
  self.mask_error_details = mask_error_details
32
 
33
  # Default to "warn" if None is provided
@@ -67,6 +65,7 @@ class ToolManager:
67
  description: str | None = None,
68
  tags: set[str] | None = None,
69
  annotations: ToolAnnotations | None = None,
 
70
  exclude_args: list[str] | None = None,
71
  ) -> Tool:
72
  """Add a tool to the server."""
@@ -81,8 +80,8 @@ class ToolManager:
81
  description=description,
82
  tags=tags,
83
  annotations=annotations,
84
- serializer=self._serializer,
85
  exclude_args=exclude_args,
 
86
  )
87
  return self.add_tool(tool)
88
 
 
23
  def __init__(
24
  self,
25
  duplicate_behavior: DuplicateBehavior | None = None,
 
26
  mask_error_details: bool = False,
27
  ):
28
  self._tools: dict[str, Tool] = {}
 
29
  self.mask_error_details = mask_error_details
30
 
31
  # Default to "warn" if None is provided
 
65
  description: str | None = None,
66
  tags: set[str] | None = None,
67
  annotations: ToolAnnotations | None = None,
68
+ serializer: Callable[[Any], str] | None = None,
69
  exclude_args: list[str] | None = None,
70
  ) -> Tool:
71
  """Add a tool to the server."""
 
80
  description=description,
81
  tags=tags,
82
  annotations=annotations,
 
83
  exclude_args=exclude_args,
84
+ serializer=serializer,
85
  )
86
  return self.add_tool(tool)
87
 
tests/server/test_server.py CHANGED
@@ -7,6 +7,7 @@ from pydantic import Field
7
  from fastmcp import Client, FastMCP
8
  from fastmcp.exceptions import NotFoundError
9
  from fastmcp.prompts.prompt import Prompt
 
10
  from fastmcp.server.server import (
11
  MountedServer,
12
  add_resource_prefix,
@@ -389,8 +390,10 @@ class TestResourceDecorator:
389
 
390
  obj = MyClass("My prefix:")
391
 
392
- mcp.add_resource_fn(
393
- obj.get_data, uri="resource://data", name="instance-resource"
 
 
394
  )
395
 
396
  async with Client(mcp) as client:
@@ -407,8 +410,10 @@ class TestResourceDecorator:
407
  def get_data(cls) -> str:
408
  return f"{cls.prefix} Hello, world!"
409
 
410
- mcp.add_resource_fn(
411
- MyClass.get_data, uri="resource://data", name="class-resource"
 
 
412
  )
413
 
414
  async with Client(mcp) as client:
@@ -508,9 +513,12 @@ class TestTemplateDecorator:
508
  return f"{self.prefix} Data for {name}"
509
 
510
  obj = MyClass("My prefix:")
511
- mcp.add_resource_fn(
512
- obj.get_data, uri="resource://{name}/data", name="instance-template"
 
 
513
  )
 
514
 
515
  async with Client(mcp) as client:
516
  result = await client.read_resource("resource://test/data")
@@ -526,11 +534,12 @@ class TestTemplateDecorator:
526
  def get_data(cls, name: str) -> str:
527
  return f"{cls.prefix} Data for {name}"
528
 
529
- mcp.add_resource_fn(
530
  MyClass.get_data,
531
- uri="resource://{name}/data",
532
  name="class-template",
533
  )
 
534
 
535
  async with Client(mcp) as client:
536
  result = await client.read_resource("resource://test/data")
 
7
  from fastmcp import Client, FastMCP
8
  from fastmcp.exceptions import NotFoundError
9
  from fastmcp.prompts.prompt import Prompt
10
+ from fastmcp.resources import Resource, ResourceTemplate
11
  from fastmcp.server.server import (
12
  MountedServer,
13
  add_resource_prefix,
 
390
 
391
  obj = MyClass("My prefix:")
392
 
393
+ mcp.add_resource(
394
+ Resource.from_function(
395
+ obj.get_data, uri="resource://data", name="instance-resource"
396
+ )
397
  )
398
 
399
  async with Client(mcp) as client:
 
410
  def get_data(cls) -> str:
411
  return f"{cls.prefix} Hello, world!"
412
 
413
+ mcp.add_resource(
414
+ Resource.from_function(
415
+ MyClass.get_data, uri="resource://data", name="class-resource"
416
+ )
417
  )
418
 
419
  async with Client(mcp) as client:
 
513
  return f"{self.prefix} Data for {name}"
514
 
515
  obj = MyClass("My prefix:")
516
+ template = ResourceTemplate.from_function(
517
+ obj.get_data,
518
+ uri_template="resource://{name}/data",
519
+ name="instance-template",
520
  )
521
+ mcp.add_template(template)
522
 
523
  async with Client(mcp) as client:
524
  result = await client.read_resource("resource://test/data")
 
534
  def get_data(cls, name: str) -> str:
535
  return f"{cls.prefix} Data for {name}"
536
 
537
+ template = ResourceTemplate.from_function(
538
  MyClass.get_data,
539
+ uri_template="resource://{name}/data",
540
  name="class-template",
541
  )
542
+ mcp.add_template(template)
543
 
544
  async with Client(mcp) as client:
545
  result = await client.read_resource("resource://test/data")
tests/server/test_server_interactions.py CHANGED
@@ -20,7 +20,7 @@ from fastmcp import Client, Context, FastMCP
20
  from fastmcp.client.transports import FastMCPTransport
21
  from fastmcp.exceptions import ToolError
22
  from fastmcp.prompts.prompt import EmbeddedResource, Prompt, PromptMessage
23
- from fastmcp.resources import FileResource
24
  from fastmcp.resources.resource import FunctionResource
25
  from fastmcp.tools.tool import Tool
26
  from fastmcp.utilities.types import Image
@@ -1074,7 +1074,10 @@ class TestResourceTemplateContext:
1074
  def __call__(self, param: str, ctx: Context) -> str:
1075
  return f"Resource template: {param} {ctx.request_id}"
1076
 
1077
- mcp.add_resource_fn(MyResource(), uri="resource://{param}")
 
 
 
1078
 
1079
  async with Client(mcp) as client:
1080
  result = await client.read_resource(AnyUrl("resource://test"))
 
20
  from fastmcp.client.transports import FastMCPTransport
21
  from fastmcp.exceptions import ToolError
22
  from fastmcp.prompts.prompt import EmbeddedResource, Prompt, PromptMessage
23
+ from fastmcp.resources import FileResource, ResourceTemplate
24
  from fastmcp.resources.resource import FunctionResource
25
  from fastmcp.tools.tool import Tool
26
  from fastmcp.utilities.types import Image
 
1074
  def __call__(self, param: str, ctx: Context) -> str:
1075
  return f"Resource template: {param} {ctx.request_id}"
1076
 
1077
+ template = ResourceTemplate.from_function(
1078
+ MyResource(), uri_template="resource://{param}"
1079
+ )
1080
+ mcp.add_template(template)
1081
 
1082
  async with Client(mcp) as client:
1083
  result = await client.read_resource(AnyUrl("resource://test"))
tests/tools/test_tool_manager.py CHANGED
@@ -11,6 +11,7 @@ from pydantic import BaseModel
11
  from fastmcp import Context, FastMCP, Image
12
  from fastmcp.exceptions import NotFoundError, ToolError
13
  from fastmcp.tools import FunctionTool, ToolManager
 
14
  from fastmcp.utilities.tests import temporary_settings
15
 
16
 
@@ -23,7 +24,8 @@ class TestAddTools:
23
  return a + b
24
 
25
  manager = ToolManager()
26
- manager.add_tool_from_fn(add)
 
27
 
28
  tool = manager.get_tool("add")
29
  assert tool is not None
@@ -40,7 +42,8 @@ class TestAddTools:
40
  return f"Data from {url}"
41
 
42
  manager = ToolManager()
43
- manager.add_tool_from_fn(fetch_data)
 
44
 
45
  tool = manager.get_tool("fetch_data")
46
  assert tool is not None
@@ -60,7 +63,8 @@ class TestAddTools:
60
  return {"id": 1, **user.model_dump()}
61
 
62
  manager = ToolManager()
63
- manager.add_tool_from_fn(create_user)
 
64
 
65
  tool = manager.get_tool("create_user")
66
  assert tool is not None
@@ -79,7 +83,8 @@ class TestAddTools:
79
  return x + y
80
 
81
  manager = ToolManager()
82
- manager.add_tool_from_fn(Adder())
 
83
 
84
  tool = manager.get_tool("Adder")
85
  assert tool is not None
@@ -98,7 +103,8 @@ class TestAddTools:
98
  return x + y
99
 
100
  manager = ToolManager()
101
- manager.add_tool_from_fn(Adder())
 
102
 
103
  tool = manager.get_tool("Adder")
104
  assert tool is not None
@@ -113,7 +119,8 @@ class TestAddTools:
113
  return Image(data=data)
114
 
115
  manager = ToolManager()
116
- manager.add_tool_from_fn(image_tool)
 
117
 
118
  tool = manager.get_tool("image_tool")
119
  result = await tool.run({"data": "test.png"})
@@ -123,11 +130,13 @@ class TestAddTools:
123
  def test_add_noncallable_tool(self):
124
  manager = ToolManager()
125
  with pytest.raises(TypeError, match="not a callable object"):
126
- manager.add_tool_from_fn(1) # type: ignore
 
127
 
128
  def test_add_lambda(self):
129
  manager = ToolManager()
130
- tool = manager.add_tool_from_fn(lambda x: x, name="my_tool")
 
131
  assert tool.name == "my_tool"
132
 
133
  def test_add_lambda_with_no_name(self):
@@ -135,7 +144,8 @@ class TestAddTools:
135
  with pytest.raises(
136
  ValueError, match="You must provide a name for lambda functions"
137
  ):
138
- manager.add_tool_from_fn(lambda x: x)
 
139
 
140
  def test_remove_tool_successfully(self):
141
  """Test removing an added tool by key."""
@@ -144,7 +154,8 @@ class TestAddTools:
144
  def add(a: int, b: int) -> int:
145
  return a + b
146
 
147
- manager.add_tool_from_fn(add)
 
148
  assert manager.get_tool("add") is not None
149
 
150
  manager.remove_tool("add")
@@ -164,8 +175,10 @@ class TestAddTools:
164
  def test_fn(x: int) -> int:
165
  return x
166
 
167
- manager.add_tool_from_fn(test_fn, name="test_tool")
168
- manager.add_tool_from_fn(test_fn, name="test_tool")
 
 
169
 
170
  assert "Tool already exists: test_tool" in caplog.text
171
  # Should have the tool
@@ -178,9 +191,11 @@ class TestAddTools:
178
  return x
179
 
180
  manager = ToolManager(duplicate_behavior="ignore")
181
- manager.add_tool_from_fn(f)
 
182
  with caplog.at_level(logging.WARNING):
183
- manager.add_tool_from_fn(f)
 
184
  assert "Tool already exists: f" not in caplog.text
185
 
186
  def test_error_on_duplicate_tools(self):
@@ -190,10 +205,12 @@ class TestAddTools:
190
  def test_fn(x: int) -> int:
191
  return x
192
 
193
- manager.add_tool_from_fn(test_fn, name="test_tool")
 
194
 
195
  with pytest.raises(ValueError, match="Tool already exists: test_tool"):
196
- manager.add_tool_from_fn(test_fn, name="test_tool")
 
197
 
198
  def test_replace_duplicate_tools(self):
199
  """Test replacing duplicate tools."""
@@ -203,12 +220,14 @@ class TestAddTools:
203
  return x
204
 
205
  def replacement_fn(x: int) -> int:
206
- return x * 2
207
 
208
- manager.add_tool_from_fn(original_fn, name="test_tool")
209
- manager.add_tool_from_fn(replacement_fn, name="test_tool")
 
 
210
 
211
- # Should have replaced with the new function
212
  tool = manager.get_tool("test_tool")
213
  assert tool is not None
214
  assert isinstance(tool, FunctionTool)
@@ -224,8 +243,10 @@ class TestAddTools:
224
  def replacement_fn(x: int) -> int:
225
  return x * 2
226
 
227
- manager.add_tool_from_fn(original_fn, name="test_tool")
228
- result = manager.add_tool_from_fn(replacement_fn, name="test_tool")
 
 
229
 
230
  # Should keep the original
231
  tool = manager.get_tool("test_tool")
@@ -234,7 +255,7 @@ class TestAddTools:
234
  assert tool.fn.__name__ == "original_fn"
235
  # Result should be the original tool
236
  assert isinstance(result, FunctionTool)
237
- assert result.fn.__name__ == "original_fn"
238
 
239
 
240
  class TestToolTags:
@@ -248,7 +269,8 @@ class TestToolTags:
248
  return x * 2
249
 
250
  manager = ToolManager()
251
- tool = manager.add_tool_from_fn(example_tool, tags={"math", "utility"})
 
252
 
253
  assert tool.tags == {"math", "utility"}
254
  tool = manager.get_tool("example_tool")
@@ -263,7 +285,8 @@ class TestToolTags:
263
  return x * 2
264
 
265
  manager = ToolManager()
266
- tool = manager.add_tool_from_fn(example_tool, tags=set())
 
267
 
268
  assert tool.tags == set()
269
 
@@ -275,7 +298,8 @@ class TestToolTags:
275
  return x * 2
276
 
277
  manager = ToolManager()
278
- tool = manager.add_tool_from_fn(example_tool, tags=None)
 
279
 
280
  assert tool.tags == set()
281
 
@@ -295,9 +319,12 @@ class TestToolTags:
295
  return str(x)
296
 
297
  manager = ToolManager()
298
- manager.add_tool_from_fn(math_tool, tags={"math"})
299
- manager.add_tool_from_fn(string_tool, tags={"string", "utility"})
300
- manager.add_tool_from_fn(mixed_tool, tags={"math", "utility", "string"})
 
 
 
301
 
302
  # Check if we can filter by tags when listing tools
303
  math_tools = [tool for tool in manager.list_tools() if "math" in tool.tags]
@@ -318,7 +345,8 @@ class TestCallTools:
318
  return a + b
319
 
320
  manager = ToolManager()
321
- manager.add_tool_from_fn(add)
 
322
  result = await manager.call_tool("add", {"a": 1, "b": 2})
323
 
324
  assert result[0].text == "3" # type: ignore[attr-defined]
@@ -329,7 +357,8 @@ class TestCallTools:
329
  return n * 2
330
 
331
  manager = ToolManager()
332
- manager.add_tool_from_fn(double)
 
333
  result = await manager.call_tool("double", {"n": 5})
334
  assert result[0].text == "10" # type: ignore[attr-defined]
335
 
@@ -342,7 +371,8 @@ class TestCallTools:
342
  return x + y
343
 
344
  manager = ToolManager()
345
- manager.add_tool_from_fn(Adder())
 
346
  result = await manager.call_tool("Adder", {"x": 1, "y": 2})
347
  assert result[0].text == "3" # type: ignore[attr-defined]
348
 
@@ -355,7 +385,8 @@ class TestCallTools:
355
  return x + y
356
 
357
  manager = ToolManager()
358
- manager.add_tool_from_fn(Adder())
 
359
  result = await manager.call_tool("Adder", {"x": 1, "y": 2})
360
  assert result[0].text == "3" # type: ignore[attr-defined]
361
 
@@ -365,7 +396,8 @@ class TestCallTools:
365
  return a + b
366
 
367
  manager = ToolManager()
368
- manager.add_tool_from_fn(add)
 
369
  result = await manager.call_tool("add", {"a": 1})
370
 
371
  assert result[0].text == "2" # type: ignore[attr-defined]
@@ -376,7 +408,8 @@ class TestCallTools:
376
  return a + b
377
 
378
  manager = ToolManager()
379
- manager.add_tool_from_fn(add)
 
380
  with pytest.raises(ToolError):
381
  await manager.call_tool("add", {"a": 1})
382
 
@@ -390,7 +423,8 @@ class TestCallTools:
390
  return sum(vals)
391
 
392
  manager = ToolManager()
393
- manager.add_tool_from_fn(sum_vals)
 
394
 
395
  result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
396
  assert result[0].text == "6" # type: ignore[attr-defined]
@@ -402,7 +436,8 @@ class TestCallTools:
402
  return sum(vals)
403
 
404
  manager = ToolManager()
405
- manager.add_tool_from_fn(sum_vals)
 
406
  # Try both with plain list and with JSON list
407
 
408
  with temporary_settings(tool_attempt_parse_json_args=True):
@@ -414,7 +449,8 @@ class TestCallTools:
414
  return vals if isinstance(vals, str) else "".join(vals)
415
 
416
  manager = ToolManager()
417
- manager.add_tool_from_fn(concat_strs)
 
418
 
419
  # Try both with plain python object and with JSON list
420
  result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]})
@@ -430,7 +466,8 @@ class TestCallTools:
430
  return vals if isinstance(vals, str) else "".join(vals)
431
 
432
  manager = ToolManager()
433
- manager.add_tool_from_fn(concat_strs)
 
434
 
435
  with temporary_settings(tool_attempt_parse_json_args=True):
436
  result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
@@ -451,7 +488,8 @@ class TestCallTools:
451
  return [x.name for x in tank.shrimp]
452
 
453
  manager = ToolManager()
454
- manager.add_tool_from_fn(name_shrimp)
 
455
 
456
  mcp = FastMCP()
457
  context = Context(fastmcp=mcp)
@@ -481,11 +519,10 @@ class TestCallTools:
481
  mcp = FastMCP(tool_serializer=custom_serializer)
482
  manager = mcp._tool_manager
483
 
 
484
  def get_data() -> dict:
485
  return {"key": "value", "number": 123}
486
 
487
- manager.add_tool_from_fn(get_data)
488
-
489
  result = await manager.call_tool("get_data", {})
490
  assert result[0].text == 'CUSTOM:{"key": "value", "number": 123}' # type: ignore[attr-defined]
491
 
@@ -500,14 +537,13 @@ class TestCallTools:
500
  mcp = FastMCP(tool_serializer=custom_serializer)
501
  manager = mcp._tool_manager
502
 
 
503
  def get_data() -> list[dict]:
504
  return [
505
  {"key": "value", "number": 123},
506
  {"key": "value2", "number": 456},
507
  ]
508
 
509
- manager.add_tool_from_fn(get_data)
510
-
511
  result = await manager.call_tool("get_data", {})
512
  assert (
513
  result[0].text # type: ignore[attr-defined]
@@ -525,11 +561,10 @@ class TestCallTools:
525
  mcp = FastMCP(tool_serializer=custom_serializer)
526
  manager = mcp._tool_manager
527
 
 
528
  def get_data() -> uuid.UUID:
529
  return uuid_result
530
 
531
- manager.add_tool_from_fn(get_data)
532
-
533
  result = await manager.call_tool("get_data", {})
534
  assert result[0].text == pydantic_core.to_json(uuid_result).decode() # type: ignore[attr-defined]
535
 
@@ -540,7 +575,8 @@ class TestToolSchema:
540
  return a
541
 
542
  manager = ToolManager()
543
- tool = manager.add_tool_from_fn(something)
 
544
  assert "ctx" not in json.dumps(tool.parameters)
545
  assert "Context" not in json.dumps(tool.parameters)
546
 
@@ -549,7 +585,8 @@ class TestToolSchema:
549
  return a
550
 
551
  manager = ToolManager()
552
- tool = manager.add_tool_from_fn(something)
 
553
  assert "ctx" not in json.dumps(tool.parameters)
554
  assert "Context" not in json.dumps(tool.parameters)
555
 
@@ -558,7 +595,8 @@ class TestToolSchema:
558
  return a
559
 
560
  manager = ToolManager()
561
- tool = manager.add_tool_from_fn(something)
 
562
  assert "ctx" not in json.dumps(tool.parameters)
563
  assert "Context" not in json.dumps(tool.parameters)
564
 
@@ -574,12 +612,13 @@ class TestContextHandling:
574
  return str(x)
575
 
576
  manager = ToolManager()
577
- manager.add_tool_from_fn(tool_with_context)
 
578
 
579
  def tool_without_context(x: int) -> str:
580
  return str(x)
581
 
582
- manager.add_tool_from_fn(tool_without_context)
583
 
584
  async def test_context_injection(self):
585
  """Test that context is properly injected during tool execution."""
@@ -589,7 +628,8 @@ class TestContextHandling:
589
  return str(x)
590
 
591
  manager = ToolManager()
592
- manager.add_tool_from_fn(tool_with_context)
 
593
 
594
  mcp = FastMCP()
595
  context = Context(fastmcp=mcp)
@@ -606,7 +646,8 @@ class TestContextHandling:
606
  return str(x)
607
 
608
  manager = ToolManager()
609
- manager.add_tool_from_fn(async_tool)
 
610
 
611
  mcp = FastMCP()
612
  context = Context(fastmcp=mcp)
@@ -622,7 +663,8 @@ class TestContextHandling:
622
  return x
623
 
624
  manager = ToolManager()
625
- manager.add_tool_from_fn(tool_with_context)
 
626
  # Should not raise an error when context is not provided
627
 
628
  mcp = FastMCP()
@@ -640,14 +682,16 @@ class TestContextHandling:
640
  return str(x)
641
 
642
  manager = ToolManager()
643
- manager.add_tool_from_fn(tool_with_context)
 
644
 
645
  def test_annotated_context_parameter_detection(self):
646
  def tool_with_context(x: int, ctx: Annotated[Context, "ctx"]) -> str:
647
  return str(x)
648
 
649
  manager = ToolManager()
650
- manager.add_tool_from_fn(tool_with_context)
 
651
 
652
  def test_parameterized_union_context_parameter_detection(self):
653
  """Test that context parameters are properly detected in
@@ -657,7 +701,8 @@ class TestContextHandling:
657
  return str(x)
658
 
659
  manager = ToolManager()
660
- manager.add_tool_from_fn(tool_with_context)
 
661
 
662
  async def test_context_error_handling(self):
663
  """Test error handling when context injection fails."""
@@ -666,7 +711,8 @@ class TestContextHandling:
666
  raise ValueError("Test error")
667
 
668
  manager = ToolManager()
669
- manager.add_tool_from_fn(tool_with_context)
 
670
 
671
  mcp = FastMCP()
672
  context = Context(fastmcp=mcp)
@@ -688,7 +734,8 @@ class TestCustomToolNames:
688
  return x * 2
689
 
690
  manager = ToolManager()
691
- tool = manager.add_tool_from_fn(original_fn, name="custom_name")
 
692
 
693
  # The tool is stored under the custom name and its .name is also set to custom_name
694
  assert manager.get_tool("custom_name") is not None
@@ -706,7 +753,7 @@ class TestCustomToolNames:
706
  return x + 1
707
 
708
  # Create a tool with a specific name
709
- tool = FunctionTool.from_function(fn, name="my_tool")
710
  manager = ToolManager()
711
  # Store it under a different name
712
  manager.add_tool(tool, key="proxy_tool")
@@ -727,7 +774,8 @@ class TestCustomToolNames:
727
  return a * b
728
 
729
  manager = ToolManager()
730
- manager.add_tool_from_fn(multiply, name="custom_multiply")
 
731
 
732
  # Tool should be callable by its custom name
733
  result = await manager.call_tool("custom_multiply", {"a": 5, "b": 3})
@@ -750,11 +798,13 @@ class TestCustomToolNames:
750
  manager = ToolManager(duplicate_behavior="replace")
751
 
752
  # Add the original tool
753
- original_tool = manager.add_tool_from_fn(original_fn, name="test_tool")
 
754
  assert original_tool.name == "test_tool"
755
 
756
  # Replace with a new function but keep the same registered name
757
- replacement_tool = manager.add_tool_from_fn(replacement_fn, name="test_tool")
 
758
 
759
  # The tool object should have been replaced
760
  stored_tool = manager.get_tool("test_tool")
@@ -780,7 +830,7 @@ class TestToolErrorHandling:
780
  """Tool that raises a ToolError."""
781
  raise ToolError("Specific tool error")
782
 
783
- manager.add_tool_from_fn(error_tool)
784
 
785
  with pytest.raises(ToolError, match="Specific tool error"):
786
  await manager.call_tool("error_tool", {"x": 42})
@@ -793,7 +843,7 @@ class TestToolErrorHandling:
793
  """Tool that raises a ValueError."""
794
  raise ValueError("Internal error details")
795
 
796
- manager.add_tool_from_fn(buggy_tool)
797
 
798
  with pytest.raises(ToolError) as excinfo:
799
  await manager.call_tool("buggy_tool", {"x": 42})
@@ -810,7 +860,7 @@ class TestToolErrorHandling:
810
  """Tool that raises a ValueError."""
811
  raise ValueError("Internal error details")
812
 
813
- manager.add_tool_from_fn(buggy_tool)
814
 
815
  with pytest.raises(ToolError) as excinfo:
816
  await manager.call_tool("buggy_tool", {"x": 42})
@@ -827,7 +877,7 @@ class TestToolErrorHandling:
827
  """Async tool that raises a ToolError."""
828
  raise ToolError("Async tool error")
829
 
830
- manager.add_tool_from_fn(async_error_tool)
831
 
832
  with pytest.raises(ToolError, match="Async tool error"):
833
  await manager.call_tool("async_error_tool", {"x": 42})
@@ -840,7 +890,7 @@ class TestToolErrorHandling:
840
  """Async tool that raises a ValueError."""
841
  raise ValueError("Internal async error details")
842
 
843
- manager.add_tool_from_fn(async_buggy_tool)
844
 
845
  with pytest.raises(ToolError) as excinfo:
846
  await manager.call_tool("async_buggy_tool", {"x": 42})
@@ -857,7 +907,7 @@ class TestToolErrorHandling:
857
  """Async tool that raises a ValueError."""
858
  raise ValueError("Internal async error details")
859
 
860
- manager.add_tool_from_fn(async_buggy_tool)
861
 
862
  with pytest.raises(ToolError) as excinfo:
863
  await manager.call_tool("async_buggy_tool", {"x": 42})
 
11
  from fastmcp import Context, FastMCP, Image
12
  from fastmcp.exceptions import NotFoundError, ToolError
13
  from fastmcp.tools import FunctionTool, ToolManager
14
+ from fastmcp.tools.tool import Tool
15
  from fastmcp.utilities.tests import temporary_settings
16
 
17
 
 
24
  return a + b
25
 
26
  manager = ToolManager()
27
+ tool = Tool.from_function(add)
28
+ manager.add_tool(tool)
29
 
30
  tool = manager.get_tool("add")
31
  assert tool is not None
 
42
  return f"Data from {url}"
43
 
44
  manager = ToolManager()
45
+ tool = Tool.from_function(fetch_data)
46
+ manager.add_tool(tool)
47
 
48
  tool = manager.get_tool("fetch_data")
49
  assert tool is not None
 
63
  return {"id": 1, **user.model_dump()}
64
 
65
  manager = ToolManager()
66
+ tool = Tool.from_function(create_user)
67
+ manager.add_tool(tool)
68
 
69
  tool = manager.get_tool("create_user")
70
  assert tool is not None
 
83
  return x + y
84
 
85
  manager = ToolManager()
86
+ tool = Tool.from_function(Adder())
87
+ manager.add_tool(tool)
88
 
89
  tool = manager.get_tool("Adder")
90
  assert tool is not None
 
103
  return x + y
104
 
105
  manager = ToolManager()
106
+ tool = Tool.from_function(Adder())
107
+ manager.add_tool(tool)
108
 
109
  tool = manager.get_tool("Adder")
110
  assert tool is not None
 
119
  return Image(data=data)
120
 
121
  manager = ToolManager()
122
+ tool = Tool.from_function(image_tool)
123
+ manager.add_tool(tool)
124
 
125
  tool = manager.get_tool("image_tool")
126
  result = await tool.run({"data": "test.png"})
 
130
  def test_add_noncallable_tool(self):
131
  manager = ToolManager()
132
  with pytest.raises(TypeError, match="not a callable object"):
133
+ tool = Tool.from_function(1) # type: ignore
134
+ manager.add_tool(tool)
135
 
136
  def test_add_lambda(self):
137
  manager = ToolManager()
138
+ tool = Tool.from_function(lambda x: x, name="my_tool")
139
+ manager.add_tool(tool)
140
  assert tool.name == "my_tool"
141
 
142
  def test_add_lambda_with_no_name(self):
 
144
  with pytest.raises(
145
  ValueError, match="You must provide a name for lambda functions"
146
  ):
147
+ tool = Tool.from_function(lambda x: x)
148
+ manager.add_tool(tool)
149
 
150
  def test_remove_tool_successfully(self):
151
  """Test removing an added tool by key."""
 
154
  def add(a: int, b: int) -> int:
155
  return a + b
156
 
157
+ tool = Tool.from_function(add)
158
+ manager.add_tool(tool)
159
  assert manager.get_tool("add") is not None
160
 
161
  manager.remove_tool("add")
 
175
  def test_fn(x: int) -> int:
176
  return x
177
 
178
+ tool1 = Tool.from_function(test_fn, name="test_tool")
179
+ manager.add_tool(tool1)
180
+ tool2 = Tool.from_function(test_fn, name="test_tool")
181
+ manager.add_tool(tool2)
182
 
183
  assert "Tool already exists: test_tool" in caplog.text
184
  # Should have the tool
 
191
  return x
192
 
193
  manager = ToolManager(duplicate_behavior="ignore")
194
+ tool1 = Tool.from_function(f)
195
+ manager.add_tool(tool1)
196
  with caplog.at_level(logging.WARNING):
197
+ tool2 = Tool.from_function(f)
198
+ manager.add_tool(tool2)
199
  assert "Tool already exists: f" not in caplog.text
200
 
201
  def test_error_on_duplicate_tools(self):
 
205
  def test_fn(x: int) -> int:
206
  return x
207
 
208
+ tool1 = Tool.from_function(test_fn, name="test_tool")
209
+ manager.add_tool(tool1)
210
 
211
  with pytest.raises(ValueError, match="Tool already exists: test_tool"):
212
+ tool2 = Tool.from_function(test_fn, name="test_tool")
213
+ manager.add_tool(tool2)
214
 
215
  def test_replace_duplicate_tools(self):
216
  """Test replacing duplicate tools."""
 
220
  return x
221
 
222
  def replacement_fn(x: int) -> int:
223
+ return x + 1
224
 
225
+ tool1 = Tool.from_function(original_fn, name="test_tool")
226
+ manager.add_tool(tool1)
227
+ result = Tool.from_function(replacement_fn, name="test_tool")
228
+ manager.add_tool(result)
229
 
230
+ # Should have replaced with the new tool
231
  tool = manager.get_tool("test_tool")
232
  assert tool is not None
233
  assert isinstance(tool, FunctionTool)
 
243
  def replacement_fn(x: int) -> int:
244
  return x * 2
245
 
246
+ tool1 = Tool.from_function(original_fn, name="test_tool")
247
+ manager.add_tool(tool1)
248
+ result = Tool.from_function(replacement_fn, name="test_tool")
249
+ manager.add_tool(result)
250
 
251
  # Should keep the original
252
  tool = manager.get_tool("test_tool")
 
255
  assert tool.fn.__name__ == "original_fn"
256
  # Result should be the original tool
257
  assert isinstance(result, FunctionTool)
258
+ assert result.fn.__name__ == "replacement_fn"
259
 
260
 
261
  class TestToolTags:
 
269
  return x * 2
270
 
271
  manager = ToolManager()
272
+ tool = Tool.from_function(example_tool, tags={"math", "utility"})
273
+ manager.add_tool(tool)
274
 
275
  assert tool.tags == {"math", "utility"}
276
  tool = manager.get_tool("example_tool")
 
285
  return x * 2
286
 
287
  manager = ToolManager()
288
+ tool = Tool.from_function(example_tool, tags=set())
289
+ manager.add_tool(tool)
290
 
291
  assert tool.tags == set()
292
 
 
298
  return x * 2
299
 
300
  manager = ToolManager()
301
+ tool = Tool.from_function(example_tool, tags=None)
302
+ manager.add_tool(tool)
303
 
304
  assert tool.tags == set()
305
 
 
319
  return str(x)
320
 
321
  manager = ToolManager()
322
+ tool1 = Tool.from_function(math_tool, tags={"math"})
323
+ manager.add_tool(tool1)
324
+ tool2 = Tool.from_function(string_tool, tags={"string", "utility"})
325
+ manager.add_tool(tool2)
326
+ tool3 = Tool.from_function(mixed_tool, tags={"math", "utility", "string"})
327
+ manager.add_tool(tool3)
328
 
329
  # Check if we can filter by tags when listing tools
330
  math_tools = [tool for tool in manager.list_tools() if "math" in tool.tags]
 
345
  return a + b
346
 
347
  manager = ToolManager()
348
+ tool = Tool.from_function(add)
349
+ manager.add_tool(tool)
350
  result = await manager.call_tool("add", {"a": 1, "b": 2})
351
 
352
  assert result[0].text == "3" # type: ignore[attr-defined]
 
357
  return n * 2
358
 
359
  manager = ToolManager()
360
+ tool = Tool.from_function(double)
361
+ manager.add_tool(tool)
362
  result = await manager.call_tool("double", {"n": 5})
363
  assert result[0].text == "10" # type: ignore[attr-defined]
364
 
 
371
  return x + y
372
 
373
  manager = ToolManager()
374
+ tool = Tool.from_function(Adder())
375
+ manager.add_tool(tool)
376
  result = await manager.call_tool("Adder", {"x": 1, "y": 2})
377
  assert result[0].text == "3" # type: ignore[attr-defined]
378
 
 
385
  return x + y
386
 
387
  manager = ToolManager()
388
+ tool = Tool.from_function(Adder())
389
+ manager.add_tool(tool)
390
  result = await manager.call_tool("Adder", {"x": 1, "y": 2})
391
  assert result[0].text == "3" # type: ignore[attr-defined]
392
 
 
396
  return a + b
397
 
398
  manager = ToolManager()
399
+ tool = Tool.from_function(add)
400
+ manager.add_tool(tool)
401
  result = await manager.call_tool("add", {"a": 1})
402
 
403
  assert result[0].text == "2" # type: ignore[attr-defined]
 
408
  return a + b
409
 
410
  manager = ToolManager()
411
+ tool = Tool.from_function(add)
412
+ manager.add_tool(tool)
413
  with pytest.raises(ToolError):
414
  await manager.call_tool("add", {"a": 1})
415
 
 
423
  return sum(vals)
424
 
425
  manager = ToolManager()
426
+ tool = Tool.from_function(sum_vals)
427
+ manager.add_tool(tool)
428
 
429
  result = await manager.call_tool("sum_vals", {"vals": [1, 2, 3]})
430
  assert result[0].text == "6" # type: ignore[attr-defined]
 
436
  return sum(vals)
437
 
438
  manager = ToolManager()
439
+ tool = Tool.from_function(sum_vals)
440
+ manager.add_tool(tool)
441
  # Try both with plain list and with JSON list
442
 
443
  with temporary_settings(tool_attempt_parse_json_args=True):
 
449
  return vals if isinstance(vals, str) else "".join(vals)
450
 
451
  manager = ToolManager()
452
+ tool = Tool.from_function(concat_strs)
453
+ manager.add_tool(tool)
454
 
455
  # Try both with plain python object and with JSON list
456
  result = await manager.call_tool("concat_strs", {"vals": ["a", "b", "c"]})
 
466
  return vals if isinstance(vals, str) else "".join(vals)
467
 
468
  manager = ToolManager()
469
+ tool = Tool.from_function(concat_strs)
470
+ manager.add_tool(tool)
471
 
472
  with temporary_settings(tool_attempt_parse_json_args=True):
473
  result = await manager.call_tool("concat_strs", {"vals": '["a", "b", "c"]'})
 
488
  return [x.name for x in tank.shrimp]
489
 
490
  manager = ToolManager()
491
+ tool = Tool.from_function(name_shrimp)
492
+ manager.add_tool(tool)
493
 
494
  mcp = FastMCP()
495
  context = Context(fastmcp=mcp)
 
519
  mcp = FastMCP(tool_serializer=custom_serializer)
520
  manager = mcp._tool_manager
521
 
522
+ @mcp.tool()
523
  def get_data() -> dict:
524
  return {"key": "value", "number": 123}
525
 
 
 
526
  result = await manager.call_tool("get_data", {})
527
  assert result[0].text == 'CUSTOM:{"key": "value", "number": 123}' # type: ignore[attr-defined]
528
 
 
537
  mcp = FastMCP(tool_serializer=custom_serializer)
538
  manager = mcp._tool_manager
539
 
540
+ @mcp.tool()
541
  def get_data() -> list[dict]:
542
  return [
543
  {"key": "value", "number": 123},
544
  {"key": "value2", "number": 456},
545
  ]
546
 
 
 
547
  result = await manager.call_tool("get_data", {})
548
  assert (
549
  result[0].text # type: ignore[attr-defined]
 
561
  mcp = FastMCP(tool_serializer=custom_serializer)
562
  manager = mcp._tool_manager
563
 
564
+ @mcp.tool()
565
  def get_data() -> uuid.UUID:
566
  return uuid_result
567
 
 
 
568
  result = await manager.call_tool("get_data", {})
569
  assert result[0].text == pydantic_core.to_json(uuid_result).decode() # type: ignore[attr-defined]
570
 
 
575
  return a
576
 
577
  manager = ToolManager()
578
+ tool = Tool.from_function(something)
579
+ manager.add_tool(tool)
580
  assert "ctx" not in json.dumps(tool.parameters)
581
  assert "Context" not in json.dumps(tool.parameters)
582
 
 
585
  return a
586
 
587
  manager = ToolManager()
588
+ tool = Tool.from_function(something)
589
+ manager.add_tool(tool)
590
  assert "ctx" not in json.dumps(tool.parameters)
591
  assert "Context" not in json.dumps(tool.parameters)
592
 
 
595
  return a
596
 
597
  manager = ToolManager()
598
+ tool = Tool.from_function(something)
599
+ manager.add_tool(tool)
600
  assert "ctx" not in json.dumps(tool.parameters)
601
  assert "Context" not in json.dumps(tool.parameters)
602
 
 
612
  return str(x)
613
 
614
  manager = ToolManager()
615
+ tool = Tool.from_function(tool_with_context)
616
+ manager.add_tool(tool)
617
 
618
  def tool_without_context(x: int) -> str:
619
  return str(x)
620
 
621
+ manager.add_tool(Tool.from_function(tool_without_context))
622
 
623
  async def test_context_injection(self):
624
  """Test that context is properly injected during tool execution."""
 
628
  return str(x)
629
 
630
  manager = ToolManager()
631
+ tool = Tool.from_function(tool_with_context)
632
+ manager.add_tool(tool)
633
 
634
  mcp = FastMCP()
635
  context = Context(fastmcp=mcp)
 
646
  return str(x)
647
 
648
  manager = ToolManager()
649
+ tool = Tool.from_function(async_tool)
650
+ manager.add_tool(tool)
651
 
652
  mcp = FastMCP()
653
  context = Context(fastmcp=mcp)
 
663
  return x
664
 
665
  manager = ToolManager()
666
+ tool = Tool.from_function(tool_with_context)
667
+ manager.add_tool(tool)
668
  # Should not raise an error when context is not provided
669
 
670
  mcp = FastMCP()
 
682
  return str(x)
683
 
684
  manager = ToolManager()
685
+ tool = Tool.from_function(tool_with_context)
686
+ manager.add_tool(tool)
687
 
688
  def test_annotated_context_parameter_detection(self):
689
  def tool_with_context(x: int, ctx: Annotated[Context, "ctx"]) -> str:
690
  return str(x)
691
 
692
  manager = ToolManager()
693
+ tool = Tool.from_function(tool_with_context)
694
+ manager.add_tool(tool)
695
 
696
  def test_parameterized_union_context_parameter_detection(self):
697
  """Test that context parameters are properly detected in
 
701
  return str(x)
702
 
703
  manager = ToolManager()
704
+ tool = Tool.from_function(tool_with_context)
705
+ manager.add_tool(tool)
706
 
707
  async def test_context_error_handling(self):
708
  """Test error handling when context injection fails."""
 
711
  raise ValueError("Test error")
712
 
713
  manager = ToolManager()
714
+ tool = Tool.from_function(tool_with_context)
715
+ manager.add_tool(tool)
716
 
717
  mcp = FastMCP()
718
  context = Context(fastmcp=mcp)
 
734
  return x * 2
735
 
736
  manager = ToolManager()
737
+ tool = Tool.from_function(original_fn, name="custom_name")
738
+ manager.add_tool(tool)
739
 
740
  # The tool is stored under the custom name and its .name is also set to custom_name
741
  assert manager.get_tool("custom_name") is not None
 
753
  return x + 1
754
 
755
  # Create a tool with a specific name
756
+ tool = Tool.from_function(fn, name="my_tool")
757
  manager = ToolManager()
758
  # Store it under a different name
759
  manager.add_tool(tool, key="proxy_tool")
 
774
  return a * b
775
 
776
  manager = ToolManager()
777
+ tool = Tool.from_function(multiply, name="custom_multiply")
778
+ manager.add_tool(tool)
779
 
780
  # Tool should be callable by its custom name
781
  result = await manager.call_tool("custom_multiply", {"a": 5, "b": 3})
 
798
  manager = ToolManager(duplicate_behavior="replace")
799
 
800
  # Add the original tool
801
+ original_tool = Tool.from_function(original_fn, name="test_tool")
802
+ manager.add_tool(original_tool)
803
  assert original_tool.name == "test_tool"
804
 
805
  # Replace with a new function but keep the same registered name
806
+ replacement_tool = Tool.from_function(replacement_fn, name="test_tool")
807
+ manager.add_tool(replacement_tool)
808
 
809
  # The tool object should have been replaced
810
  stored_tool = manager.get_tool("test_tool")
 
830
  """Tool that raises a ToolError."""
831
  raise ToolError("Specific tool error")
832
 
833
+ manager.add_tool(Tool.from_function(error_tool))
834
 
835
  with pytest.raises(ToolError, match="Specific tool error"):
836
  await manager.call_tool("error_tool", {"x": 42})
 
843
  """Tool that raises a ValueError."""
844
  raise ValueError("Internal error details")
845
 
846
+ manager.add_tool(Tool.from_function(buggy_tool))
847
 
848
  with pytest.raises(ToolError) as excinfo:
849
  await manager.call_tool("buggy_tool", {"x": 42})
 
860
  """Tool that raises a ValueError."""
861
  raise ValueError("Internal error details")
862
 
863
+ manager.add_tool(Tool.from_function(buggy_tool))
864
 
865
  with pytest.raises(ToolError) as excinfo:
866
  await manager.call_tool("buggy_tool", {"x": 42})
 
877
  """Async tool that raises a ToolError."""
878
  raise ToolError("Async tool error")
879
 
880
+ manager.add_tool(Tool.from_function(async_error_tool))
881
 
882
  with pytest.raises(ToolError, match="Async tool error"):
883
  await manager.call_tool("async_error_tool", {"x": 42})
 
890
  """Async tool that raises a ValueError."""
891
  raise ValueError("Internal async error details")
892
 
893
+ manager.add_tool(Tool.from_function(async_buggy_tool))
894
 
895
  with pytest.raises(ToolError) as excinfo:
896
  await manager.call_tool("async_buggy_tool", {"x": 42})
 
907
  """Async tool that raises a ValueError."""
908
  raise ValueError("Internal async error details")
909
 
910
+ manager.add_tool(Tool.from_function(async_buggy_tool))
911
 
912
  with pytest.raises(ToolError) as excinfo:
913
  await manager.call_tool("async_buggy_tool", {"x": 42})