Jeremiah Lowin commited on
Commit
35489c1
·
unverified ·
2 Parent(s): beb209543fa1ab

Merge pull request #701 from jlowin/prompt-strict

Browse files

Use strict basemodel for Prompt; relax from_function deprecation

src/fastmcp/prompts/prompt.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations as _annotations
4
 
5
  import inspect
 
6
  from collections.abc import Awaitable, Callable, Sequence
7
  from typing import TYPE_CHECKING, Annotated, Any
8
 
@@ -10,13 +11,14 @@ import pydantic_core
10
  from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
11
  from mcp.types import Prompt as MCPPrompt
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
- from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
 
15
  from fastmcp.exceptions import PromptError
16
  from fastmcp.server.dependencies import get_context
17
  from fastmcp.utilities.json_schema import compress_schema
18
  from fastmcp.utilities.logging import get_logger
19
  from fastmcp.utilities.types import (
 
20
  _convert_set_defaults,
21
  find_kwarg_by_type,
22
  get_cached_typeadapter,
@@ -52,7 +54,7 @@ SyncPromptResult = (
52
  PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
53
 
54
 
55
- class PromptArgument(BaseModel):
56
  """An argument that can be passed to a prompt."""
57
 
58
  name: str = Field(description="Name of the argument")
@@ -64,7 +66,7 @@ class PromptArgument(BaseModel):
64
  )
65
 
66
 
67
- class Prompt(BaseModel):
68
  """A prompt template that can be rendered with parameters."""
69
 
70
  name: str = Field(description="Name of the prompt")
@@ -77,6 +79,61 @@ class Prompt(BaseModel):
77
  arguments: list[PromptArgument] | None = Field(
78
  None, description="Arguments that can be passed to the prompt"
79
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
81
 
82
  @classmethod
@@ -86,7 +143,7 @@ class Prompt(BaseModel):
86
  name: str | None = None,
87
  description: str | None = None,
88
  tags: set[str] | None = None,
89
- ) -> Prompt:
90
  """Create a Prompt from a function.
91
 
92
  The function can return:
@@ -147,8 +204,8 @@ class Prompt(BaseModel):
147
  name=func_name,
148
  description=description,
149
  arguments=arguments,
150
- fn=fn,
151
  tags=tags or set(),
 
152
  )
153
 
154
  async def render(
@@ -212,25 +269,3 @@ class Prompt(BaseModel):
212
  except Exception as e:
213
  logger.exception(f"Error rendering prompt {self.name}: {e}")
214
  raise PromptError(f"Error rendering prompt {self.name}.")
215
-
216
- def __eq__(self, other: object) -> bool:
217
- if not isinstance(other, Prompt):
218
- return False
219
- return self.model_dump() == other.model_dump()
220
-
221
- def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
222
- """Convert the prompt to an MCP prompt."""
223
- arguments = [
224
- MCPPromptArgument(
225
- name=arg.name,
226
- description=arg.description,
227
- required=arg.required,
228
- )
229
- for arg in self.arguments or []
230
- ]
231
- kwargs = {
232
- "name": self.name,
233
- "description": self.description,
234
- "arguments": arguments,
235
- }
236
- return MCPPrompt(**kwargs | overrides)
 
3
  from __future__ import annotations as _annotations
4
 
5
  import inspect
6
+ from abc import ABC, abstractmethod
7
  from collections.abc import Awaitable, Callable, Sequence
8
  from typing import TYPE_CHECKING, Annotated, Any
9
 
 
11
  from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
12
  from mcp.types import Prompt as MCPPrompt
13
  from mcp.types import PromptArgument as MCPPromptArgument
14
+ from pydantic import BeforeValidator, Field, TypeAdapter, validate_call
15
 
16
  from fastmcp.exceptions import PromptError
17
  from fastmcp.server.dependencies import get_context
18
  from fastmcp.utilities.json_schema import compress_schema
19
  from fastmcp.utilities.logging import get_logger
20
  from fastmcp.utilities.types import (
21
+ FastMCPBaseModel,
22
  _convert_set_defaults,
23
  find_kwarg_by_type,
24
  get_cached_typeadapter,
 
54
  PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
55
 
56
 
57
+ class PromptArgument(FastMCPBaseModel):
58
  """An argument that can be passed to a prompt."""
59
 
60
  name: str = Field(description="Name of the argument")
 
66
  )
67
 
68
 
69
+ class Prompt(FastMCPBaseModel, ABC):
70
  """A prompt template that can be rendered with parameters."""
71
 
72
  name: str = Field(description="Name of the prompt")
 
79
  arguments: list[PromptArgument] | None = Field(
80
  None, description="Arguments that can be passed to the prompt"
81
  )
82
+
83
+ def __eq__(self, other: object) -> bool:
84
+ if type(self) is not type(other):
85
+ return False
86
+ assert isinstance(other, type(self))
87
+ return self.model_dump() == other.model_dump()
88
+
89
+ def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
90
+ """Convert the prompt to an MCP prompt."""
91
+ arguments = [
92
+ MCPPromptArgument(
93
+ name=arg.name,
94
+ description=arg.description,
95
+ required=arg.required,
96
+ )
97
+ for arg in self.arguments or []
98
+ ]
99
+ kwargs = {
100
+ "name": self.name,
101
+ "description": self.description,
102
+ "arguments": arguments,
103
+ }
104
+ return MCPPrompt(**kwargs | overrides)
105
+
106
+ @staticmethod
107
+ def from_function(
108
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]],
109
+ name: str | None = None,
110
+ description: str | None = None,
111
+ tags: set[str] | None = None,
112
+ ) -> FunctionPrompt:
113
+ """Create a Prompt from a function.
114
+
115
+ The function can return:
116
+ - A string (converted to a message)
117
+ - A Message object
118
+ - A dict (converted to a message)
119
+ - A sequence of any of the above
120
+ """
121
+ return FunctionPrompt.from_function(
122
+ fn=fn, name=name, description=description, tags=tags
123
+ )
124
+
125
+ @abstractmethod
126
+ async def render(
127
+ self,
128
+ arguments: dict[str, Any] | None = None,
129
+ ) -> list[PromptMessage]:
130
+ """Render the prompt with arguments."""
131
+ raise NotImplementedError("Prompt.render() must be implemented by subclasses")
132
+
133
+
134
+ class FunctionPrompt(Prompt):
135
+ """A prompt that is a function."""
136
+
137
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
138
 
139
  @classmethod
 
143
  name: str | None = None,
144
  description: str | None = None,
145
  tags: set[str] | None = None,
146
+ ) -> FunctionPrompt:
147
  """Create a Prompt from a function.
148
 
149
  The function can return:
 
204
  name=func_name,
205
  description=description,
206
  arguments=arguments,
 
207
  tags=tags or set(),
208
+ fn=fn,
209
  )
210
 
211
  async def render(
 
269
  except Exception as e:
270
  logger.exception(f"Error rendering prompt {self.name}: {e}")
271
  raise PromptError(f"Error rendering prompt {self.name}.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
8
  from mcp import GetPromptResult
9
 
10
  from fastmcp.exceptions import NotFoundError, PromptError
11
- from fastmcp.prompts.prompt import Prompt, PromptResult
12
  from fastmcp.settings import DuplicateBehavior
13
  from fastmcp.utilities.logging import get_logger
14
 
@@ -55,10 +55,12 @@ class PromptManager:
55
  name: str | None = None,
56
  description: str | None = None,
57
  tags: set[str] | None = None,
58
- ) -> Prompt:
59
  """Create a prompt from a function."""
60
- prompt = Prompt.from_function(fn, name=name, description=description, tags=tags)
61
- return self.add_prompt(prompt)
 
 
62
 
63
  def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt:
64
  """Add a prompt to the manager."""
 
8
  from mcp import GetPromptResult
9
 
10
  from fastmcp.exceptions import NotFoundError, PromptError
11
+ from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
12
  from fastmcp.settings import DuplicateBehavior
13
  from fastmcp.utilities.logging import get_logger
14
 
 
55
  name: str | None = None,
56
  description: str | None = None,
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
+ )
63
+ return self.add_prompt(prompt) # type: ignore
64
 
65
  def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt:
66
  """Add a prompt to the manager."""
src/fastmcp/server/proxy.py CHANGED
@@ -153,6 +153,8 @@ class ProxyTemplate(ResourceTemplate):
153
 
154
 
155
  class ProxyPrompt(Prompt):
 
 
156
  def __init__(self, client: Client, **kwargs):
157
  super().__init__(**kwargs)
158
  self._client = client
@@ -164,7 +166,6 @@ class ProxyPrompt(Prompt):
164
  name=prompt.name,
165
  description=prompt.description,
166
  arguments=[a.model_dump() for a in prompt.arguments or []],
167
- fn=_proxy_passthrough,
168
  )
169
 
170
  async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
 
153
 
154
 
155
  class ProxyPrompt(Prompt):
156
+ _client: Client
157
+
158
  def __init__(self, client: Client, **kwargs):
159
  super().__init__(**kwargs)
160
  self._client = client
 
166
  name=prompt.name,
167
  description=prompt.description,
168
  arguments=[a.model_dump() for a in prompt.arguments or []],
 
169
  )
170
 
171
  async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
src/fastmcp/server/server.py CHANGED
@@ -55,7 +55,7 @@ from fastmcp.server.http import (
55
  create_streamable_http_app,
56
  )
57
  from fastmcp.tools import ToolManager
58
- from fastmcp.tools.tool import FunctionTool, Tool
59
  from fastmcp.utilities.cache import TimedCache
60
  from fastmcp.utilities.decorators import DecoratedFunction
61
  from fastmcp.utilities.logging import get_logger
@@ -508,7 +508,7 @@ class FastMCP(Generic[LifespanResultT]):
508
  if isinstance(annotations, dict):
509
  annotations = ToolAnnotations(**annotations)
510
 
511
- tool = FunctionTool.from_function(
512
  fn,
513
  name=name,
514
  description=description,
 
55
  create_streamable_http_app,
56
  )
57
  from fastmcp.tools import ToolManager
58
+ from fastmcp.tools.tool import Tool
59
  from fastmcp.utilities.cache import TimedCache
60
  from fastmcp.utilities.decorators import DecoratedFunction
61
  from fastmcp.utilities.logging import get_logger
 
508
  if isinstance(annotations, dict):
509
  annotations = ToolAnnotations(**annotations)
510
 
511
+ tool = Tool.from_function(
512
  fn,
513
  name=name,
514
  description=description,
src/fastmcp/tools/tool.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
 
3
  import inspect
4
  import json
5
- import warnings
6
  from abc import ABC, abstractmethod
7
  from collections.abc import Callable
8
  from typing import TYPE_CHECKING, Annotated, Any
@@ -66,12 +65,25 @@ class Tool(FastMCPBaseModel, ABC):
66
  return MCPTool(**kwargs | overrides)
67
 
68
  @staticmethod
69
- def from_function(fn: Callable[..., Any], **overrides: Any) -> FunctionTool:
70
- # deprecated in 2.6.2
71
- warnings.warn(
72
- "Tool.from_function() is deprecated. Use FunctionTool.from_function() instead."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  )
74
- return FunctionTool.from_function(fn, **overrides)
75
 
76
  def __eq__(self, other: object) -> bool:
77
  if not isinstance(other, Tool):
 
2
 
3
  import inspect
4
  import json
 
5
  from abc import ABC, abstractmethod
6
  from collections.abc import Callable
7
  from typing import TYPE_CHECKING, Annotated, Any
 
65
  return MCPTool(**kwargs | overrides)
66
 
67
  @staticmethod
68
+ def from_function(
69
+ fn: Callable[..., Any],
70
+ name: str | None = None,
71
+ description: str | None = None,
72
+ tags: set[str] | None = None,
73
+ annotations: ToolAnnotations | None = None,
74
+ exclude_args: list[str] | None = None,
75
+ serializer: Callable[[Any], str] | None = None,
76
+ ) -> FunctionTool:
77
+ """Create a Tool from a function."""
78
+ return FunctionTool.from_function(
79
+ fn=fn,
80
+ name=name,
81
+ description=description,
82
+ tags=tags,
83
+ annotations=annotations,
84
+ exclude_args=exclude_args,
85
+ serializer=serializer,
86
  )
 
87
 
88
  def __eq__(self, other: object) -> bool:
89
  if not isinstance(other, Tool):
src/fastmcp/tools/tool_manager.py CHANGED
@@ -7,7 +7,7 @@ from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotatio
7
 
8
  from fastmcp.exceptions import NotFoundError, ToolError
9
  from fastmcp.settings import DuplicateBehavior
10
- from fastmcp.tools.tool import FunctionTool, Tool
11
  from fastmcp.utilities.logging import get_logger
12
 
13
  if TYPE_CHECKING:
@@ -69,7 +69,7 @@ class ToolManager:
69
  exclude_args: list[str] | None = None,
70
  ) -> Tool:
71
  """Add a tool to the server."""
72
- tool = FunctionTool.from_function(
73
  fn,
74
  name=name,
75
  description=description,
 
7
 
8
  from fastmcp.exceptions import NotFoundError, ToolError
9
  from fastmcp.settings import DuplicateBehavior
10
+ from fastmcp.tools.tool import Tool
11
  from fastmcp.utilities.logging import get_logger
12
 
13
  if TYPE_CHECKING:
 
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,
tests/deprecated/test_tool_from_function_deprecated.py DELETED
@@ -1,76 +0,0 @@
1
- """Tests for deprecated Tool.from_function() method.
2
-
3
- The Tool.from_function() method was deprecated in version 2.6.2 in favor of
4
- FunctionTool.from_function().
5
- """
6
-
7
- import warnings
8
-
9
- import pytest
10
-
11
- from fastmcp.tools.tool import FunctionTool, Tool
12
-
13
-
14
- def test_tool_from_function_deprecation_warning():
15
- """Test that Tool.from_function() raises a deprecation warning."""
16
-
17
- def example_function(x: int) -> str:
18
- """Example function for testing."""
19
- return f"Result: {x}"
20
-
21
- with pytest.warns(
22
- UserWarning,
23
- match="Tool.from_function\\(\\) is deprecated. Use FunctionTool.from_function\\(\\) instead.",
24
- ):
25
- tool = Tool.from_function(example_function)
26
-
27
- # Verify the tool was created correctly despite the warning
28
- assert isinstance(tool, FunctionTool)
29
- assert tool.name == "example_function"
30
- assert tool.description == "Example function for testing."
31
-
32
-
33
- def test_tool_from_function_produces_same_result_as_function_tool():
34
- """Test that Tool.from_function() produces the same result as FunctionTool.from_function()."""
35
-
36
- def example_function(x: int, y: str = "default") -> dict:
37
- """Example function with parameters."""
38
- return {"x": x, "y": y}
39
-
40
- # Create tool using the deprecated method (with warning suppressed)
41
- with warnings.catch_warnings():
42
- warnings.simplefilter("ignore")
43
- deprecated_tool = Tool.from_function(example_function)
44
-
45
- # Create tool using the new method
46
- new_tool = FunctionTool.from_function(example_function)
47
-
48
- # They should be equivalent
49
- assert deprecated_tool == new_tool
50
- assert deprecated_tool.name == new_tool.name
51
- assert deprecated_tool.description == new_tool.description
52
- assert deprecated_tool.parameters == new_tool.parameters
53
-
54
-
55
- def test_tool_from_function_with_overrides():
56
- """Test that Tool.from_function() works with parameter overrides."""
57
-
58
- def example_function() -> str:
59
- """Original description."""
60
- return "test"
61
-
62
- custom_name = "custom_tool_name"
63
- custom_description = "Custom description"
64
- custom_tags = {"test", "deprecated"}
65
-
66
- with pytest.warns(UserWarning, match="Tool.from_function\\(\\) is deprecated"):
67
- tool = Tool.from_function(
68
- example_function,
69
- name=custom_name,
70
- description=custom_description,
71
- tags=custom_tags,
72
- )
73
-
74
- assert tool.name == custom_name
75
- assert tool.description == custom_description
76
- assert tool.tags == custom_tags
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/prompts/test_prompt_manager.py CHANGED
@@ -5,7 +5,7 @@ import pytest
5
  from fastmcp import Context
6
  from fastmcp.exceptions import NotFoundError, PromptError
7
  from fastmcp.prompts import Prompt
8
- from fastmcp.prompts.prompt import PromptMessage, TextContent
9
  from fastmcp.prompts.prompt_manager import PromptManager
10
 
11
 
@@ -97,6 +97,7 @@ class TestPromptManager:
97
  # Should have replaced with the new prompt
98
  prompt = manager.get_prompt("test_prompt")
99
  assert prompt is not None
 
100
  assert prompt.fn.__name__ == "replacement_fn"
101
 
102
  def test_ignore_duplicate_prompts(self):
@@ -118,8 +119,10 @@ class TestPromptManager:
118
  # Should keep the original
119
  prompt = manager.get_prompt("test_prompt")
120
  assert prompt is not None
 
121
  assert prompt.fn.__name__ == "original_fn"
122
  # Result should be the original prompt
 
123
  assert result.fn.__name__ == "original_fn"
124
 
125
  def test_get_prompts(self):
 
5
  from fastmcp import Context
6
  from fastmcp.exceptions import NotFoundError, PromptError
7
  from fastmcp.prompts import Prompt
8
+ from fastmcp.prompts.prompt import FunctionPrompt, PromptMessage, TextContent
9
  from fastmcp.prompts.prompt_manager import PromptManager
10
 
11
 
 
97
  # Should have replaced with the new prompt
98
  prompt = manager.get_prompt("test_prompt")
99
  assert prompt is not None
100
+ assert isinstance(prompt, FunctionPrompt)
101
  assert prompt.fn.__name__ == "replacement_fn"
102
 
103
  def test_ignore_duplicate_prompts(self):
 
119
  # Should keep the original
120
  prompt = manager.get_prompt("test_prompt")
121
  assert prompt is not None
122
+ assert isinstance(prompt, FunctionPrompt)
123
  assert prompt.fn.__name__ == "original_fn"
124
  # Result should be the original prompt
125
+ assert isinstance(result, FunctionPrompt)
126
  assert result.fn.__name__ == "original_fn"
127
 
128
  def test_get_prompts(self):
tests/tools/test_tool.py CHANGED
@@ -5,7 +5,7 @@ from pydantic import AnyUrl, BaseModel
5
  from fastmcp import FastMCP, Image
6
  from fastmcp.client import Client
7
  from fastmcp.exceptions import ToolError
8
- from fastmcp.tools.tool import FunctionTool, _convert_to_content
9
  from fastmcp.utilities.tests import temporary_settings
10
 
11
 
@@ -17,7 +17,7 @@ class TestToolFromFunction:
17
  """Add two numbers."""
18
  return a + b
19
 
20
- tool = FunctionTool.from_function(add)
21
 
22
  assert tool.name == "add"
23
  assert tool.description == "Add two numbers."
@@ -32,7 +32,7 @@ class TestToolFromFunction:
32
  """Fetch data from URL."""
33
  return f"Data from {url}"
34
 
35
- tool = FunctionTool.from_function(fetch_data)
36
 
37
  assert tool.name == "fetch_data"
38
  assert tool.description == "Fetch data from URL."
@@ -46,7 +46,7 @@ class TestToolFromFunction:
46
  """ignore this"""
47
  return x + y
48
 
49
- tool = FunctionTool.from_function(Adder())
50
  assert tool.name == "Adder"
51
  assert tool.description == "Adds two numbers."
52
  assert len(tool.parameters["properties"]) == 2
@@ -61,7 +61,7 @@ class TestToolFromFunction:
61
  """ignore this"""
62
  return x + y
63
 
64
- tool = FunctionTool.from_function(Adder())
65
  assert tool.name == "Adder"
66
  assert tool.description == "Adds two numbers."
67
  assert len(tool.parameters["properties"]) == 2
@@ -79,7 +79,7 @@ class TestToolFromFunction:
79
  """Create a new user."""
80
  return {"id": 1, **user.model_dump()}
81
 
82
- tool = FunctionTool.from_function(create_user)
83
 
84
  assert tool.name == "create_user"
85
  assert tool.description == "Create a new user."
@@ -91,7 +91,7 @@ class TestToolFromFunction:
91
  def image_tool(data: bytes) -> Image:
92
  return Image(data=data)
93
 
94
- tool = FunctionTool.from_function(image_tool)
95
 
96
  result = await tool.run({"data": "test.png"})
97
  assert tool.parameters["properties"]["data"]["type"] == "string"
@@ -99,24 +99,24 @@ class TestToolFromFunction:
99
 
100
  def test_non_callable_fn(self):
101
  with pytest.raises(TypeError, match="not a callable object"):
102
- FunctionTool.from_function(1) # type: ignore
103
 
104
  def test_lambda(self):
105
- tool = FunctionTool.from_function(lambda x: x, name="my_tool")
106
  assert tool.name == "my_tool"
107
 
108
  def test_lambda_with_no_name(self):
109
  with pytest.raises(
110
  ValueError, match="You must provide a name for lambda functions"
111
  ):
112
- FunctionTool.from_function(lambda x: x)
113
 
114
  def test_private_arguments(self):
115
  def add(_a: int, _b: int) -> int:
116
  """Add two numbers."""
117
  return _a + _b
118
 
119
- tool = FunctionTool.from_function(add)
120
  assert tool.parameters["properties"]["_a"]["type"] == "integer"
121
  assert tool.parameters["properties"]["_b"]["type"] == "integer"
122
 
@@ -128,7 +128,7 @@ class TestToolFromFunction:
128
  with pytest.raises(
129
  ValueError, match=r"Functions with \*args are not supported as tools"
130
  ):
131
- FunctionTool.from_function(func)
132
 
133
  def test_tool_with_varkwargs_not_allowed(self):
134
  def func(a: int, b: int, **kwargs: int) -> int:
@@ -138,7 +138,7 @@ class TestToolFromFunction:
138
  with pytest.raises(
139
  ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
140
  ):
141
- FunctionTool.from_function(func)
142
 
143
  async def test_instance_method(self):
144
  class MyClass:
@@ -148,7 +148,7 @@ class TestToolFromFunction:
148
 
149
  obj = MyClass()
150
 
151
- tool = FunctionTool.from_function(obj.add)
152
  assert tool.name == "add"
153
  assert tool.description == "Add two numbers."
154
  assert "self" not in tool.parameters["properties"]
@@ -164,7 +164,7 @@ class TestToolFromFunction:
164
  with pytest.raises(
165
  ValueError, match=r"Functions with \*args are not supported as tools"
166
  ):
167
- FunctionTool.from_function(obj.add)
168
 
169
  async def test_instance_method_with_varkwargs_not_allowed(self):
170
  class MyClass:
@@ -177,7 +177,7 @@ class TestToolFromFunction:
177
  with pytest.raises(
178
  ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
179
  ):
180
- FunctionTool.from_function(obj.add)
181
 
182
  async def test_classmethod(self):
183
  class MyClass:
@@ -188,7 +188,7 @@ class TestToolFromFunction:
188
  """Add two numbers."""
189
  return x + y
190
 
191
- tool = FunctionTool.from_function(MyClass.call)
192
  assert tool.name == "call"
193
  assert tool.description == "Add two numbers."
194
  assert "x" in tool.parameters["properties"]
@@ -203,7 +203,7 @@ class TestToolFromFunction:
203
  def process_list(items: list[int]) -> int:
204
  return sum(items)
205
 
206
- tool = FunctionTool.from_function(process_list, serializer=custom_serializer)
207
 
208
  result = await tool.run(arguments={"items": [1, 2, 3, 4, 5]})
209
  assert isinstance(result[0], TextContent)
@@ -225,7 +225,7 @@ class TestLegacyToolJsonParsing:
225
  return f"{x}-{','.join(y)}"
226
 
227
  # Create a tool to use its JSON pre-parsing logic
228
- tool = FunctionTool.from_function(simple_func)
229
 
230
  # Prepare arguments where some are JSON strings
231
  json_args = {
@@ -243,7 +243,7 @@ class TestLegacyToolJsonParsing:
243
  def func_with_str_types(str_or_list: str | list[str]) -> str | list[str]:
244
  return str_or_list
245
 
246
- tool = FunctionTool.from_function(func_with_str_types)
247
 
248
  # Test regular string input (should remain a string)
249
  result = await tool.run({"str_or_list": "hello"})
@@ -269,7 +269,7 @@ class TestLegacyToolJsonParsing:
269
  def func_with_str_types(string: str) -> str:
270
  return string
271
 
272
- tool = FunctionTool.from_function(func_with_str_types)
273
 
274
  # Invalid JSON should remain a string
275
  invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
@@ -284,7 +284,7 @@ class TestLegacyToolJsonParsing:
284
  ) -> str | dict[int, str] | None:
285
  return string
286
 
287
- tool = FunctionTool.from_function(func_with_str_types)
288
 
289
  # Invalid JSON for the union type should remain a string
290
  invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
@@ -301,7 +301,7 @@ class TestLegacyToolJsonParsing:
301
  def func_with_complex_type(data: SomeModel) -> SomeModel:
302
  return data
303
 
304
- tool = FunctionTool.from_function(func_with_complex_type)
305
 
306
  # Valid JSON for the model
307
  valid_json = '{"x": 1, "y": {"1": "hello"}}'
 
5
  from fastmcp import FastMCP, Image
6
  from fastmcp.client import Client
7
  from fastmcp.exceptions import ToolError
8
+ from fastmcp.tools.tool import Tool, _convert_to_content
9
  from fastmcp.utilities.tests import temporary_settings
10
 
11
 
 
17
  """Add two numbers."""
18
  return a + b
19
 
20
+ tool = Tool.from_function(add)
21
 
22
  assert tool.name == "add"
23
  assert tool.description == "Add two numbers."
 
32
  """Fetch data from URL."""
33
  return f"Data from {url}"
34
 
35
+ tool = Tool.from_function(fetch_data)
36
 
37
  assert tool.name == "fetch_data"
38
  assert tool.description == "Fetch data from URL."
 
46
  """ignore this"""
47
  return x + y
48
 
49
+ tool = Tool.from_function(Adder())
50
  assert tool.name == "Adder"
51
  assert tool.description == "Adds two numbers."
52
  assert len(tool.parameters["properties"]) == 2
 
61
  """ignore this"""
62
  return x + y
63
 
64
+ tool = Tool.from_function(Adder())
65
  assert tool.name == "Adder"
66
  assert tool.description == "Adds two numbers."
67
  assert len(tool.parameters["properties"]) == 2
 
79
  """Create a new user."""
80
  return {"id": 1, **user.model_dump()}
81
 
82
+ tool = Tool.from_function(create_user)
83
 
84
  assert tool.name == "create_user"
85
  assert tool.description == "Create a new user."
 
91
  def image_tool(data: bytes) -> Image:
92
  return Image(data=data)
93
 
94
+ tool = Tool.from_function(image_tool)
95
 
96
  result = await tool.run({"data": "test.png"})
97
  assert tool.parameters["properties"]["data"]["type"] == "string"
 
99
 
100
  def test_non_callable_fn(self):
101
  with pytest.raises(TypeError, match="not a callable object"):
102
+ Tool.from_function(1) # type: ignore
103
 
104
  def test_lambda(self):
105
+ tool = Tool.from_function(lambda x: x, name="my_tool")
106
  assert tool.name == "my_tool"
107
 
108
  def test_lambda_with_no_name(self):
109
  with pytest.raises(
110
  ValueError, match="You must provide a name for lambda functions"
111
  ):
112
+ Tool.from_function(lambda x: x)
113
 
114
  def test_private_arguments(self):
115
  def add(_a: int, _b: int) -> int:
116
  """Add two numbers."""
117
  return _a + _b
118
 
119
+ tool = Tool.from_function(add)
120
  assert tool.parameters["properties"]["_a"]["type"] == "integer"
121
  assert tool.parameters["properties"]["_b"]["type"] == "integer"
122
 
 
128
  with pytest.raises(
129
  ValueError, match=r"Functions with \*args are not supported as tools"
130
  ):
131
+ Tool.from_function(func)
132
 
133
  def test_tool_with_varkwargs_not_allowed(self):
134
  def func(a: int, b: int, **kwargs: int) -> int:
 
138
  with pytest.raises(
139
  ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
140
  ):
141
+ Tool.from_function(func)
142
 
143
  async def test_instance_method(self):
144
  class MyClass:
 
148
 
149
  obj = MyClass()
150
 
151
+ tool = Tool.from_function(obj.add)
152
  assert tool.name == "add"
153
  assert tool.description == "Add two numbers."
154
  assert "self" not in tool.parameters["properties"]
 
164
  with pytest.raises(
165
  ValueError, match=r"Functions with \*args are not supported as tools"
166
  ):
167
+ Tool.from_function(obj.add)
168
 
169
  async def test_instance_method_with_varkwargs_not_allowed(self):
170
  class MyClass:
 
177
  with pytest.raises(
178
  ValueError, match=r"Functions with \*\*kwargs are not supported as tools"
179
  ):
180
+ Tool.from_function(obj.add)
181
 
182
  async def test_classmethod(self):
183
  class MyClass:
 
188
  """Add two numbers."""
189
  return x + y
190
 
191
+ tool = Tool.from_function(MyClass.call)
192
  assert tool.name == "call"
193
  assert tool.description == "Add two numbers."
194
  assert "x" in tool.parameters["properties"]
 
203
  def process_list(items: list[int]) -> int:
204
  return sum(items)
205
 
206
+ tool = Tool.from_function(process_list, serializer=custom_serializer)
207
 
208
  result = await tool.run(arguments={"items": [1, 2, 3, 4, 5]})
209
  assert isinstance(result[0], TextContent)
 
225
  return f"{x}-{','.join(y)}"
226
 
227
  # Create a tool to use its JSON pre-parsing logic
228
+ tool = Tool.from_function(simple_func)
229
 
230
  # Prepare arguments where some are JSON strings
231
  json_args = {
 
243
  def func_with_str_types(str_or_list: str | list[str]) -> str | list[str]:
244
  return str_or_list
245
 
246
+ tool = Tool.from_function(func_with_str_types)
247
 
248
  # Test regular string input (should remain a string)
249
  result = await tool.run({"str_or_list": "hello"})
 
269
  def func_with_str_types(string: str) -> str:
270
  return string
271
 
272
+ tool = Tool.from_function(func_with_str_types)
273
 
274
  # Invalid JSON should remain a string
275
  invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
 
284
  ) -> str | dict[int, str] | None:
285
  return string
286
 
287
+ tool = Tool.from_function(func_with_str_types)
288
 
289
  # Invalid JSON for the union type should remain a string
290
  invalid_json = "{'nice to meet you': 'hello', 'goodbye': 5}"
 
301
  def func_with_complex_type(data: SomeModel) -> SomeModel:
302
  return data
303
 
304
+ tool = Tool.from_function(func_with_complex_type)
305
 
306
  # Valid JSON for the model
307
  valid_json = '{"x": 1, "y": {"1": "hello"}}'
tests/tools/test_tool_manager.py CHANGED
@@ -568,7 +568,7 @@ class TestContextHandling:
568
 
569
  def test_context_parameter_detection(self):
570
  """Test that context parameters are properly detected in
571
- FunctionTool.from_function()."""
572
 
573
  def tool_with_context(x: int, ctx: Context) -> str:
574
  return str(x)
@@ -634,7 +634,7 @@ class TestContextHandling:
634
 
635
  def test_parameterized_context_parameter_detection(self):
636
  """Test that context parameters are properly detected in
637
- FunctionTool.from_function()."""
638
 
639
  def tool_with_context(x: int, ctx: Context) -> str:
640
  return str(x)
@@ -651,7 +651,7 @@ class TestContextHandling:
651
 
652
  def test_parameterized_union_context_parameter_detection(self):
653
  """Test that context parameters are properly detected in
654
- FunctionTool.from_function()."""
655
 
656
  def tool_with_context(x: int, ctx: Context | None) -> str:
657
  return str(x)
 
568
 
569
  def test_context_parameter_detection(self):
570
  """Test that context parameters are properly detected in
571
+ Tool.from_function()."""
572
 
573
  def tool_with_context(x: int, ctx: Context) -> str:
574
  return str(x)
 
634
 
635
  def test_parameterized_context_parameter_detection(self):
636
  """Test that context parameters are properly detected in
637
+ Tool.from_function()."""
638
 
639
  def tool_with_context(x: int, ctx: Context) -> str:
640
  return str(x)
 
651
 
652
  def test_parameterized_union_context_parameter_detection(self):
653
  """Test that context parameters are properly detected in
654
+ Tool.from_function()."""
655
 
656
  def tool_with_context(x: int, ctx: Context | None) -> str:
657
  return str(x)