Skip to content

Commit 02ccedf

Browse files
committed
refactor!: make tool registration object-based
BREAKING CHANGE: MCPServer.add_tool() and ToolManager.add_tool() now require prebuilt Tool objects. Use Tool.from_function() or the @mcp.tool() decorator to construct tools before registration.
1 parent f27d2aa commit 02ccedf

File tree

6 files changed

+138
-148
lines changed

6 files changed

+138
-148
lines changed

docs/migration.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,34 @@ async def my_tool(x: int, ctx: Context) -> str:
428428

429429
The internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.
430430

431+
### Tool registration now accepts prebuilt `Tool` objects
432+
433+
`MCPServer.add_tool()` and `ToolManager.add_tool()` now expect a fully constructed `Tool` instance, matching the resource registration pattern. Build tools with `Tool.from_function(...)` or register them through the `@mcp.tool()` decorator, which still handles construction for you.
434+
435+
**Before (v1):**
436+
437+
```python
438+
def add(a: int, b: int) -> int:
439+
return a + b
440+
441+
mcp.add_tool(add)
442+
```
443+
444+
**After (v2):**
445+
446+
```python
447+
from mcp.server.mcpserver.tools import Tool
448+
449+
450+
def add(a: int, b: int) -> int:
451+
return a + b
452+
453+
454+
mcp.add_tool(Tool.from_function(add))
455+
```
456+
457+
If you need to customize the tool metadata before registration, build the `Tool` first and then pass it to `add_tool()`.
458+
431459
### Registering lowlevel handlers on `MCPServer` (workaround)
432460

433461
`MCPServer` does not expose public APIs for `subscribe_resource`, `unsubscribe_resource`, or `set_logging_level` handlers. In v1, the workaround was to reach into the private lowlevel server and use its decorator methods:

src/mcp/server/mcpserver/server.py

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -455,45 +455,13 @@ async def read_resource(
455455
# If an exception happens when reading the resource, we should not leak the exception to the client.
456456
raise ResourceError(f"Error reading resource {uri}") from exc
457457

458-
def add_tool(
459-
self,
460-
fn: Callable[..., Any],
461-
name: str | None = None,
462-
title: str | None = None,
463-
description: str | None = None,
464-
annotations: ToolAnnotations | None = None,
465-
icons: list[Icon] | None = None,
466-
meta: dict[str, Any] | None = None,
467-
structured_output: bool | None = None,
468-
) -> None:
458+
def add_tool(self, tool: Tool) -> None:
469459
"""Add a tool to the server.
470460
471-
The tool function can optionally request a Context object by adding a parameter
472-
with the Context type annotation. See the @tool decorator for examples.
473-
474461
Args:
475-
fn: The function to register as a tool
476-
name: Optional name for the tool (defaults to function name)
477-
title: Optional human-readable title for the tool
478-
description: Optional description of what the tool does
479-
annotations: Optional ToolAnnotations providing additional tool information
480-
icons: Optional list of icons for the tool
481-
meta: Optional metadata dictionary for the tool
482-
structured_output: Controls whether the tool's output is structured or unstructured
483-
- If None, auto-detects based on the function's return type annotation
484-
- If True, creates a structured tool (return type annotation permitting)
485-
- If False, unconditionally creates an unstructured tool
462+
tool: A Tool instance to add
486463
"""
487-
self._tool_manager.add_tool(
488-
fn,
489-
name=name,
490-
title=title,
491-
description=description,
492-
annotations=annotations,
493-
icons=icons,
494-
meta=meta,
495-
structured_output=structured_output,
496-
)
464+
self._tool_manager.add_tool(tool)
497465

498466
def remove_tool(self, name: str) -> None:
499467
"""Remove a tool from the server by name.
@@ -562,7 +530,7 @@ async def async_tool(x: int, context: Context) -> str:
562530
)
563531

564532
def decorator(fn: _CallableT) -> _CallableT:
565-
self.add_tool(
533+
tool = Tool.from_function(
566534
fn,
567535
name=name,
568536
title=title,
@@ -572,6 +540,7 @@ def decorator(fn: _CallableT) -> _CallableT:
572540
meta=meta,
573541
structured_output=structured_output,
574542
)
543+
self.add_tool(tool)
575544
return fn
576545

577546
return decorator

src/mcp/server/mcpserver/tools/base.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from functools import cached_property
55
from typing import TYPE_CHECKING, Any
66

7-
from pydantic import BaseModel, Field
7+
from pydantic import BaseModel, Field, field_validator
88

99
from mcp.server.mcpserver.exceptions import ToolError
1010
from mcp.server.mcpserver.utilities.context_injection import find_context_parameter
@@ -36,6 +36,12 @@ class Tool(BaseModel):
3636
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this tool")
3737
meta: dict[str, Any] | None = Field(default=None, description="Optional metadata for this tool")
3838

39+
@field_validator("name")
40+
@classmethod
41+
def validate_name(cls, name: str) -> str:
42+
validate_and_warn_tool_name(name)
43+
return name
44+
3945
@cached_property
4046
def output_schema(self) -> dict[str, Any] | None:
4147
return self.fn_metadata.output_schema
@@ -56,8 +62,6 @@ def from_function(
5662
"""Create a Tool from a function."""
5763
func_name = name or fn.__name__
5864

59-
validate_and_warn_tool_name(func_name)
60-
6165
if func_name == "<lambda>":
6266
raise ValueError("You must provide a name for lambda functions")
6367

src/mcp/server/mcpserver/tools/tool_manager.py

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from __future__ import annotations
22

3-
from collections.abc import Callable
43
from typing import TYPE_CHECKING, Any
54

65
from mcp.server.mcpserver.exceptions import ToolError
76
from mcp.server.mcpserver.tools.base import Tool
87
from mcp.server.mcpserver.utilities.logging import get_logger
9-
from mcp.types import Icon, ToolAnnotations
108

119
if TYPE_CHECKING:
1210
from mcp.server.context import LifespanContextT, RequestT
@@ -25,13 +23,10 @@ def __init__(
2523
tools: list[Tool] | None = None,
2624
):
2725
self._tools: dict[str, Tool] = {}
26+
self.warn_on_duplicate_tools = warn_on_duplicate_tools
2827
if tools is not None:
2928
for tool in tools:
30-
if warn_on_duplicate_tools and tool.name in self._tools:
31-
logger.warning(f"Tool already exists: {tool.name}")
32-
self._tools[tool.name] = tool
33-
34-
self.warn_on_duplicate_tools = warn_on_duplicate_tools
29+
self.add_tool(tool)
3530

3631
def get_tool(self, name: str) -> Tool | None:
3732
"""Get tool by name."""
@@ -43,26 +38,9 @@ def list_tools(self) -> list[Tool]:
4338

4439
def add_tool(
4540
self,
46-
fn: Callable[..., Any],
47-
name: str | None = None,
48-
title: str | None = None,
49-
description: str | None = None,
50-
annotations: ToolAnnotations | None = None,
51-
icons: list[Icon] | None = None,
52-
meta: dict[str, Any] | None = None,
53-
structured_output: bool | None = None,
41+
tool: Tool,
5442
) -> Tool:
55-
"""Add a tool to the server."""
56-
tool = Tool.from_function(
57-
fn,
58-
name=name,
59-
title=title,
60-
description=description,
61-
annotations=annotations,
62-
icons=icons,
63-
meta=meta,
64-
structured_output=structured_output,
65-
)
43+
"""Add a tool to the manager."""
6644
existing = self._tools.get(tool.name)
6745
if existing:
6846
if self.warn_on_duplicate_tools:

0 commit comments

Comments
 (0)