Server add_* methods no longer accept functions

This commit is contained in:
Jeremiah Lowin 2025-06-04 12:39:34 -04:00
commit 0535a70d31
12 changed files with 151 additions and 83 deletions

View file

@ -86,7 +86,7 @@ fallback-version = "0.0.0"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
asyncio_default_test_loop_scope = "session"
filterwarnings = ["error::DeprecationWarning"]
# filterwarnings = ["error::DeprecationWarning"]
timeout = 3
env = [
"FASTMCP_TEST_MODE=1",

View file

@ -3,6 +3,10 @@
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources.resource import Resource
from fastmcp.tools.tool import Tool
if TYPE_CHECKING:
from fastmcp.server import FastMCP
@ -128,7 +132,8 @@ class MCPMixin:
registration_info["name"] = (
f"{prefix}{separator}{registration_info['name']}"
)
mcp_server.add_tool(fn=method, **registration_info)
tool = Tool.from_function(fn=method, **registration_info)
mcp_server.add_tool(tool)
def register_resources(
self,
@ -156,7 +161,8 @@ class MCPMixin:
registration_info["uri"] = (
f"{prefix}{separator}{registration_info['uri']}"
)
mcp_server.add_resource_fn(fn=method, **registration_info)
resource = Resource.from_function(fn=method, **registration_info)
mcp_server.add_resource(resource)
def register_prompts(
self,
@ -180,7 +186,8 @@ class MCPMixin:
registration_info["name"] = (
f"{prefix}{separator}{registration_info['name']}"
)
mcp_server.add_prompt(fn=method, **registration_info)
prompt = Prompt.from_function(fn=method, **registration_info)
mcp_server.add_prompt(prompt)
def register_all(
self,

View file

@ -1,7 +1,6 @@
"""Prompt management functionality."""
from __future__ import annotations as _annotations
import warnings
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, Any
@ -57,6 +56,11 @@ class PromptManager:
tags: set[str] | None = None,
) -> FunctionPrompt:
"""Create a prompt from a function."""
# deprecated in 2.7.0
warnings.warn(
"PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.",
DeprecationWarning,
)
prompt = FunctionPrompt.from_function(
fn, name=name, description=description, tags=tags
)

View file

@ -1,6 +1,7 @@
"""Resource manager functionality."""
import inspect
import warnings
from collections.abc import Callable
from typing import Any
@ -120,6 +121,11 @@ class ResourceManager:
The added resource. If a resource with the same URI already exists,
returns the existing resource.
"""
# deprecated in 2.7.0
warnings.warn(
"add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
DeprecationWarning,
)
resource = Resource.from_function(
fn=fn,
uri=uri,
@ -171,7 +177,11 @@ class ResourceManager:
tags: set[str] | None = None,
) -> ResourceTemplate:
"""Create a template from a function."""
# deprecated in 2.7.0
warnings.warn(
"add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
DeprecationWarning,
)
template = ResourceTemplate.from_function(
fn,
uri_template=uri_template,

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import datetime
import inspect
import re
import warnings
from collections.abc import AsyncIterator, Awaitable, Callable
@ -44,7 +45,6 @@ import fastmcp.server
import fastmcp.settings
from fastmcp.exceptions import NotFoundError
from fastmcp.prompts import Prompt, PromptManager
from fastmcp.prompts.prompt import PromptResult
from fastmcp.resources import Resource, ResourceManager
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.auth.auth import OAuthProvider
@ -183,10 +183,9 @@ class FastMCP(Generic[LifespanResultT]):
if tools:
for tool in tools:
if isinstance(tool, Tool):
self._tool_manager.add_tool(tool)
else:
self.add_tool(tool)
if not isinstance(tool, Tool):
tool = Tool.from_function(tool)
self.add_tool(tool)
# Set up MCP protocol handlers
self._setup_handlers()
@ -349,18 +348,18 @@ class FastMCP(Generic[LifespanResultT]):
"""
def decorator(
func: Callable[[Request], Awaitable[Response]],
fn: Callable[[Request], Awaitable[Response]],
) -> Callable[[Request], Awaitable[Response]]:
self._additional_http_routes.append(
Route(
path,
endpoint=func,
endpoint=fn,
methods=methods,
name=name,
include_in_schema=include_in_schema,
)
)
return func
return fn
return decorator
@ -484,15 +483,7 @@ class FastMCP(Generic[LifespanResultT]):
raise NotFoundError(f"Unknown prompt: {name}")
def add_tool(
self,
fn: AnyFunction,
name: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
) -> None:
def add_tool(self, tool: Tool) -> None:
"""Add a tool to the server.
The tool function can optionally request a Context object by adding a parameter
@ -505,18 +496,6 @@ class FastMCP(Generic[LifespanResultT]):
tags: Optional set of tags for categorizing the tool
annotations: Optional annotations about the tool's behavior
"""
if isinstance(annotations, dict):
annotations = ToolAnnotations(**annotations)
tool = Tool.from_function(
fn,
name=name,
description=description,
tags=tags,
annotations=annotations,
exclude_args=exclude_args,
)
self._tool_manager.add_tool(tool)
self._cache.clear()
@ -574,9 +553,11 @@ class FastMCP(Generic[LifespanResultT]):
"The @tool decorator was used incorrectly. "
"Did you forget to call it? Use @tool() instead of @tool"
)
if isinstance(annotations, dict):
annotations = ToolAnnotations(**annotations)
def decorator(fn: AnyFunction) -> AnyFunction:
self.add_tool(
tool = Tool.from_function(
fn,
name=name,
description=description,
@ -584,6 +565,7 @@ class FastMCP(Generic[LifespanResultT]):
annotations=annotations,
exclude_args=exclude_args,
)
self.add_tool(tool)
return fn
return decorator
@ -598,6 +580,14 @@ class FastMCP(Generic[LifespanResultT]):
self._resource_manager.add_resource(resource, key=key)
self._cache.clear()
def add_template(self, template: ResourceTemplate, key: str | None = None) -> None:
"""Add a resource template to the server.
Args:
template: A ResourceTemplate instance to add
"""
self._resource_manager.add_template(template, key=key)
def add_resource_fn(
self,
fn: AnyFunction,
@ -620,6 +610,12 @@ class FastMCP(Generic[LifespanResultT]):
mime_type: Optional MIME type for the resource
tags: Optional set of tags for categorizing the resource
"""
# deprecated since 2.7.0
warnings.warn(
"The add_resource_fn method is deprecated. Use the resource decorator instead.",
DeprecationWarning,
stacklevel=2,
)
self._resource_manager.add_resource_or_template_from_fn(
fn=fn,
uri=uri,
@ -693,36 +689,54 @@ class FastMCP(Generic[LifespanResultT]):
)
def decorator(fn: AnyFunction) -> AnyFunction:
self.add_resource_fn(
fn=fn,
uri=uri,
name=name,
description=description,
mime_type=mime_type,
tags=tags,
from fastmcp.server.context import Context
# Check if this should be a template
has_uri_params = "{" in uri and "}" in uri
# check if the function has any parameters (other than injected context)
has_func_params = any(
p
for p in inspect.signature(fn).parameters.values()
if p.annotation is not Context
)
if has_uri_params or has_func_params:
template = ResourceTemplate.from_function(
fn=fn,
uri_template=uri,
name=name,
description=description,
mime_type=mime_type,
tags=tags,
)
self.add_template(template)
elif not has_uri_params and not has_func_params:
resource = Resource.from_function(
fn=fn,
uri=uri,
name=name,
description=description,
mime_type=mime_type,
tags=tags,
)
self.add_resource(resource)
else:
raise ValueError(
"Invalid resource or template definition due to a "
"mismatch between URI parameters and function parameters."
)
return fn
return decorator
def add_prompt(
self,
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
name: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
) -> None:
def add_prompt(self, prompt: Prompt) -> None:
"""Add a prompt to the server.
Args:
prompt: A Prompt instance to add
"""
self._prompt_manager.add_prompt_from_fn(
fn=fn,
name=name,
description=description,
tags=tags,
)
self._prompt_manager.add_prompt(prompt)
self._cache.clear()
def prompt(
@ -787,9 +801,16 @@ class FastMCP(Generic[LifespanResultT]):
"Did you forget to call it? Use @prompt() instead of @prompt"
)
def decorator(func: AnyFunction) -> AnyFunction:
self.add_prompt(func, name=name, description=description, tags=tags)
return DecoratedFunction(func)
def decorator(fn: AnyFunction) -> AnyFunction:
prompt = Prompt.from_function(
fn=fn,
name=name,
description=description,
tags=tags,
)
self.add_prompt(prompt)
return DecoratedFunction(fn)
return decorator

View file

@ -1,5 +1,6 @@
from __future__ import annotations as _annotations
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
@ -69,6 +70,11 @@ class ToolManager:
exclude_args: list[str] | None = None,
) -> Tool:
"""Add a tool to the server."""
# deprecated in 2.7.0
warnings.warn(
"ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.",
DeprecationWarning,
)
tool = Tool.from_function(
fn,
name=name,

View file

@ -9,6 +9,7 @@ from fastmcp.contrib.bulk_tool_caller.bulk_tool_caller import (
CallToolRequest,
CallToolRequestResult,
)
from fastmcp.tools.tool import Tool
ContentType = TextContent | ImageContent | EmbeddedResource
@ -68,9 +69,9 @@ def no_return_tool_result_factory(arg1: str) -> CallToolRequestResult:
def live_server_with_tool() -> FastMCP:
"""Fixture to create a FastMCP server instance with the echo_tool registered."""
server = FastMCP()
server.add_tool(echo_tool)
server.add_tool(error_tool)
server.add_tool(no_return_tool)
server.add_tool(Tool.from_function(echo_tool))
server.add_tool(Tool.from_function(error_tool))
server.add_tool(Tool.from_function(no_return_tool))
return server

View file

@ -3,7 +3,7 @@ from urllib.parse import quote
from fastmcp.client.client import Client
from fastmcp.server.server import FastMCP
from fastmcp.tools.tool import FunctionTool
from fastmcp.tools.tool import FunctionTool, Tool
async def test_import_basic_functionality():
@ -199,7 +199,7 @@ async def test_tool_custom_name_preserved_when_imported():
def fetch_data(query: str) -> str:
return f"Data for query: {query}"
api_app.add_tool(fetch_data, name="get_data")
api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
await main_app.import_server("api", api_app)
# Check that the tool is accessible by its prefixed name
@ -219,7 +219,7 @@ async def test_call_imported_custom_named_tool():
def fetch_data(query: str) -> str:
return f"Data for query: {query}"
api_app.add_tool(fetch_data, name="get_data")
api_app.add_tool(Tool.from_function(fetch_data, name="get_data"))
await main_app.import_server("api", api_app)
async with Client(main_app) as client:
@ -235,7 +235,7 @@ async def test_first_level_importing_with_custom_name():
def calculate_value(input: int) -> int:
return input * 2
provider_app.add_tool(calculate_value, name="compute")
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
await service_app.import_server("provider", provider_app)
# Tool is accessible in the service app with the first prefix
@ -254,7 +254,7 @@ async def test_nested_importing_preserves_prefixes():
def calculate_value(input: int) -> int:
return input * 2
provider_app.add_tool(calculate_value, name="compute")
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
await service_app.import_server("provider", provider_app)
await main_app.import_server("service", service_app)
@ -272,7 +272,7 @@ async def test_call_nested_imported_tool():
def calculate_value(input: int) -> int:
return input * 2
provider_app.add_tool(calculate_value, name="compute")
provider_app.add_tool(Tool.from_function(calculate_value, name="compute"))
await service_app.import_server("provider", provider_app)
await main_app.import_server("service", service_app)

View file

@ -6,6 +6,7 @@ from pydantic import Field
from fastmcp import Client, FastMCP
from fastmcp.exceptions import NotFoundError
from fastmcp.prompts.prompt import Prompt
from fastmcp.server.server import (
MountedServer,
add_resource_prefix,
@ -13,6 +14,7 @@ from fastmcp.server.server import (
remove_resource_prefix,
)
from fastmcp.tools import FunctionTool
from fastmcp.tools.tool import Tool
class TestCreateServer:
@ -173,7 +175,7 @@ class TestToolDecorator:
return self.x + y
obj = MyClass(10)
mcp.add_tool(obj.add)
mcp.add_tool(Tool.from_function(obj.add))
result = await mcp._mcp_call_tool("add", {"y": 2})
assert result[0].text == "12" # type: ignore[attr-defined]
@ -187,7 +189,7 @@ class TestToolDecorator:
def add(cls, y: int) -> int:
return cls.x + y
mcp.add_tool(MyClass.add)
mcp.add_tool(Tool.from_function(MyClass.add))
result = await mcp._mcp_call_tool("add", {"y": 2})
assert result[0].text == "12" # type: ignore[attr-defined]
@ -223,7 +225,7 @@ class TestToolDecorator:
async def add(cls, y: int) -> int:
return cls.x + y
mcp.add_tool(MyClass.add)
mcp.add_tool(Tool.from_function(MyClass.add))
result = await mcp._mcp_call_tool("add", {"y": 2})
assert result[0].text == "12" # type: ignore[attr-defined]
@ -235,7 +237,7 @@ class TestToolDecorator:
async def add(x: int, y: int) -> int:
return x + y
mcp.add_tool(MyClass.add)
mcp.add_tool(Tool.from_function(MyClass.add))
result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2})
assert result[0].text == "3" # type: ignore[attr-defined]
@ -260,7 +262,7 @@ class TestToolDecorator:
"""Multiply two numbers."""
return a * b
mcp.add_tool(multiply, name="custom_multiply")
mcp.add_tool(Tool.from_function(multiply, name="custom_multiply"))
# Check that the tool is registered with the custom name
tools = await mcp.get_tools()
@ -386,6 +388,7 @@ class TestResourceDecorator:
return f"{self.prefix} Hello, world!"
obj = MyClass("My prefix:")
mcp.add_resource_fn(
obj.get_data, uri="resource://data", name="instance-resource"
)
@ -678,7 +681,7 @@ class TestPromptDecorator:
return f"{self.prefix} Hello, world!"
obj = MyClass("My prefix:")
mcp.add_prompt(obj.test_prompt, name="test_prompt")
mcp.add_prompt(Prompt.from_function(obj.test_prompt, name="test_prompt"))
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")
@ -696,7 +699,7 @@ class TestPromptDecorator:
def test_prompt(cls) -> str:
return f"{cls.prefix} Hello, world!"
mcp.add_prompt(MyClass.test_prompt, name="test_prompt")
mcp.add_prompt(Prompt.from_function(MyClass.test_prompt, name="test_prompt"))
async with Client(mcp) as client:
result = await client.get_prompt("test_prompt")

View file

@ -19,9 +19,10 @@ from pydantic import AnyUrl, Field
from fastmcp import Client, Context, FastMCP
from fastmcp.client.transports import FastMCPTransport
from fastmcp.exceptions import ToolError
from fastmcp.prompts.prompt import EmbeddedResource, PromptMessage
from fastmcp.prompts.prompt import EmbeddedResource, Prompt, PromptMessage
from fastmcp.resources import FileResource
from fastmcp.resources.resource import FunctionResource
from fastmcp.tools.tool import Tool
from fastmcp.utilities.types import Image
@ -691,7 +692,7 @@ class TestToolContextInjection:
async def __call__(self, x: int, ctx: Context) -> int:
return x + int(ctx.request_id)
mcp.add_tool(MyTool())
mcp.add_tool(Tool.from_function(MyTool(), name="MyTool"))
async with Client(mcp) as client:
result = await client.call_tool("MyTool", {"x": 2})
@ -1285,7 +1286,7 @@ class TestPromptContext:
def __call__(self, name: str, ctx: Context) -> str:
return f"Hello, {name}! {ctx.request_id}"
mcp.add_prompt(MyPrompt(), name="my_prompt")
mcp.add_prompt(Prompt.from_function(MyPrompt(), name="my_prompt")) # noqa: F821
async with Client(mcp) as client:
result = await client.get_prompt("my_prompt", {"name": "World"})

View file

@ -3,6 +3,7 @@ from typing import Any
from mcp.types import ToolAnnotations
from fastmcp import Client, FastMCP
from fastmcp.tools.tool import Tool
async def test_tool_annotations_in_tool_manager():
@ -169,7 +170,7 @@ async def test_add_tool_method_annotations():
"""Create a new item."""
return {"name": name, "value": value}
mcp.add_tool(
tool = Tool.from_function(
create_item,
name="create_item",
annotations=ToolAnnotations(
@ -179,6 +180,8 @@ async def test_add_tool_method_annotations():
),
)
mcp.add_tool(tool)
# Check internal tool objects directly
tools = mcp._tool_manager.list_tools()
assert len(tools) == 1
@ -196,7 +199,7 @@ async def test_tool_functionality_with_annotations():
"""Create a new item."""
return {"name": name, "value": value}
mcp.add_tool(
tool = Tool.from_function(
create_item,
name="create_item",
annotations=ToolAnnotations(
@ -205,6 +208,7 @@ async def test_tool_functionality_with_annotations():
destructiveHint=False,
),
)
mcp.add_tool(tool)
# Use the tool to verify functionality is preserved
async with Client(mcp) as client:

View file

@ -4,6 +4,7 @@ import pytest
from mcp.types import TextContent
from fastmcp import Client, FastMCP
from fastmcp.tools.tool import Tool
async def test_tool_exclude_args_in_tool_manager():
@ -53,7 +54,12 @@ async def test_add_tool_method_exclude_args():
pass
return {"name": name, "value": value}
mcp.add_tool(create_item, name="create_item", exclude_args=["state"])
tool = Tool.from_function(
create_item,
name="create_item",
exclude_args=["state"],
)
mcp.add_tool(tool)
# Check internal tool objects directly
tools = mcp._tool_manager.list_tools()
@ -77,7 +83,12 @@ async def test_tool_functionality_with_exclude_args():
pass
return {"name": name, "value": value}
mcp.add_tool(create_item, name="create_item", exclude_args=["state"])
tool = Tool.from_function(
create_item,
name="create_item",
exclude_args=["state"],
)
mcp.add_tool(tool)
# Use the tool to verify functionality is preserved
async with Client(mcp) as client: