Skip to content

Positional-only handler parameters break tools, prompts and resource templates #3760

Open

Description

@PerryLink

Handlers registered with a positional-only parameter (def echo(x: str, /)) are accepted and advertised, but every call fails, for tools, prompts and resource templates alike.

Reproduction

from mcp.server.mcpserver import MCPServer
from mcp import Client

mcp = MCPServer("posonly")

@mcp.tool()
def echo(x: str, /) -> str:
    return f"got:{x}"

async with Client(mcp) as client:
    result = await client.call_tool("echo", {"x": "hi"})

Actual: tools/list advertises {"properties": {"x": {"type": "string"}}, "required": ["x"]}, but the call returns is_error=True with Error executing tool echo; the server logs TypeError: echo() got some positional-only arguments passed as keyword arguments: 'x'.

The same happens for prompts (def greet(name: str, /) → client.get_prompt raises MCPError: Internal server error, log ValueError: Error rendering prompt greet) and resource templates (@mcp.resource("posonly://{name}") def read(name: str, /) → MCPError: Error creating resource from template posonly://thing).

Expected: got:hi, the rendered prompt, and the resource content respectively.

Cause

The handlers are invoked with keyword arguments only:

  • src/mcp/server/mcpserver/utilities/func_metadata.py (call_fn: fn(**kwargs) / functools.partial(fn, **kwargs))
  • src/mcp/server/mcpserver/prompts/base.py (Prompt.render: same)
  • src/mcp/server/mcpserver/resources/templates.py (ResourceTemplate.create_resource: same)

There is no POSITIONAL_ONLY handling anywhere in src. The tool/prompt/resource input schemas are derived from the type hints, so the parameter is advertised as required and then cannot be bound.

A positional-only parameter is valid Python and the docs describe the handler signature as "just Python" (docs/servers/tools.md), so either the parameters should be bound positionally (a small helper that splits the leading positional-only run out of the kwargs dict, using declared defaults when omitted) or registration should reject positional-only parameters instead of advertising an uncallable handler.

Happy to prepare the binding change if maintainers want it — not opening a PR because the contributing guide asks for an assigned issue first.

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Oct 25, 2026
  2. Rajasimhareddybolla commented on Oct 25, 2026

    @Rajasimhareddybolla

    Confirmed on main. Because handlers are dispatched via fn(**kwargs) in call_fn, Prompt.render, and ResourceTemplate.create_resource, functions using Python 3.8+ positional-only syntax (/) fail at runtime with TypeError: got some positional-only arguments passed as keyword arguments despite generating valid JSON schemas.

    I have verified a fix on branch Rajasimhareddybolla:fix/pos-only-handler-parameters that splits positional-only parameters in signature order via a split_positional_arguments helper before invocation, along with regression tests covering sync/async tools, prompts, and resources (pytest, ruff, and pyright passing).

    I would love to be assigned to this issue and open the PR

  3. insuffer commented on Oct 27, 2026

    @insuffer

    Confirmed the reproduction on current main, all three surfaces, with the same output you reported:

    TOOL     is_error=True  content=['Error executing tool echo']
    PROMPT   raised MCPError: Internal server error
    RESOURCE raised MCPError: Error creating resource from template posonly://thing
    

    (server log for the tool case is the same TypeError: echo() got some positional-only arguments passed as keyword arguments: 'x')

    On the intended behaviour — I want to check the direction before writing anything, because there are two defensible answers and they are different changes:

    1. Make the call work: split the kwargs into positional and keyword parts at the three call sites, so a positional-only parameter receives its value positionally. This is what the issue's "Expected" section asks for.
    2. Reject at registration: refuse a handler with a positional-only parameter when it is registered, so the failure moves from call time to registration time and tools/list never advertises something that cannot be called.

    I have a patch for (1) — a shared helper next to func_metadata that walks the signature's positional-only parameters in order, plus the three call sites — but if you would rather have (2), or both, I will redo it that way.

    One detail worth a maintainer's eye either way: with (1), tools/list still advertises the parameter exactly as it does today, so a client's view is unchanged and only the invocation path differs. With (2), an existing server that registers such a handler today would start failing at startup, which is a behaviour change rather than a bug fix.

    For transparency per the AI contribution policy: I used an AI assistant to investigate and run the reproduction, and a person has read the output and is accountable for this comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions