Jeremiah Lowin commited on
Commit
9866fde
·
unverified ·
2 Parent(s): 2d031b9dd6c43f

Merge pull request #705 from jlowin/object-functions

Browse files

Deprecate passing functions to the server in favor of core objects

.github/workflows/run-tests.yml CHANGED
@@ -47,4 +47,4 @@ jobs:
47
  run: uv sync --locked
48
 
49
  - name: Run tests
50
- run: uv run pytest tests
 
47
  run: uv sync --locked
48
 
49
  - name: Run tests
50
+ run: uv run pytest tests -n auto
pyproject.toml CHANGED
@@ -86,7 +86,7 @@ fallback-version = "0.0.0"
86
  asyncio_mode = "auto"
87
  asyncio_default_fixture_loop_scope = "session"
88
  asyncio_default_test_loop_scope = "session"
89
- filterwarnings = []
90
  timeout = 3
91
  env = [
92
  "FASTMCP_TEST_MODE=1",
 
86
  asyncio_mode = "auto"
87
  asyncio_default_fixture_loop_scope = "session"
88
  asyncio_default_test_loop_scope = "session"
89
+ # filterwarnings = ["error::DeprecationWarning"]
90
  timeout = 3
91
  env = [
92
  "FASTMCP_TEST_MODE=1",
src/fastmcp/contrib/mcp_mixin/mcp_mixin.py CHANGED
@@ -3,6 +3,10 @@
3
  from collections.abc import Callable
4
  from typing import TYPE_CHECKING, Any
5
 
 
 
 
 
6
  if TYPE_CHECKING:
7
  from fastmcp.server import FastMCP
8
 
@@ -128,7 +132,8 @@ class MCPMixin:
128
  registration_info["name"] = (
129
  f"{prefix}{separator}{registration_info['name']}"
130
  )
131
- mcp_server.add_tool(fn=method, **registration_info)
 
132
 
133
  def register_resources(
134
  self,
@@ -156,7 +161,8 @@ class MCPMixin:
156
  registration_info["uri"] = (
157
  f"{prefix}{separator}{registration_info['uri']}"
158
  )
159
- mcp_server.add_resource_fn(fn=method, **registration_info)
 
160
 
161
  def register_prompts(
162
  self,
@@ -180,7 +186,8 @@ class MCPMixin:
180
  registration_info["name"] = (
181
  f"{prefix}{separator}{registration_info['name']}"
182
  )
183
- mcp_server.add_prompt(fn=method, **registration_info)
 
184
 
185
  def register_all(
186
  self,
 
3
  from collections.abc import Callable
4
  from typing import TYPE_CHECKING, Any
5
 
6
+ from fastmcp.prompts.prompt import Prompt
7
+ from fastmcp.resources.resource import Resource
8
+ from fastmcp.tools.tool import Tool
9
+
10
  if TYPE_CHECKING:
11
  from fastmcp.server import FastMCP
12
 
 
132
  registration_info["name"] = (
133
  f"{prefix}{separator}{registration_info['name']}"
134
  )
135
+ tool = Tool.from_function(fn=method, **registration_info)
136
+ mcp_server.add_tool(tool)
137
 
138
  def register_resources(
139
  self,
 
161
  registration_info["uri"] = (
162
  f"{prefix}{separator}{registration_info['uri']}"
163
  )
164
+ resource = Resource.from_function(fn=method, **registration_info)
165
+ mcp_server.add_resource(resource)
166
 
167
  def register_prompts(
168
  self,
 
186
  registration_info["name"] = (
187
  f"{prefix}{separator}{registration_info['name']}"
188
  )
189
+ prompt = Prompt.from_function(fn=method, **registration_info)
190
+ mcp_server.add_prompt(prompt)
191
 
192
  def register_all(
193
  self,
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -1,7 +1,6 @@
1
- """Prompt management functionality."""
2
-
3
  from __future__ import annotations as _annotations
4
 
 
5
  from collections.abc import Awaitable, Callable
6
  from typing import TYPE_CHECKING, Any
7
 
@@ -57,6 +56,11 @@ class PromptManager:
57
  tags: set[str] | None = None,
58
  ) -> FunctionPrompt:
59
  """Create a prompt from a function."""
 
 
 
 
 
60
  prompt = FunctionPrompt.from_function(
61
  fn, name=name, description=description, tags=tags
62
  )
 
 
 
1
  from __future__ import annotations as _annotations
2
 
3
+ import warnings
4
  from collections.abc import Awaitable, Callable
5
  from typing import TYPE_CHECKING, Any
6
 
 
56
  tags: set[str] | None = None,
57
  ) -> FunctionPrompt:
58
  """Create a prompt from a function."""
59
+ # deprecated in 2.7.0
60
+ warnings.warn(
61
+ "PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.",
62
+ DeprecationWarning,
63
+ )
64
  prompt = FunctionPrompt.from_function(
65
  fn, name=name, description=description, tags=tags
66
  )
src/fastmcp/resources/resource_manager.py CHANGED
@@ -1,6 +1,7 @@
1
  """Resource manager functionality."""
2
 
3
  import inspect
 
4
  from collections.abc import Callable
5
  from typing import Any
6
 
@@ -120,6 +121,11 @@ class ResourceManager:
120
  The added resource. If a resource with the same URI already exists,
121
  returns the existing resource.
122
  """
 
 
 
 
 
123
  resource = Resource.from_function(
124
  fn=fn,
125
  uri=uri,
@@ -171,7 +177,11 @@ class ResourceManager:
171
  tags: set[str] | None = None,
172
  ) -> ResourceTemplate:
173
  """Create a template from a function."""
174
-
 
 
 
 
175
  template = ResourceTemplate.from_function(
176
  fn,
177
  uri_template=uri_template,
 
1
  """Resource manager functionality."""
2
 
3
  import inspect
4
+ import warnings
5
  from collections.abc import Callable
6
  from typing import Any
7
 
 
121
  The added resource. If a resource with the same URI already exists,
122
  returns the existing resource.
123
  """
124
+ # deprecated in 2.7.0
125
+ warnings.warn(
126
+ "add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
127
+ DeprecationWarning,
128
+ )
129
  resource = Resource.from_function(
130
  fn=fn,
131
  uri=uri,
 
177
  tags: set[str] | None = None,
178
  ) -> ResourceTemplate:
179
  """Create a template from a function."""
180
+ # deprecated in 2.7.0
181
+ warnings.warn(
182
+ "add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
183
+ DeprecationWarning,
184
+ )
185
  template = ResourceTemplate.from_function(
186
  fn,
187
  uri_template=uri_template,
src/fastmcp/server/server.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations
4
 
5
  import datetime
 
6
  import re
7
  import warnings
8
  from collections.abc import AsyncIterator, Awaitable, Callable
@@ -44,7 +45,6 @@ import fastmcp.server
44
  import fastmcp.settings
45
  from fastmcp.exceptions import NotFoundError
46
  from fastmcp.prompts import Prompt, PromptManager
47
- from fastmcp.prompts.prompt import PromptResult
48
  from fastmcp.resources import Resource, ResourceManager
49
  from fastmcp.resources.template import ResourceTemplate
50
  from fastmcp.server.auth.auth import OAuthProvider
@@ -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
@@ -183,10 +183,9 @@ class FastMCP(Generic[LifespanResultT]):
183
 
184
  if tools:
185
  for tool in tools:
186
- if isinstance(tool, Tool):
187
- self._tool_manager.add_tool(tool)
188
- else:
189
- self.add_tool(tool)
190
 
191
  # Set up MCP protocol handlers
192
  self._setup_handlers()
@@ -349,18 +348,18 @@ class FastMCP(Generic[LifespanResultT]):
349
  """
350
 
351
  def decorator(
352
- func: Callable[[Request], Awaitable[Response]],
353
  ) -> Callable[[Request], Awaitable[Response]]:
354
  self._additional_http_routes.append(
355
  Route(
356
  path,
357
- endpoint=func,
358
  methods=methods,
359
  name=name,
360
  include_in_schema=include_in_schema,
361
  )
362
  )
363
- return func
364
 
365
  return decorator
366
 
@@ -484,15 +483,7 @@ class FastMCP(Generic[LifespanResultT]):
484
 
485
  raise NotFoundError(f"Unknown prompt: {name}")
486
 
487
- def add_tool(
488
- self,
489
- fn: AnyFunction,
490
- name: str | None = None,
491
- description: str | None = None,
492
- tags: set[str] | None = None,
493
- annotations: ToolAnnotations | dict[str, Any] | None = None,
494
- exclude_args: list[str] | None = None,
495
- ) -> None:
496
  """Add a tool to the server.
497
 
498
  The tool function can optionally request a Context object by adding a parameter
@@ -505,18 +496,6 @@ class FastMCP(Generic[LifespanResultT]):
505
  tags: Optional set of tags for categorizing the tool
506
  annotations: Optional annotations about the tool's behavior
507
  """
508
- if isinstance(annotations, dict):
509
- annotations = ToolAnnotations(**annotations)
510
-
511
- tool = Tool.from_function(
512
- fn,
513
- name=name,
514
- description=description,
515
- tags=tags,
516
- annotations=annotations,
517
- exclude_args=exclude_args,
518
- )
519
-
520
  self._tool_manager.add_tool(tool)
521
  self._cache.clear()
522
 
@@ -574,16 +553,20 @@ class FastMCP(Generic[LifespanResultT]):
574
  "The @tool decorator was used incorrectly. "
575
  "Did you forget to call it? Use @tool() instead of @tool"
576
  )
 
 
577
 
578
  def decorator(fn: AnyFunction) -> AnyFunction:
579
- self.add_tool(
580
  fn,
581
  name=name,
582
  description=description,
583
  tags=tags,
584
  annotations=annotations,
585
  exclude_args=exclude_args,
 
586
  )
 
587
  return fn
588
 
589
  return decorator
@@ -598,6 +581,14 @@ class FastMCP(Generic[LifespanResultT]):
598
  self._resource_manager.add_resource(resource, key=key)
599
  self._cache.clear()
600
 
 
 
 
 
 
 
 
 
601
  def add_resource_fn(
602
  self,
603
  fn: AnyFunction,
@@ -620,6 +611,12 @@ class FastMCP(Generic[LifespanResultT]):
620
  mime_type: Optional MIME type for the resource
621
  tags: Optional set of tags for categorizing the resource
622
  """
 
 
 
 
 
 
623
  self._resource_manager.add_resource_or_template_from_fn(
624
  fn=fn,
625
  uri=uri,
@@ -693,36 +690,54 @@ class FastMCP(Generic[LifespanResultT]):
693
  )
694
 
695
  def decorator(fn: AnyFunction) -> AnyFunction:
696
- self.add_resource_fn(
697
- fn=fn,
698
- uri=uri,
699
- name=name,
700
- description=description,
701
- mime_type=mime_type,
702
- tags=tags,
 
 
703
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
704
  return fn
705
 
706
  return decorator
707
 
708
- def add_prompt(
709
- self,
710
- fn: Callable[..., PromptResult | Awaitable[PromptResult]],
711
- name: str | None = None,
712
- description: str | None = None,
713
- tags: set[str] | None = None,
714
- ) -> None:
715
  """Add a prompt to the server.
716
 
717
  Args:
718
  prompt: A Prompt instance to add
719
  """
720
- self._prompt_manager.add_prompt_from_fn(
721
- fn=fn,
722
- name=name,
723
- description=description,
724
- tags=tags,
725
- )
726
  self._cache.clear()
727
 
728
  def prompt(
@@ -787,9 +802,16 @@ class FastMCP(Generic[LifespanResultT]):
787
  "Did you forget to call it? Use @prompt() instead of @prompt"
788
  )
789
 
790
- def decorator(func: AnyFunction) -> AnyFunction:
791
- self.add_prompt(func, name=name, description=description, tags=tags)
792
- return DecoratedFunction(func)
 
 
 
 
 
 
 
793
 
794
  return decorator
795
 
 
3
  from __future__ import annotations
4
 
5
  import datetime
6
+ import inspect
7
  import re
8
  import warnings
9
  from collections.abc import AsyncIterator, Awaitable, Callable
 
45
  import fastmcp.settings
46
  from fastmcp.exceptions import NotFoundError
47
  from fastmcp.prompts import Prompt, PromptManager
 
48
  from fastmcp.resources import Resource, ResourceManager
49
  from fastmcp.resources.template import ResourceTemplate
50
  from fastmcp.server.auth.auth import OAuthProvider
 
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
 
183
 
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
191
  self._setup_handlers()
 
348
  """
349
 
350
  def decorator(
351
+ fn: Callable[[Request], Awaitable[Response]],
352
  ) -> Callable[[Request], Awaitable[Response]]:
353
  self._additional_http_routes.append(
354
  Route(
355
  path,
356
+ endpoint=fn,
357
  methods=methods,
358
  name=name,
359
  include_in_schema=include_in_schema,
360
  )
361
  )
362
+ return fn
363
 
364
  return decorator
365
 
 
483
 
484
  raise NotFoundError(f"Unknown prompt: {name}")
485
 
486
+ def add_tool(self, tool: Tool) -> None:
 
 
 
 
 
 
 
 
487
  """Add a tool to the server.
488
 
489
  The tool function can optionally request a Context object by adding a parameter
 
496
  tags: Optional set of tags for categorizing the tool
497
  annotations: Optional annotations about the tool's behavior
498
  """
 
 
 
 
 
 
 
 
 
 
 
 
499
  self._tool_manager.add_tool(tool)
500
  self._cache.clear()
501
 
 
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,
566
  exclude_args=exclude_args,
567
+ serializer=self._tool_serializer,
568
  )
569
+ self.add_tool(tool)
570
  return fn
571
 
572
  return decorator
 
581
  self._resource_manager.add_resource(resource, key=key)
582
  self._cache.clear()
583
 
584
+ def add_template(self, template: ResourceTemplate, key: str | None = None) -> None:
585
+ """Add a resource template to the server.
586
+
587
+ Args:
588
+ template: A ResourceTemplate instance to add
589
+ """
590
+ self._resource_manager.add_template(template, key=key)
591
+
592
  def add_resource_fn(
593
  self,
594
  fn: AnyFunction,
 
611
  mime_type: Optional MIME type for the resource
612
  tags: Optional set of tags for categorizing the resource
613
  """
614
+ # deprecated since 2.7.0
615
+ warnings.warn(
616
+ "The add_resource_fn method is deprecated. Use the resource decorator instead.",
617
+ DeprecationWarning,
618
+ stacklevel=2,
619
+ )
620
  self._resource_manager.add_resource_or_template_from_fn(
621
  fn=fn,
622
  uri=uri,
 
690
  )
691
 
692
  def decorator(fn: AnyFunction) -> AnyFunction:
693
+ from fastmcp.server.context import Context
694
+
695
+ # Check if this should be a template
696
+ has_uri_params = "{" in uri and "}" in uri
697
+ # check if the function has any parameters (other than injected context)
698
+ has_func_params = any(
699
+ p
700
+ for p in inspect.signature(fn).parameters.values()
701
+ if p.annotation is not Context
702
  )
703
+
704
+ if has_uri_params or has_func_params:
705
+ template = ResourceTemplate.from_function(
706
+ fn=fn,
707
+ uri_template=uri,
708
+ name=name,
709
+ description=description,
710
+ mime_type=mime_type,
711
+ tags=tags,
712
+ )
713
+ self.add_template(template)
714
+ elif not has_uri_params and not has_func_params:
715
+ resource = Resource.from_function(
716
+ fn=fn,
717
+ uri=uri,
718
+ name=name,
719
+ description=description,
720
+ mime_type=mime_type,
721
+ tags=tags,
722
+ )
723
+ self.add_resource(resource)
724
+ else:
725
+ raise ValueError(
726
+ "Invalid resource or template definition due to a "
727
+ "mismatch between URI parameters and function parameters."
728
+ )
729
+
730
  return fn
731
 
732
  return decorator
733
 
734
+ def add_prompt(self, prompt: Prompt) -> None:
 
 
 
 
 
 
735
  """Add a prompt to the server.
736
 
737
  Args:
738
  prompt: A Prompt instance to add
739
  """
740
+ self._prompt_manager.add_prompt(prompt)
 
 
 
 
 
741
  self._cache.clear()
742
 
743
  def prompt(
 
802
  "Did you forget to call it? Use @prompt() instead of @prompt"
803
  )
804
 
805
+ def decorator(fn: AnyFunction) -> AnyFunction:
806
+ prompt = Prompt.from_function(
807
+ fn=fn,
808
+ name=name,
809
+ description=description,
810
+ tags=tags,
811
+ )
812
+
813
+ self.add_prompt(prompt)
814
+ return DecoratedFunction(fn)
815
 
816
  return decorator
817
 
src/fastmcp/tools/tool_manager.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations as _annotations
2
 
 
3
  from collections.abc import Callable
4
  from typing import TYPE_CHECKING, Any
5
 
@@ -22,11 +23,9 @@ class ToolManager:
22
  def __init__(
23
  self,
24
  duplicate_behavior: DuplicateBehavior | None = None,
25
- serializer: Callable[[Any], str] | None = None,
26
  mask_error_details: bool = False,
27
  ):
28
  self._tools: dict[str, Tool] = {}
29
- self._serializer = serializer
30
  self.mask_error_details = mask_error_details
31
 
32
  # Default to "warn" if None is provided
@@ -66,17 +65,23 @@ class ToolManager:
66
  description: str | None = None,
67
  tags: set[str] | None = None,
68
  annotations: ToolAnnotations | None = None,
 
69
  exclude_args: list[str] | None = None,
70
  ) -> Tool:
71
  """Add a tool to the server."""
 
 
 
 
 
72
  tool = Tool.from_function(
73
  fn,
74
  name=name,
75
  description=description,
76
  tags=tags,
77
  annotations=annotations,
78
- serializer=self._serializer,
79
  exclude_args=exclude_args,
 
80
  )
81
  return self.add_tool(tool)
82
 
 
1
  from __future__ import annotations as _annotations
2
 
3
+ import warnings
4
  from collections.abc import Callable
5
  from typing import TYPE_CHECKING, Any
6
 
 
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."""
72
+ # deprecated in 2.7.0
73
+ warnings.warn(
74
+ "ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.",
75
+ DeprecationWarning,
76
+ )
77
  tool = Tool.from_function(
78
  fn,
79
  name=name,
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/contrib/test_bulk_tool_caller.py CHANGED
@@ -9,6 +9,7 @@ from fastmcp.contrib.bulk_tool_caller.bulk_tool_caller import (
9
  CallToolRequest,
10
  CallToolRequestResult,
11
  )
 
12
 
13
  ContentType = TextContent | ImageContent | EmbeddedResource
14
 
@@ -68,9 +69,9 @@ def no_return_tool_result_factory(arg1: str) -> CallToolRequestResult:
68
  def live_server_with_tool() -> FastMCP:
69
  """Fixture to create a FastMCP server instance with the echo_tool registered."""
70
  server = FastMCP()
71
- server.add_tool(echo_tool)
72
- server.add_tool(error_tool)
73
- server.add_tool(no_return_tool)
74
  return server
75
 
76
 
 
9
  CallToolRequest,
10
  CallToolRequestResult,
11
  )
12
+ from fastmcp.tools.tool import Tool
13
 
14
  ContentType = TextContent | ImageContent | EmbeddedResource
15
 
 
69
  def live_server_with_tool() -> FastMCP:
70
  """Fixture to create a FastMCP server instance with the echo_tool registered."""
71
  server = FastMCP()
72
+ server.add_tool(Tool.from_function(echo_tool))
73
+ server.add_tool(Tool.from_function(error_tool))
74
+ server.add_tool(Tool.from_function(no_return_tool))
75
  return server
76
 
77
 
tests/deprecated/__init__.py CHANGED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import pytest
2
+
3
+ # reset deprecation warnings for this module
4
+ pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
tests/deprecated/test_deprecated.py CHANGED
@@ -1,5 +1,3 @@
1
- """Tests for deprecated functionality."""
2
-
3
  import warnings
4
  from unittest.mock import AsyncMock, patch
5
 
@@ -8,6 +6,9 @@ from starlette.applications import Starlette
8
 
9
  from fastmcp import Client, FastMCP
10
 
 
 
 
11
 
12
  def test_sse_app_deprecation_warning():
13
  """Test that sse_app raises a deprecation warning."""
 
 
 
1
  import warnings
2
  from unittest.mock import AsyncMock, patch
3
 
 
6
 
7
  from fastmcp import Client, FastMCP
8
 
9
+ # reset deprecation warnings for this module
10
+ pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
11
+
12
 
13
  def test_sse_app_deprecation_warning():
14
  """Test that sse_app raises a deprecation warning."""
tests/deprecated/test_mount_separators.py CHANGED
@@ -4,6 +4,9 @@ import pytest
4
 
5
  from fastmcp import FastMCP
6
 
 
 
 
7
 
8
  def test_mount_tool_separator_deprecation_warning():
9
  """Test that using tool_separator in mount() raises a deprecation warning."""
 
4
 
5
  from fastmcp import FastMCP
6
 
7
+ # reset deprecation warnings for this module
8
+ pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
9
+
10
 
11
  def test_mount_tool_separator_deprecation_warning():
12
  """Test that using tool_separator in mount() raises a deprecation warning."""
tests/deprecated/test_resource_prefixes.py CHANGED
@@ -1,5 +1,7 @@
1
  """Tests for legacy resource prefix behavior."""
2
 
 
 
3
  from fastmcp import Client, FastMCP
4
  from fastmcp.server.server import (
5
  add_resource_prefix,
@@ -8,6 +10,9 @@ from fastmcp.server.server import (
8
  )
9
  from fastmcp.utilities.tests import temporary_settings
10
 
 
 
 
11
 
12
  class TestLegacyResourcePrefixes:
13
  """Test the legacy resource prefix behavior."""
 
1
  """Tests for legacy resource prefix behavior."""
2
 
3
+ import pytest
4
+
5
  from fastmcp import Client, FastMCP
6
  from fastmcp.server.server import (
7
  add_resource_prefix,
 
10
  )
11
  from fastmcp.utilities.tests import temporary_settings
12
 
13
+ # reset deprecation warnings for this module
14
+ pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
15
+
16
 
17
  class TestLegacyResourcePrefixes:
18
  """Test the legacy resource prefix behavior."""
tests/deprecated/test_route_type_ignore.py CHANGED
@@ -12,6 +12,9 @@ from fastmcp.server.openapi import (
12
  RouteType,
13
  )
14
 
 
 
 
15
 
16
  def test_route_type_ignore_deprecation_warning():
17
  """Test that using RouteType.IGNORE emits a deprecation warning."""
 
12
  RouteType,
13
  )
14
 
15
+ # reset deprecation warnings for this module
16
+ pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
17
+
18
 
19
  def test_route_type_ignore_deprecation_warning():
20
  """Test that using RouteType.IGNORE emits a deprecation warning."""
tests/server/test_import_server.py CHANGED
@@ -3,7 +3,7 @@ from urllib.parse import quote
3
 
4
  from fastmcp.client.client import Client
5
  from fastmcp.server.server import FastMCP
6
- from fastmcp.tools.tool import FunctionTool
7
 
8
 
9
  async def test_import_basic_functionality():
@@ -199,7 +199,7 @@ async def test_tool_custom_name_preserved_when_imported():
199
  def fetch_data(query: str) -> str:
200
  return f"Data for query: {query}"
201
 
202
- api_app.add_tool(fetch_data, name="get_data")
203
  await main_app.import_server("api", api_app)
204
 
205
  # Check that the tool is accessible by its prefixed name
@@ -219,7 +219,7 @@ async def test_call_imported_custom_named_tool():
219
  def fetch_data(query: str) -> str:
220
  return f"Data for query: {query}"
221
 
222
- api_app.add_tool(fetch_data, name="get_data")
223
  await main_app.import_server("api", api_app)
224
 
225
  async with Client(main_app) as client:
@@ -235,7 +235,7 @@ async def test_first_level_importing_with_custom_name():
235
  def calculate_value(input: int) -> int:
236
  return input * 2
237
 
238
- provider_app.add_tool(calculate_value, name="compute")
239
  await service_app.import_server("provider", provider_app)
240
 
241
  # Tool is accessible in the service app with the first prefix
@@ -254,7 +254,7 @@ async def test_nested_importing_preserves_prefixes():
254
  def calculate_value(input: int) -> int:
255
  return input * 2
256
 
257
- provider_app.add_tool(calculate_value, name="compute")
258
  await service_app.import_server("provider", provider_app)
259
  await main_app.import_server("service", service_app)
260
 
@@ -272,7 +272,7 @@ async def test_call_nested_imported_tool():
272
  def calculate_value(input: int) -> int:
273
  return input * 2
274
 
275
- provider_app.add_tool(calculate_value, name="compute")
276
  await service_app.import_server("provider", provider_app)
277
  await main_app.import_server("service", service_app)
278
 
 
3
 
4
  from fastmcp.client.client import Client
5
  from fastmcp.server.server import FastMCP
6
+ from fastmcp.tools.tool import FunctionTool, Tool
7
 
8
 
9
  async def test_import_basic_functionality():
 
199
  def fetch_data(query: str) -> str:
200
  return f"Data for query: {query}"
201
 
202
+ api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
203
  await main_app.import_server("api", api_app)
204
 
205
  # Check that the tool is accessible by its prefixed name
 
219
  def fetch_data(query: str) -> str:
220
  return f"Data for query: {query}"
221
 
222
+ api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
223
  await main_app.import_server("api", api_app)
224
 
225
  async with Client(main_app) as client:
 
235
  def calculate_value(input: int) -> int:
236
  return input * 2
237
 
238
+ provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
239
  await service_app.import_server("provider", provider_app)
240
 
241
  # Tool is accessible in the service app with the first prefix
 
254
  def calculate_value(input: int) -> int:
255
  return input * 2
256
 
257
+ provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
258
  await service_app.import_server("provider", provider_app)
259
  await main_app.import_server("service", service_app)
260
 
 
272
  def calculate_value(input: int) -> int:
273
  return input * 2
274
 
275
+ provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
276
  await service_app.import_server("provider", provider_app)
277
  await main_app.import_server("service", service_app)
278
 
tests/server/test_server.py CHANGED
@@ -6,6 +6,8 @@ from pydantic import Field
6
 
7
  from fastmcp import Client, FastMCP
8
  from fastmcp.exceptions import NotFoundError
 
 
9
  from fastmcp.server.server import (
10
  MountedServer,
11
  add_resource_prefix,
@@ -13,6 +15,7 @@ from fastmcp.server.server import (
13
  remove_resource_prefix,
14
  )
15
  from fastmcp.tools import FunctionTool
 
16
 
17
 
18
  class TestCreateServer:
@@ -173,7 +176,7 @@ class TestToolDecorator:
173
  return self.x + y
174
 
175
  obj = MyClass(10)
176
- mcp.add_tool(obj.add)
177
  result = await mcp._mcp_call_tool("add", {"y": 2})
178
  assert result[0].text == "12" # type: ignore[attr-defined]
179
 
@@ -187,7 +190,7 @@ class TestToolDecorator:
187
  def add(cls, y: int) -> int:
188
  return cls.x + y
189
 
190
- mcp.add_tool(MyClass.add)
191
  result = await mcp._mcp_call_tool("add", {"y": 2})
192
  assert result[0].text == "12" # type: ignore[attr-defined]
193
 
@@ -223,7 +226,7 @@ class TestToolDecorator:
223
  async def add(cls, y: int) -> int:
224
  return cls.x + y
225
 
226
- mcp.add_tool(MyClass.add)
227
  result = await mcp._mcp_call_tool("add", {"y": 2})
228
  assert result[0].text == "12" # type: ignore[attr-defined]
229
 
@@ -235,7 +238,7 @@ class TestToolDecorator:
235
  async def add(x: int, y: int) -> int:
236
  return x + y
237
 
238
- mcp.add_tool(MyClass.add)
239
  result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
240
  assert result[0].text == "3" # type: ignore[attr-defined]
241
 
@@ -260,7 +263,7 @@ class TestToolDecorator:
260
  """Multiply two numbers."""
261
  return a * b
262
 
263
- mcp.add_tool(multiply, name="custom_multiply")
264
 
265
  # Check that the tool is registered with the custom name
266
  tools = await mcp.get_tools()
@@ -386,8 +389,11 @@ class TestResourceDecorator:
386
  return f"{self.prefix} Hello, world!"
387
 
388
  obj = MyClass("My prefix:")
389
- mcp.add_resource_fn(
390
- obj.get_data, uri="resource://data", name="instance-resource"
 
 
 
391
  )
392
 
393
  async with Client(mcp) as client:
@@ -404,8 +410,10 @@ class TestResourceDecorator:
404
  def get_data(cls) -> str:
405
  return f"{cls.prefix} Hello, world!"
406
 
407
- mcp.add_resource_fn(
408
- MyClass.get_data, uri="resource://data", name="class-resource"
 
 
409
  )
410
 
411
  async with Client(mcp) as client:
@@ -505,9 +513,12 @@ class TestTemplateDecorator:
505
  return f"{self.prefix} Data for {name}"
506
 
507
  obj = MyClass("My prefix:")
508
- mcp.add_resource_fn(
509
- obj.get_data, uri="resource://{name}/data", name="instance-template"
 
 
510
  )
 
511
 
512
  async with Client(mcp) as client:
513
  result = await client.read_resource("resource://test/data")
@@ -523,11 +534,12 @@ class TestTemplateDecorator:
523
  def get_data(cls, name: str) -> str:
524
  return f"{cls.prefix} Data for {name}"
525
 
526
- mcp.add_resource_fn(
527
  MyClass.get_data,
528
- uri="resource://{name}/data",
529
  name="class-template",
530
  )
 
531
 
532
  async with Client(mcp) as client:
533
  result = await client.read_resource("resource://test/data")
@@ -678,7 +690,7 @@ class TestPromptDecorator:
678
  return f"{self.prefix} Hello, world!"
679
 
680
  obj = MyClass("My prefix:")
681
- mcp.add_prompt(obj.test_prompt, name="test_prompt")
682
 
683
  async with Client(mcp) as client:
684
  result = await client.get_prompt("test_prompt")
@@ -696,7 +708,7 @@ class TestPromptDecorator:
696
  def test_prompt(cls) -> str:
697
  return f"{cls.prefix} Hello, world!"
698
 
699
- mcp.add_prompt(MyClass.test_prompt, name="test_prompt")
700
 
701
  async with Client(mcp) as client:
702
  result = await client.get_prompt("test_prompt")
 
6
 
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,
 
15
  remove_resource_prefix,
16
  )
17
  from fastmcp.tools import FunctionTool
18
+ from fastmcp.tools.tool import Tool
19
 
20
 
21
  class TestCreateServer:
 
176
  return self.x + y
177
 
178
  obj = MyClass(10)
179
+ mcp.add_tool(Tool.from_function(obj.add))
180
  result = await mcp._mcp_call_tool("add", {"y": 2})
181
  assert result[0].text == "12" # type: ignore[attr-defined]
182
 
 
190
  def add(cls, y: int) -> int:
191
  return cls.x + y
192
 
193
+ mcp.add_tool(Tool.from_function(MyClass.add))
194
  result = await mcp._mcp_call_tool("add", {"y": 2})
195
  assert result[0].text == "12" # type: ignore[attr-defined]
196
 
 
226
  async def add(cls, y: int) -> int:
227
  return cls.x + y
228
 
229
+ mcp.add_tool(Tool.from_function(MyClass.add))
230
  result = await mcp._mcp_call_tool("add", {"y": 2})
231
  assert result[0].text == "12" # type: ignore[attr-defined]
232
 
 
238
  async def add(x: int, y: int) -> int:
239
  return x + y
240
 
241
+ mcp.add_tool(Tool.from_function(MyClass.add))
242
  result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
243
  assert result[0].text == "3" # type: ignore[attr-defined]
244
 
 
263
  """Multiply two numbers."""
264
  return a * b
265
 
266
+ mcp.add_tool(Tool.from_function(multiply, name="custom_multiply"))
267
 
268
  # Check that the tool is registered with the custom name
269
  tools = await mcp.get_tools()
 
389
  return f"{self.prefix} Hello, world!"
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")
 
690
  return f"{self.prefix} Hello, world!"
691
 
692
  obj = MyClass("My prefix:")
693
+ mcp.add_prompt(Prompt.from_function(obj.test_prompt, name="test_prompt"))
694
 
695
  async with Client(mcp) as client:
696
  result = await client.get_prompt("test_prompt")
 
708
  def test_prompt(cls) -> str:
709
  return f"{cls.prefix} Hello, world!"
710
 
711
+ mcp.add_prompt(Prompt.from_function(MyClass.test_prompt, name="test_prompt"))
712
 
713
  async with Client(mcp) as client:
714
  result = await client.get_prompt("test_prompt")
tests/server/test_server_interactions.py CHANGED
@@ -19,9 +19,10 @@ from pydantic import AnyUrl, Field
19
  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, PromptMessage
23
- from fastmcp.resources import FileResource
24
  from fastmcp.resources.resource import FunctionResource
 
25
  from fastmcp.utilities.types import Image
26
 
27
 
@@ -691,7 +692,7 @@ class TestToolContextInjection:
691
  async def __call__(self, x: int, ctx: Context) -> int:
692
  return x + int(ctx.request_id)
693
 
694
- mcp.add_tool(MyTool())
695
 
696
  async with Client(mcp) as client:
697
  result = await client.call_tool("MyTool", {"x": 2})
@@ -1073,7 +1074,10 @@ class TestResourceTemplateContext:
1073
  def __call__(self, param: str, ctx: Context) -> str:
1074
  return f"Resource template: {param} {ctx.request_id}"
1075
 
1076
- mcp.add_resource_fn(MyResource(), uri="resource://{param}")
 
 
 
1077
 
1078
  async with Client(mcp) as client:
1079
  result = await client.read_resource(AnyUrl("resource://test"))
@@ -1285,7 +1289,7 @@ class TestPromptContext:
1285
  def __call__(self, name: str, ctx: Context) -> str:
1286
  return f"Hello, {name}! {ctx.request_id}"
1287
 
1288
- mcp.add_prompt(MyPrompt(), name="my_prompt")
1289
 
1290
  async with Client(mcp) as client:
1291
  result = await client.get_prompt("my_prompt", {"name": "World"})
 
19
  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, ResourceTemplate
24
  from fastmcp.resources.resource import FunctionResource
25
+ from fastmcp.tools.tool import Tool
26
  from fastmcp.utilities.types import Image
27
 
28
 
 
692
  async def __call__(self, x: int, ctx: Context) -> int:
693
  return x + int(ctx.request_id)
694
 
695
+ mcp.add_tool(Tool.from_function(MyTool(), name="MyTool"))
696
 
697
  async with Client(mcp) as client:
698
  result = await client.call_tool("MyTool", {"x": 2})
 
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"))
 
1289
  def __call__(self, name: str, ctx: Context) -> str:
1290
  return f"Hello, {name}! {ctx.request_id}"
1291
 
1292
+ mcp.add_prompt(Prompt.from_function(MyPrompt(), name="my_prompt")) # noqa: F821
1293
 
1294
  async with Client(mcp) as client:
1295
  result = await client.get_prompt("my_prompt", {"name": "World"})
tests/server/test_tool_annotations.py CHANGED
@@ -3,6 +3,7 @@ from typing import Any
3
  from mcp.types import ToolAnnotations
4
 
5
  from fastmcp import Client, FastMCP
 
6
 
7
 
8
  async def test_tool_annotations_in_tool_manager():
@@ -169,7 +170,7 @@ async def test_add_tool_method_annotations():
169
  """Create a new item."""
170
  return {"name": name, "value": value}
171
 
172
- mcp.add_tool(
173
  create_item,
174
  name="create_item",
175
  annotations=ToolAnnotations(
@@ -179,6 +180,8 @@ async def test_add_tool_method_annotations():
179
  ),
180
  )
181
 
 
 
182
  # Check internal tool objects directly
183
  tools = mcp._tool_manager.list_tools()
184
  assert len(tools) == 1
@@ -196,7 +199,7 @@ async def test_tool_functionality_with_annotations():
196
  """Create a new item."""
197
  return {"name": name, "value": value}
198
 
199
- mcp.add_tool(
200
  create_item,
201
  name="create_item",
202
  annotations=ToolAnnotations(
@@ -205,6 +208,7 @@ async def test_tool_functionality_with_annotations():
205
  destructiveHint=False,
206
  ),
207
  )
 
208
 
209
  # Use the tool to verify functionality is preserved
210
  async with Client(mcp) as client:
 
3
  from mcp.types import ToolAnnotations
4
 
5
  from fastmcp import Client, FastMCP
6
+ from fastmcp.tools.tool import Tool
7
 
8
 
9
  async def test_tool_annotations_in_tool_manager():
 
170
  """Create a new item."""
171
  return {"name": name, "value": value}
172
 
173
+ tool = Tool.from_function(
174
  create_item,
175
  name="create_item",
176
  annotations=ToolAnnotations(
 
180
  ),
181
  )
182
 
183
+ mcp.add_tool(tool)
184
+
185
  # Check internal tool objects directly
186
  tools = mcp._tool_manager.list_tools()
187
  assert len(tools) == 1
 
199
  """Create a new item."""
200
  return {"name": name, "value": value}
201
 
202
+ tool = Tool.from_function(
203
  create_item,
204
  name="create_item",
205
  annotations=ToolAnnotations(
 
208
  destructiveHint=False,
209
  ),
210
  )
211
+ mcp.add_tool(tool)
212
 
213
  # Use the tool to verify functionality is preserved
214
  async with Client(mcp) as client:
tests/server/test_tool_exclude_args.py CHANGED
@@ -4,6 +4,7 @@ import pytest
4
  from mcp.types import TextContent
5
 
6
  from fastmcp import Client, FastMCP
 
7
 
8
 
9
  async def test_tool_exclude_args_in_tool_manager():
@@ -53,7 +54,12 @@ async def test_add_tool_method_exclude_args():
53
  pass
54
  return {"name": name, "value": value}
55
 
56
- mcp.add_tool(create_item, name="create_item", exclude_args=["state"])
 
 
 
 
 
57
 
58
  # Check internal tool objects directly
59
  tools = mcp._tool_manager.list_tools()
@@ -77,7 +83,12 @@ async def test_tool_functionality_with_exclude_args():
77
  pass
78
  return {"name": name, "value": value}
79
 
80
- mcp.add_tool(create_item, name="create_item", exclude_args=["state"])
 
 
 
 
 
81
 
82
  # Use the tool to verify functionality is preserved
83
  async with Client(mcp) as client:
 
4
  from mcp.types import TextContent
5
 
6
  from fastmcp import Client, FastMCP
7
+ from fastmcp.tools.tool import Tool
8
 
9
 
10
  async def test_tool_exclude_args_in_tool_manager():
 
54
  pass
55
  return {"name": name, "value": value}
56
 
57
+ tool = Tool.from_function(
58
+ create_item,
59
+ name="create_item",
60
+ exclude_args=["state"],
61
+ )
62
+ mcp.add_tool(tool)
63
 
64
  # Check internal tool objects directly
65
  tools = mcp._tool_manager.list_tools()
 
83
  pass
84
  return {"name": name, "value": value}
85
 
86
+ tool = Tool.from_function(
87
+ create_item,
88
+ name="create_item",
89
+ exclude_args=["state"],
90
+ )
91
+ mcp.add_tool(tool)
92
 
93
  # Use the tool to verify functionality is preserved
94
  async with Client(mcp) as client:
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})