Jeremiah Lowin commited on
Commit
beb2095
·
unverified ·
2 Parent(s): 9d663e6ce89f2f

Merge pull request #700 from jlowin/transform-tools

Browse files
src/fastmcp/resources/resource.py CHANGED
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Annotated, Any
8
  from mcp.types import Resource as MCPResource
9
  from pydantic import (
10
  AnyUrl,
11
- BaseModel,
12
  BeforeValidator,
13
  ConfigDict,
14
  Field,
@@ -17,13 +16,13 @@ from pydantic import (
17
  field_validator,
18
  )
19
 
20
- from fastmcp.utilities.types import _convert_set_defaults
21
 
22
  if TYPE_CHECKING:
23
  pass
24
 
25
 
26
- class Resource(BaseModel, abc.ABC):
27
  """Base class for all resources."""
28
 
29
  model_config = ConfigDict(validate_default=True)
 
8
  from mcp.types import Resource as MCPResource
9
  from pydantic import (
10
  AnyUrl,
 
11
  BeforeValidator,
12
  ConfigDict,
13
  Field,
 
16
  field_validator,
17
  )
18
 
19
+ from fastmcp.utilities.types import FastMCPBaseModel, _convert_set_defaults
20
 
21
  if TYPE_CHECKING:
22
  pass
23
 
24
 
25
+ class Resource(FastMCPBaseModel, abc.ABC):
26
  """Base class for all resources."""
27
 
28
  model_config = ConfigDict(validate_default=True)
src/fastmcp/resources/template.py CHANGED
@@ -11,7 +11,6 @@ from urllib.parse import unquote
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
12
  from pydantic import (
13
  AnyUrl,
14
- BaseModel,
15
  BeforeValidator,
16
  Field,
17
  field_validator,
@@ -22,6 +21,7 @@ from fastmcp.resources.types import FunctionResource, Resource
22
  from fastmcp.server.dependencies import get_context
23
  from fastmcp.utilities.json_schema import compress_schema
24
  from fastmcp.utilities.types import (
 
25
  _convert_set_defaults,
26
  find_kwarg_by_type,
27
  get_cached_typeadapter,
@@ -52,12 +52,7 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
52
  return None
53
 
54
 
55
- class MyModel(BaseModel):
56
- key: str
57
- value: int
58
-
59
-
60
- class ResourceTemplate(BaseModel):
61
  """A template for dynamically creating resources."""
62
 
63
  uri_template: str = Field(
 
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
12
  from pydantic import (
13
  AnyUrl,
 
14
  BeforeValidator,
15
  Field,
16
  field_validator,
 
21
  from fastmcp.server.dependencies import get_context
22
  from fastmcp.utilities.json_schema import compress_schema
23
  from fastmcp.utilities.types import (
24
+ FastMCPBaseModel,
25
  _convert_set_defaults,
26
  find_kwarg_by_type,
27
  get_cached_typeadapter,
 
52
  return None
53
 
54
 
55
+ class ResourceTemplate(FastMCPBaseModel):
 
 
 
 
 
56
  """A template for dynamically creating resources."""
57
 
58
  uri_template: str = Field(
src/fastmcp/server/openapi.py CHANGED
@@ -233,7 +233,6 @@ class OpenAPITool(Tool):
233
  name=name,
234
  description=description,
235
  parameters=parameters,
236
- fn=self._execute_request, # We'll use an instance method instead of a global function
237
  tags=tags,
238
  annotations=annotations,
239
  exclude_args=exclude_args,
@@ -247,9 +246,10 @@ class OpenAPITool(Tool):
247
  """Custom representation to prevent recursion errors when printing."""
248
  return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
249
 
250
- async def _execute_request(self, *args, **kwargs):
 
 
251
  """Execute the HTTP request based on the route configuration."""
252
- context = kwargs.get("context")
253
 
254
  # Prepare URL
255
  path = self._route.path
@@ -258,11 +258,11 @@ class OpenAPITool(Tool):
258
  # Path parameters should never be None as they're typically required
259
  # but we'll handle that case anyway
260
  path_params = {
261
- p.name: kwargs.get(p.name)
262
  for p in self._route.parameters
263
  if p.location == "path"
264
- and p.name in kwargs
265
- and kwargs.get(p.name) is not None
266
  }
267
 
268
  # Ensure all path parameters are provided
@@ -340,11 +340,11 @@ class OpenAPITool(Tool):
340
  for p in self._route.parameters:
341
  if (
342
  p.location == "query"
343
- and p.name in kwargs
344
- and kwargs.get(p.name) is not None
345
- and kwargs.get(p.name) != ""
346
  ):
347
- param_value = kwargs.get(p.name)
348
 
349
  # Format array query parameters as comma-separated strings
350
  # following OpenAPI form style (default for query parameters)
@@ -399,10 +399,10 @@ class OpenAPITool(Tool):
399
  for p in self._route.parameters:
400
  if (
401
  p.location == "header"
402
- and p.name in kwargs
403
- and kwargs[p.name] is not None
404
  ):
405
- openapi_headers[p.name.lower()] = str(kwargs[p.name])
406
  headers.update(openapi_headers)
407
 
408
  # Add headers from the current MCP client HTTP request (these take precedence)
@@ -420,21 +420,13 @@ class OpenAPITool(Tool):
420
  }
421
  body_params = {
422
  k: v
423
- for k, v in kwargs.items()
424
  if k not in path_query_header_params and k != "context"
425
  }
426
 
427
  if body_params:
428
  json_data = body_params
429
 
430
- # Log the request details if a context is available
431
- if context:
432
- try:
433
- await context.info(f"Making {self._route.method} request to {path}")
434
- except (ValueError, AttributeError):
435
- # Silently continue if context logging is not available
436
- pass
437
-
438
  # Execute the request
439
  try:
440
  response = await self._client.request(
@@ -451,10 +443,11 @@ class OpenAPITool(Tool):
451
 
452
  # Try to parse as JSON first
453
  try:
454
- return response.json()
455
  except (json.JSONDecodeError, ValueError):
456
  # Return text content if not JSON
457
- return response.text
 
458
 
459
  except httpx.HTTPStatusError as e:
460
  # Handle HTTP errors (4xx, 5xx)
@@ -474,13 +467,6 @@ class OpenAPITool(Tool):
474
  # Handle request errors (connection, timeout, etc.)
475
  raise ValueError(f"Request error: {str(e)}")
476
 
477
- async def run(
478
- self, arguments: dict[str, Any]
479
- ) -> list[TextContent | ImageContent | EmbeddedResource]:
480
- """Run the tool with arguments and optional context."""
481
- response = await self._execute_request(**arguments)
482
- return _convert_to_content(response)
483
-
484
 
485
  class OpenAPIResource(Resource):
486
  """Resource implementation for OpenAPI endpoints."""
 
233
  name=name,
234
  description=description,
235
  parameters=parameters,
 
236
  tags=tags,
237
  annotations=annotations,
238
  exclude_args=exclude_args,
 
246
  """Custom representation to prevent recursion errors when printing."""
247
  return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
248
 
249
+ async def run(
250
+ self, arguments: dict[str, Any]
251
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
252
  """Execute the HTTP request based on the route configuration."""
 
253
 
254
  # Prepare URL
255
  path = self._route.path
 
258
  # Path parameters should never be None as they're typically required
259
  # but we'll handle that case anyway
260
  path_params = {
261
+ p.name: arguments.get(p.name)
262
  for p in self._route.parameters
263
  if p.location == "path"
264
+ and p.name in arguments
265
+ and arguments.get(p.name) is not None
266
  }
267
 
268
  # Ensure all path parameters are provided
 
340
  for p in self._route.parameters:
341
  if (
342
  p.location == "query"
343
+ and p.name in arguments
344
+ and arguments.get(p.name) is not None
345
+ and arguments.get(p.name) != ""
346
  ):
347
+ param_value = arguments.get(p.name)
348
 
349
  # Format array query parameters as comma-separated strings
350
  # following OpenAPI form style (default for query parameters)
 
399
  for p in self._route.parameters:
400
  if (
401
  p.location == "header"
402
+ and p.name in arguments
403
+ and arguments[p.name] is not None
404
  ):
405
+ openapi_headers[p.name.lower()] = str(arguments[p.name])
406
  headers.update(openapi_headers)
407
 
408
  # Add headers from the current MCP client HTTP request (these take precedence)
 
420
  }
421
  body_params = {
422
  k: v
423
+ for k, v in arguments.items()
424
  if k not in path_query_header_params and k != "context"
425
  }
426
 
427
  if body_params:
428
  json_data = body_params
429
 
 
 
 
 
 
 
 
 
430
  # Execute the request
431
  try:
432
  response = await self._client.request(
 
443
 
444
  # Try to parse as JSON first
445
  try:
446
+ result = response.json()
447
  except (json.JSONDecodeError, ValueError):
448
  # Return text content if not JSON
449
+ result = response.text
450
+ return _convert_to_content(result)
451
 
452
  except httpx.HTTPStatusError as e:
453
  # Handle HTTP errors (4xx, 5xx)
 
467
  # Handle request errors (connection, timeout, etc.)
468
  raise ValueError(f"Request error: {str(e)}")
469
 
 
 
 
 
 
 
 
470
 
471
  class OpenAPIResource(Resource):
472
  """Resource implementation for OpenAPI endpoints."""
src/fastmcp/server/proxy.py CHANGED
@@ -48,7 +48,6 @@ class ProxyTool(Tool):
48
  name=tool.name,
49
  description=tool.description,
50
  parameters=tool.inputSchema,
51
- fn=_proxy_passthrough,
52
  )
53
 
54
  async def run(
@@ -69,6 +68,9 @@ class ProxyTool(Tool):
69
 
70
 
71
  class ProxyResource(Resource):
 
 
 
72
  def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
73
  super().__init__(**kwargs)
74
  self._client = client
@@ -146,7 +148,6 @@ class ProxyTemplate(ResourceTemplate):
146
  name=self.name,
147
  description=self.description,
148
  mime_type=result[0].mimeType,
149
- contents=result,
150
  _value=value,
151
  )
152
 
 
48
  name=tool.name,
49
  description=tool.description,
50
  parameters=tool.inputSchema,
 
51
  )
52
 
53
  async def run(
 
68
 
69
 
70
  class ProxyResource(Resource):
71
+ _client: Client
72
+ _value: str | bytes | None = None
73
+
74
  def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
75
  super().__init__(**kwargs)
76
  self._client = client
 
148
  name=self.name,
149
  description=self.description,
150
  mime_type=result[0].mimeType,
 
151
  _value=value,
152
  )
153
 
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 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
- self._tool_manager.add_tool_from_fn(
512
  fn,
513
  name=name,
514
  description=description,
@@ -516,6 +516,8 @@ class FastMCP(Generic[LifespanResultT]):
516
  annotations=annotations,
517
  exclude_args=exclude_args,
518
  )
 
 
519
  self._cache.clear()
520
 
521
  def remove_tool(self, name: str) -> None:
 
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
  if isinstance(annotations, dict):
509
  annotations = ToolAnnotations(**annotations)
510
 
511
+ tool = FunctionTool.from_function(
512
  fn,
513
  name=name,
514
  description=description,
 
516
  annotations=annotations,
517
  exclude_args=exclude_args,
518
  )
519
+
520
+ self._tool_manager.add_tool(tool)
521
  self._cache.clear()
522
 
523
  def remove_tool(self, name: str) -> None:
src/fastmcp/tools/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .tool import Tool
2
  from .tool_manager import ToolManager
3
 
4
- __all__ = ["Tool", "ToolManager"]
 
1
+ from .tool import Tool, FunctionTool
2
  from .tool_manager import ToolManager
3
 
4
+ __all__ = ["Tool", "ToolManager", "FunctionTool"]
src/fastmcp/tools/tool.py CHANGED
@@ -2,19 +2,22 @@ from __future__ import annotations
2
 
3
  import inspect
4
  import json
 
 
5
  from collections.abc import Callable
6
  from typing import TYPE_CHECKING, Annotated, Any
7
 
8
  import pydantic_core
9
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
10
  from mcp.types import Tool as MCPTool
11
- from pydantic import BaseModel, BeforeValidator, Field
12
 
13
  import fastmcp
14
  from fastmcp.server.dependencies import get_context
15
  from fastmcp.utilities.json_schema import compress_schema
16
  from fastmcp.utilities.logging import get_logger
17
  from fastmcp.utilities.types import (
 
18
  Image,
19
  _convert_set_defaults,
20
  find_kwarg_by_type,
@@ -31,10 +34,9 @@ def default_serializer(data: Any) -> str:
31
  return pydantic_core.to_json(data, fallback=str, indent=2).decode()
32
 
33
 
34
- class Tool(BaseModel):
35
  """Internal tool registration info."""
36
 
37
- fn: Callable[..., Any]
38
  name: str = Field(description="Name of the tool")
39
  description: str | None = Field(
40
  default=None, description="Description of what the tool does"
@@ -54,6 +56,39 @@ class Tool(BaseModel):
54
  None, description="Optional custom serializer for tool results"
55
  )
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  @classmethod
58
  def from_function(
59
  cls,
@@ -64,7 +99,7 @@ class Tool(BaseModel):
64
  annotations: ToolAnnotations | None = None,
65
  exclude_args: list[str] | None = None,
66
  serializer: Callable[[Any], str] | None = None,
67
- ) -> Tool:
68
  """Create a Tool from a function."""
69
  from fastmcp.server.context import Context
70
 
@@ -170,20 +205,6 @@ class Tool(BaseModel):
170
 
171
  return _convert_to_content(result, serializer=self.serializer)
172
 
173
- def to_mcp_tool(self, **overrides: Any) -> MCPTool:
174
- kwargs = {
175
- "name": self.name,
176
- "description": self.description,
177
- "inputSchema": self.parameters,
178
- "annotations": self.annotations,
179
- }
180
- return MCPTool(**kwargs | overrides)
181
-
182
- def __eq__(self, other: object) -> bool:
183
- if not isinstance(other, Tool):
184
- return False
185
- return self.model_dump() == other.model_dump()
186
-
187
 
188
  def _convert_to_content(
189
  result: Any,
 
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
9
 
10
  import pydantic_core
11
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
12
  from mcp.types import Tool as MCPTool
13
+ from pydantic import BeforeValidator, Field
14
 
15
  import fastmcp
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
+ FastMCPBaseModel,
21
  Image,
22
  _convert_set_defaults,
23
  find_kwarg_by_type,
 
34
  return pydantic_core.to_json(data, fallback=str, indent=2).decode()
35
 
36
 
37
+ class Tool(FastMCPBaseModel, ABC):
38
  """Internal tool registration info."""
39
 
 
40
  name: str = Field(description="Name of the tool")
41
  description: str | None = Field(
42
  default=None, description="Description of what the tool does"
 
56
  None, description="Optional custom serializer for tool results"
57
  )
58
 
59
+ def to_mcp_tool(self, **overrides: Any) -> MCPTool:
60
+ kwargs = {
61
+ "name": self.name,
62
+ "description": self.description,
63
+ "inputSchema": self.parameters,
64
+ "annotations": self.annotations,
65
+ }
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):
78
+ return False
79
+ return self.model_dump() == other.model_dump()
80
+
81
+ @abstractmethod
82
+ async def run(
83
+ self, arguments: dict[str, Any]
84
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
85
+ """Run the tool with arguments."""
86
+ raise NotImplementedError("Subclasses must implement run()")
87
+
88
+
89
+ class FunctionTool(Tool):
90
+ fn: Callable[..., Any]
91
+
92
  @classmethod
93
  def from_function(
94
  cls,
 
99
  annotations: ToolAnnotations | None = None,
100
  exclude_args: list[str] | None = None,
101
  serializer: Callable[[Any], str] | None = None,
102
+ ) -> FunctionTool:
103
  """Create a Tool from a function."""
104
  from fastmcp.server.context import Context
105
 
 
205
 
206
  return _convert_to_content(result, serializer=self.serializer)
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  def _convert_to_content(
210
  result: Any,
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 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 = Tool.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 FunctionTool, 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 = FunctionTool.from_function(
73
  fn,
74
  name=name,
75
  description=description,
src/fastmcp/utilities/mcp_config.py CHANGED
@@ -3,7 +3,9 @@ from __future__ import annotations
3
  from typing import TYPE_CHECKING, Any, Literal
4
  from urllib.parse import urlparse
5
 
6
- from pydantic import AnyUrl, BaseModel, Field
 
 
7
 
8
  if TYPE_CHECKING:
9
  from fastmcp.client.transports import (
@@ -32,7 +34,7 @@ def infer_transport_type_from_url(
32
  return "streamable-http"
33
 
34
 
35
- class StdioMCPServer(BaseModel):
36
  command: str
37
  args: list[str] = Field(default_factory=list)
38
  env: dict[str, Any] = Field(default_factory=dict)
@@ -50,7 +52,7 @@ class StdioMCPServer(BaseModel):
50
  )
51
 
52
 
53
- class RemoteMCPServer(BaseModel):
54
  url: str
55
  headers: dict[str, str] = Field(default_factory=dict)
56
  transport: Literal["streamable-http", "sse", "http"] | None = None
@@ -69,7 +71,7 @@ class RemoteMCPServer(BaseModel):
69
  return StreamableHttpTransport(self.url, headers=self.headers)
70
 
71
 
72
- class MCPConfig(BaseModel):
73
  mcpServers: dict[str, StdioMCPServer | RemoteMCPServer]
74
 
75
  @classmethod
 
3
  from typing import TYPE_CHECKING, Any, Literal
4
  from urllib.parse import urlparse
5
 
6
+ from pydantic import AnyUrl, Field
7
+
8
+ from fastmcp.utilities.types import FastMCPBaseModel
9
 
10
  if TYPE_CHECKING:
11
  from fastmcp.client.transports import (
 
34
  return "streamable-http"
35
 
36
 
37
+ class StdioMCPServer(FastMCPBaseModel):
38
  command: str
39
  args: list[str] = Field(default_factory=list)
40
  env: dict[str, Any] = Field(default_factory=dict)
 
52
  )
53
 
54
 
55
+ class RemoteMCPServer(FastMCPBaseModel):
56
  url: str
57
  headers: dict[str, str] = Field(default_factory=dict)
58
  transport: Literal["streamable-http", "sse", "http"] | None = None
 
71
  return StreamableHttpTransport(self.url, headers=self.headers)
72
 
73
 
74
+ class MCPConfig(FastMCPBaseModel):
75
  mcpServers: dict[str, StdioMCPServer | RemoteMCPServer]
76
 
77
  @classmethod
src/fastmcp/utilities/openapi.py CHANGED
@@ -25,6 +25,7 @@ from openapi_pydantic.v3.v3_0 import Schema as Schema_30
25
  from pydantic import BaseModel, Field, ValidationError
26
 
27
  from fastmcp.utilities.json_schema import compress_schema
 
28
 
29
  logger = logging.getLogger(__name__)
30
 
@@ -38,7 +39,7 @@ ParameterLocation = Literal["path", "query", "header", "cookie"]
38
  JsonSchema = dict[str, Any]
39
 
40
 
41
- class ParameterInfo(BaseModel):
42
  """Represents a single parameter for an HTTP operation in our IR."""
43
 
44
  name: str
@@ -48,7 +49,7 @@ class ParameterInfo(BaseModel):
48
  description: str | None = None
49
 
50
 
51
- class RequestBodyInfo(BaseModel):
52
  """Represents the request body for an HTTP operation in our IR."""
53
 
54
  required: bool = False
@@ -58,7 +59,7 @@ class RequestBodyInfo(BaseModel):
58
  description: str | None = None
59
 
60
 
61
- class ResponseInfo(BaseModel):
62
  """Represents response information in our IR."""
63
 
64
  description: str | None = None
@@ -66,7 +67,7 @@ class ResponseInfo(BaseModel):
66
  content_schema: dict[str, JsonSchema] = Field(default_factory=dict)
67
 
68
 
69
- class HTTPRoute(BaseModel):
70
  """Intermediate Representation for a single OpenAPI operation."""
71
 
72
  path: str
 
25
  from pydantic import BaseModel, Field, ValidationError
26
 
27
  from fastmcp.utilities.json_schema import compress_schema
28
+ from fastmcp.utilities.types import FastMCPBaseModel
29
 
30
  logger = logging.getLogger(__name__)
31
 
 
39
  JsonSchema = dict[str, Any]
40
 
41
 
42
+ class ParameterInfo(FastMCPBaseModel):
43
  """Represents a single parameter for an HTTP operation in our IR."""
44
 
45
  name: str
 
49
  description: str | None = None
50
 
51
 
52
+ class RequestBodyInfo(FastMCPBaseModel):
53
  """Represents the request body for an HTTP operation in our IR."""
54
 
55
  required: bool = False
 
59
  description: str | None = None
60
 
61
 
62
+ class ResponseInfo(FastMCPBaseModel):
63
  """Represents response information in our IR."""
64
 
65
  description: str | None = None
 
67
  content_schema: dict[str, JsonSchema] = Field(default_factory=dict)
68
 
69
 
70
+ class HTTPRoute(FastMCPBaseModel):
71
  """Intermediate Representation for a single OpenAPI operation."""
72
 
73
  path: str
src/fastmcp/utilities/types.py CHANGED
@@ -9,11 +9,17 @@ from types import UnionType
9
  from typing import Annotated, TypeVar, Union, get_args, get_origin
10
 
11
  from mcp.types import ImageContent
12
- from pydantic import TypeAdapter
13
 
14
  T = TypeVar("T")
15
 
16
 
 
 
 
 
 
 
17
  @lru_cache(maxsize=5000)
18
  def get_cached_typeadapter(cls: T) -> TypeAdapter[T]:
19
  """
 
9
  from typing import Annotated, TypeVar, Union, get_args, get_origin
10
 
11
  from mcp.types import ImageContent
12
+ from pydantic import BaseModel, ConfigDict, TypeAdapter
13
 
14
  T = TypeVar("T")
15
 
16
 
17
+ class FastMCPBaseModel(BaseModel):
18
+ """Base model for FastMCP models."""
19
+
20
+ model_config = ConfigDict(extra="forbid")
21
+
22
+
23
  @lru_cache(maxsize=5000)
24
  def get_cached_typeadapter(cls: T) -> TypeAdapter[T]:
25
  """
tests/deprecated/test_tool_from_function_deprecated.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/server/openapi/test_openapi_path_parameters.py CHANGED
@@ -128,7 +128,7 @@ async def test_array_path_parameter_handling(mock_client):
128
  )
129
 
130
  # Test with a single value
131
- await tool._execute_request(days=["monday"])
132
 
133
  # Check that the path parameter is formatted correctly
134
  # This is where the bug is: it should be '/select/monday' not '/select/[\'monday\']'
@@ -143,7 +143,7 @@ async def test_array_path_parameter_handling(mock_client):
143
  mock_client.request.reset_mock()
144
 
145
  # Test with multiple values
146
- await tool._execute_request(days=["monday", "tuesday"])
147
 
148
  # Check that the path parameter is formatted correctly
149
  # It should be '/select/monday,tuesday' not '/select/[\'monday\', \'tuesday\']'
@@ -234,7 +234,7 @@ async def test_complex_nested_array_path_parameter(mock_client):
234
  ]
235
 
236
  # Execute the request with complex filters
237
- await tool._execute_request(filters=complex_filters)
238
 
239
  # The complex object should be properly serialized in the URL
240
  # For path parameters, this would typically need a custom serialization strategy
@@ -359,7 +359,7 @@ async def test_array_query_parameter_format(mock_client):
359
  )
360
 
361
  # Test with a single value
362
- await tool._execute_request(days=["monday"])
363
 
364
  # Check that the query parameter is formatted correctly
365
  mock_client.request.assert_called_with(
@@ -373,7 +373,7 @@ async def test_array_query_parameter_format(mock_client):
373
  mock_client.request.reset_mock()
374
 
375
  # Test with multiple values
376
- await tool._execute_request(days=["monday", "tuesday"])
377
 
378
  # Check that the query parameter is formatted correctly
379
  # It should be 'days=monday,tuesday' not 'days=["monday","tuesday"]'
@@ -429,7 +429,7 @@ async def test_array_query_parameter_exploded_format(mock_client):
429
  )
430
 
431
  # Test with a single value
432
- await tool._execute_request(days=["monday"])
433
 
434
  # Check that the query parameter is formatted correctly
435
  mock_client.request.assert_called_with(
@@ -443,7 +443,7 @@ async def test_array_query_parameter_exploded_format(mock_client):
443
  mock_client.request.reset_mock()
444
 
445
  # Test with multiple values
446
- await tool._execute_request(days=["monday", "tuesday"])
447
 
448
  # Check that the query parameter is formatted correctly
449
  # It should be passed as an array, which httpx will serialize as days=monday&days=tuesday
 
128
  )
129
 
130
  # Test with a single value
131
+ await tool.run({"days": ["monday"]})
132
 
133
  # Check that the path parameter is formatted correctly
134
  # This is where the bug is: it should be '/select/monday' not '/select/[\'monday\']'
 
143
  mock_client.request.reset_mock()
144
 
145
  # Test with multiple values
146
+ await tool.run({"days": ["monday", "tuesday"]})
147
 
148
  # Check that the path parameter is formatted correctly
149
  # It should be '/select/monday,tuesday' not '/select/[\'monday\', \'tuesday\']'
 
234
  ]
235
 
236
  # Execute the request with complex filters
237
+ await tool.run({"filters": complex_filters})
238
 
239
  # The complex object should be properly serialized in the URL
240
  # For path parameters, this would typically need a custom serialization strategy
 
359
  )
360
 
361
  # Test with a single value
362
+ await tool.run({"days": ["monday"]})
363
 
364
  # Check that the query parameter is formatted correctly
365
  mock_client.request.assert_called_with(
 
373
  mock_client.request.reset_mock()
374
 
375
  # Test with multiple values
376
+ await tool.run({"days": ["monday", "tuesday"]})
377
 
378
  # Check that the query parameter is formatted correctly
379
  # It should be 'days=monday,tuesday' not 'days=["monday","tuesday"]'
 
429
  )
430
 
431
  # Test with a single value
432
+ await tool.run({"days": ["monday"]})
433
 
434
  # Check that the query parameter is formatted correctly
435
  mock_client.request.assert_called_with(
 
443
  mock_client.request.reset_mock()
444
 
445
  # Test with multiple values
446
+ await tool.run({"days": ["monday", "tuesday"]})
447
 
448
  # Check that the query parameter is formatted correctly
449
  # It should be passed as an array, which httpx will serialize as days=monday&days=tuesday
tests/server/test_import_server.py CHANGED
@@ -3,6 +3,7 @@ from urllib.parse import quote
3
 
4
  from fastmcp.client.client import Client
5
  from fastmcp.server.server import FastMCP
 
6
 
7
 
8
  async def test_import_basic_functionality():
@@ -27,6 +28,7 @@ async def test_import_basic_functionality():
27
  tool = main_app._tool_manager.get_tool("sub_sub_tool")
28
  assert tool is not None
29
  assert tool.name == "sub_tool"
 
30
  assert callable(tool.fn)
31
 
32
 
@@ -205,6 +207,7 @@ async def test_tool_custom_name_preserved_when_imported():
205
  assert tool is not None
206
 
207
  # Check that the function name is preserved
 
208
  assert tool.fn.__name__ == "fetch_data"
209
 
210
 
@@ -238,6 +241,7 @@ async def test_first_level_importing_with_custom_name():
238
  # Tool is accessible in the service app with the first prefix
239
  tool = service_app._tool_manager.get_tool("provider_compute")
240
  assert tool is not None
 
241
  assert tool.fn.__name__ == "calculate_value"
242
 
243
 
 
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():
 
28
  tool = main_app._tool_manager.get_tool("sub_sub_tool")
29
  assert tool is not None
30
  assert tool.name == "sub_tool"
31
+ assert isinstance(tool, FunctionTool)
32
  assert callable(tool.fn)
33
 
34
 
 
207
  assert tool is not None
208
 
209
  # Check that the function name is preserved
210
+ assert isinstance(tool, FunctionTool)
211
  assert tool.fn.__name__ == "fetch_data"
212
 
213
 
 
241
  # Tool is accessible in the service app with the first prefix
242
  tool = service_app._tool_manager.get_tool("provider_compute")
243
  assert tool is not None
244
+ assert isinstance(tool, FunctionTool)
245
  assert tool.fn.__name__ == "calculate_value"
246
 
247
 
tests/server/test_server.py CHANGED
@@ -12,7 +12,7 @@ from fastmcp.server.server import (
12
  has_resource_prefix,
13
  remove_resource_prefix,
14
  )
15
- from fastmcp.tools.tool import Tool
16
 
17
 
18
  class TestCreateServer:
@@ -102,7 +102,7 @@ class TestTools:
102
  """add two to a number"""
103
  return x + 2
104
 
105
- g_tool = Tool.from_function(g, name="g-tool")
106
 
107
  mcp = FastMCP(tools=[f, g_tool])
108
 
 
12
  has_resource_prefix,
13
  remove_resource_prefix,
14
  )
15
+ from fastmcp.tools import FunctionTool
16
 
17
 
18
  class TestCreateServer:
 
102
  """add two to a number"""
103
  return x + 2
104
 
105
+ g_tool = FunctionTool.from_function(g, name="g-tool")
106
 
107
  mcp = FastMCP(tools=[f, g_tool])
108
 
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 Tool, _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 = Tool.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 = Tool.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 = Tool.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 = Tool.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 = Tool.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 = Tool.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
- 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,7 +128,7 @@ class TestToolFromFunction:
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,7 +138,7 @@ class TestToolFromFunction:
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,7 +148,7 @@ class TestToolFromFunction:
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,7 +164,7 @@ class TestToolFromFunction:
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,7 +177,7 @@ class TestToolFromFunction:
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,7 +188,7 @@ class TestToolFromFunction:
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,7 +203,7 @@ class TestToolFromFunction:
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,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 = Tool.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 = 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,7 +269,7 @@ class TestLegacyToolJsonParsing:
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,7 +284,7 @@ class TestLegacyToolJsonParsing:
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,7 +301,7 @@ class TestLegacyToolJsonParsing:
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"}}'
 
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
  """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
  """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
  """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
  """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
  """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
  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
 
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
  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
  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
 
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
  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
  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
  """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
  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
  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
  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
  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
  ) -> 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
  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"}}'
tests/tools/test_tool_manager.py CHANGED
@@ -10,8 +10,7 @@ from pydantic import BaseModel
10
 
11
  from fastmcp import Context, FastMCP, Image
12
  from fastmcp.exceptions import NotFoundError, ToolError
13
- from fastmcp.tools import ToolManager
14
- from fastmcp.tools.tool import Tool
15
  from fastmcp.utilities.tests import temporary_settings
16
 
17
 
@@ -212,6 +211,7 @@ class TestAddTools:
212
  # Should have replaced with the new function
213
  tool = manager.get_tool("test_tool")
214
  assert tool is not None
 
215
  assert tool.fn.__name__ == "replacement_fn"
216
 
217
  def test_ignore_duplicate_tools(self):
@@ -230,8 +230,10 @@ class TestAddTools:
230
  # Should keep the original
231
  tool = manager.get_tool("test_tool")
232
  assert tool is not None
 
233
  assert tool.fn.__name__ == "original_fn"
234
  # Result should be the original tool
 
235
  assert result.fn.__name__ == "original_fn"
236
 
237
 
@@ -566,7 +568,7 @@ class TestContextHandling:
566
 
567
  def test_context_parameter_detection(self):
568
  """Test that context parameters are properly detected in
569
- Tool.from_function()."""
570
 
571
  def tool_with_context(x: int, ctx: Context) -> str:
572
  return str(x)
@@ -632,7 +634,7 @@ class TestContextHandling:
632
 
633
  def test_parameterized_context_parameter_detection(self):
634
  """Test that context parameters are properly detected in
635
- Tool.from_function()."""
636
 
637
  def tool_with_context(x: int, ctx: Context) -> str:
638
  return str(x)
@@ -649,7 +651,7 @@ class TestContextHandling:
649
 
650
  def test_parameterized_union_context_parameter_detection(self):
651
  """Test that context parameters are properly detected in
652
- Tool.from_function()."""
653
 
654
  def tool_with_context(x: int, ctx: Context | None) -> str:
655
  return str(x)
@@ -691,6 +693,7 @@ class TestCustomToolNames:
691
  # The tool is stored under the custom name and its .name is also set to custom_name
692
  assert manager.get_tool("custom_name") is not None
693
  assert tool.name == "custom_name"
 
694
  assert tool.fn.__name__ == "original_fn"
695
  # The tool should not be accessible via its original function name
696
  with pytest.raises(NotFoundError, match="Unknown tool: original_fn"):
@@ -703,7 +706,7 @@ class TestCustomToolNames:
703
  return x + 1
704
 
705
  # Create a tool with a specific name
706
- tool = Tool.from_function(fn, name="my_tool")
707
  manager = ToolManager()
708
  # Store it under a different name
709
  manager.add_tool(tool, key="proxy_tool")
@@ -762,6 +765,7 @@ class TestCustomToolNames:
762
  assert stored_tool.name == "test_tool"
763
 
764
  # But the function is different
 
765
  assert stored_tool.fn.__name__ == "replacement_fn"
766
 
767
 
 
10
 
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
 
 
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)
215
  assert tool.fn.__name__ == "replacement_fn"
216
 
217
  def test_ignore_duplicate_tools(self):
 
230
  # Should keep the original
231
  tool = manager.get_tool("test_tool")
232
  assert tool is not None
233
+ assert isinstance(tool, FunctionTool)
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
 
 
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
 
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
 
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)
 
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
695
  assert tool.name == "custom_name"
696
+ assert isinstance(tool, FunctionTool)
697
  assert tool.fn.__name__ == "original_fn"
698
  # The tool should not be accessible via its original function name
699
  with pytest.raises(NotFoundError, match="Unknown tool: original_fn"):
 
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")
 
765
  assert stored_tool.name == "test_tool"
766
 
767
  # But the function is different
768
+ assert isinstance(stored_tool, FunctionTool)
769
  assert stored_tool.fn.__name__ == "replacement_fn"
770
 
771