Skip to content
27 changes: 21 additions & 6 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
asynccontextmanager,
)
from itertools import chain
from typing import Any, Generic, Literal
from typing import Any, Generic, Literal, overload

import anyio
import pydantic_core
Expand All @@ -37,7 +37,7 @@
from mcp.server.fastmcp.exceptions import ResourceError
from mcp.server.fastmcp.prompts import Prompt, PromptManager
from mcp.server.fastmcp.resources import FunctionResource, Resource, ResourceManager
from mcp.server.fastmcp.tools import ToolManager
from mcp.server.fastmcp.tools import Tool, ToolManager
from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger
from mcp.server.fastmcp.utilities.types import Image
from mcp.server.lowlevel.helper_types import ReadResourceContents
Expand Down Expand Up @@ -315,27 +315,42 @@ async def read_resource(self, uri: AnyUrl | str) -> Iterable[ReadResourceContent
logger.error(f"Error reading resource {uri}: {e}")
raise ResourceError(str(e))

@overload
def add_tool(self, fn: Tool) -> None: ...

@overload
def add_tool(
self,
fn: AnyFunction,
name: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
) -> None: ...

def add_tool(
self,
fn: AnyFunction | Tool,
name: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
) -> None:
"""Add a tool to the server.

The tool function can optionally request a Context object by adding a parameter
with the Context type annotation. See the @tool decorator for examples.

Args:
fn: The function to register as a tool
fn: The function to register as a tool or a Tool instance
name: Optional name for the tool (defaults to function name)
description: Optional description of what the tool does
annotations: Optional ToolAnnotations providing additional tool information
"""
self._tool_manager.add_tool(
fn, name=name, description=description, annotations=annotations
)
if isinstance(fn, Tool):
self._tool_manager.add_tool(fn)
else:
self._tool_manager.add_tool(
fn, name=name, description=description, annotations=annotations
)

def tool(
self,
Expand Down
26 changes: 22 additions & 4 deletions src/mcp/server/fastmcp/tools/tool_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations as _annotations

from collections.abc import Callable
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, overload

from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.tools.base import Tool
Expand Down Expand Up @@ -31,17 +31,35 @@ def list_tools(self) -> list[Tool]:
"""List all registered tools."""
return list(self._tools.values())

@overload
def add_tool(
self,
fn: Tool,
) -> Tool: ...

@overload
def add_tool(
self,
fn: Callable[..., Any],
name: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
) -> Tool: ...

def add_tool(
self,
fn: Callable[..., Any] | Tool,
name: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
) -> Tool:
"""Add a tool to the server."""
tool = Tool.from_function(
fn, name=name, description=description, annotations=annotations
)
if isinstance(fn, Tool):
tool = fn
else:
tool = Tool.from_function(
fn, name=name, description=description, annotations=annotations
)
existing = self._tools.get(tool.name)
if existing:
if self.warn_on_duplicate_tools:
Expand Down
29 changes: 28 additions & 1 deletion tests/server/fastmcp/test_tool_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

from mcp.server.fastmcp import Context, FastMCP
from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.tools import ToolManager
from mcp.server.fastmcp.tools import Tool, ToolManager
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase, FuncMetadata
from mcp.server.session import ServerSessionT
from mcp.shared.context import LifespanContextT
from mcp.types import ToolAnnotations
Expand All @@ -31,6 +32,32 @@ def add(a: int, b: int) -> int:
assert tool.parameters["properties"]["a"]["type"] == "integer"
assert tool.parameters["properties"]["b"]["type"] == "integer"

def test_add_tool_directly(self):
manager = ToolManager()

def add(a: int, b: int) -> int:
return a + b

class AddArguments(ArgModelBase):
a: int
b: int

fn_metadata = FuncMetadata(arg_model=AddArguments)

original_tool = Tool(
name="add",
description="Add two numbers.",
fn=add,
fn_metadata=fn_metadata,
is_async=False,
parameters=AddArguments.model_json_schema(),
context_kwarg=None,
annotations=None,
)
manager.add_tool(original_tool)
saved_tool = manager.get_tool("add")
assert saved_tool == original_tool

@pytest.mark.anyio
async def test_async_function(self):
"""Test registering and running an async function."""
Expand Down
Loading