Skip to content

Decorator type signatures lose parameter types (use ParamSpec) #1822

@Morriz

Description

@Morriz

Issue: Decorator type signatures lose parameter types

Summary

The MCP server decorators (@server.call_tool(), @server.list_tools(), etc.) use Callable[..., Awaitable[...]] which discards parameter type information, preventing static type checkers from validating decorated functions.

Current Behavior

# In mcp/server/lowlevel/server.py
def call_tool(self, *, validate_input: bool = True):
    def decorator(
        func: Callable[
            ...,  # <-- Ellipsis loses parameter types!
            Awaitable[UnstructuredContent | StructuredContent | ...],
        ],
    ):
        # ...

When decorating a function:

@server.call_tool()  # type: ignore  # <-- Required!
async def call_tool(name: str, arguments: dict[str, object]) -> list[TextContent]:
    ...

Mypy cannot verify:

  • Parameter names/types are correct
  • Arguments are passed correctly to decorated function
  • Return type matches decorator constraints

Expected Behavior

Decorators should preserve parameter types using ParamSpec:

from typing import ParamSpec, TypeVar, Callable, Awaitable

P = ParamSpec('P')
R = TypeVar('R')

def call_tool(self, *, validate_input: bool = True):
    def decorator(
        func: Callable[P, Awaitable[R]]
    ) -> Callable[P, Awaitable[R]]:
        # ... wrapper implementation
        return func  # Type signature preserved

This would:

  • ✅ Preserve parameter types through decoration
  • ✅ Enable static type checking
  • ✅ Eliminate need for # type: ignore
  • ✅ Improve IDE autocomplete/hints

Environment

  • mcp version: 1.24.0
  • Python version: 3.14 (but affects 3.10+)
  • Type checker: mypy (strict mode)

Impact

The package includes py.typed marker claiming type support, but decorators break type checking. Projects using strict mypy must either:

  1. Disable type checking for entire MCP server modules
  2. Add # type: ignore to every decorated function
  3. Create local type stubs to work around the issue

Workaround

Currently using broad mypy overrides:

[[tool.mypy.overrides]]
module = "myproject.mcp_server"
disable_error_code = ["misc", "no-untyped-call", "explicit-any"]

This defeats the purpose of having py.typed.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Nice to haves, rare edge casesenhancementRequest for a new feature that's not currently supportedready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions