Skip to content

Commit b88e728

Browse files
author
Robin1987China
committed
fix: preserve parameter types in tool() and resource() decorators with ParamSpec (#1822)
Replace the type-erasing _CallableT with ParamSpec for the tool() and resource() decorators, which are pure pass-through and benefit fully from parameter type preservation. This allows type checkers to validate decorated function signatures without # type: ignore. completion() and prompt() keep explicit Callable[..., Any] because they feed into downstream type-constrained APIs where ParamSpec would create false type errors. AI assistance: Fix implemented with AI assistance (opencode). I've reviewed every changed line and validated with pyright + pytest.
1 parent a527142 commit b88e728

1 file changed

Lines changed: 11 additions & 10 deletions

File tree

src/mcp/server/mcpserver/server.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import re
88
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
99
from contextlib import AbstractAsyncContextManager, asynccontextmanager
10-
from typing import Any, Generic, Literal, TypeVar, overload
10+
from typing import Any, Generic, Literal, ParamSpec, TypeVar, overload
1111

1212
import anyio
1313
import pydantic_core
@@ -74,7 +74,8 @@
7474

7575
logger = get_logger(__name__)
7676

77-
_CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
77+
P = ParamSpec("P")
78+
R = TypeVar("R")
7879

7980

8081
class Settings(BaseSettings, Generic[LifespanResultT]):
@@ -508,7 +509,7 @@ def tool(
508509
icons: list[Icon] | None = None,
509510
meta: dict[str, Any] | None = None,
510511
structured_output: bool | None = None,
511-
) -> Callable[[_CallableT], _CallableT]:
512+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
512513
"""Decorator to register a tool.
513514
514515
Tools can optionally request a Context object by adding a parameter with the
@@ -554,7 +555,7 @@ async def async_tool(x: int, context: Context) -> str:
554555
"The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool"
555556
)
556557

557-
def decorator(fn: _CallableT) -> _CallableT:
558+
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
558559
self.add_tool(
559560
fn,
560561
name=name,
@@ -569,7 +570,7 @@ def decorator(fn: _CallableT) -> _CallableT:
569570

570571
return decorator
571572

572-
def completion(self):
573+
def completion(self) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
573574
"""Decorator to register a completion handler.
574575
575576
The completion handler receives:
@@ -588,7 +589,7 @@ async def handle_completion(ref, argument, context):
588589
```
589590
"""
590591

591-
def decorator(func: _CallableT) -> _CallableT:
592+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
592593
async def handler(
593594
ctx: ServerRequestContext[LifespanResultT], params: CompleteRequestParams
594595
) -> CompleteResult:
@@ -621,7 +622,7 @@ def resource(
621622
icons: list[Icon] | None = None,
622623
annotations: Annotations | None = None,
623624
meta: dict[str, Any] | None = None,
624-
) -> Callable[[_CallableT], _CallableT]:
625+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
625626
"""Decorator to register a function as a resource.
626627
627628
The function will be called when the resource is read to generate its content.
@@ -671,7 +672,7 @@ async def get_weather(city: str) -> str:
671672
"Did you forget to call it? Use @resource('uri') instead of @resource"
672673
)
673674

674-
def decorator(fn: _CallableT) -> _CallableT:
675+
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
675676
# Check if this should be a template
676677
sig = inspect.signature(fn)
677678
has_uri_params = "{" in uri and "}" in uri
@@ -736,7 +737,7 @@ def prompt(
736737
title: str | None = None,
737738
description: str | None = None,
738739
icons: list[Icon] | None = None,
739-
) -> Callable[[_CallableT], _CallableT]:
740+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
740741
"""Decorator to register a prompt.
741742
742743
Args:
@@ -781,7 +782,7 @@ async def analyze_file(path: str) -> list[Message]:
781782
"Did you forget to call it? Use @prompt() instead of @prompt"
782783
)
783784

784-
def decorator(func: _CallableT) -> _CallableT:
785+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
785786
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)
786787
self.add_prompt(prompt)
787788
return func

0 commit comments

Comments
 (0)