From 0927cc492afe96c657219551be9bc0c0d4f30fe4 Mon Sep 17 00:00:00 2001 From: Gyanu Mayank Date: Fri, 18 Sep 2026 08:47:47 +0530 Subject: [PATCH] fix: reject *args and **kwargs on tool signatures A tool with a variadic parameter is advertised with a schema that can never be satisfied, so the call always fails validation. Raise InvalidSignature at registration, the same way a leading underscore already is. Resource templates still allow **kwargs for URI variables that are only known at match time. --- .../server/mcpserver/resources/templates.py | 5 +++- .../mcpserver/utilities/func_metadata.py | 19 +++++++++++++++ tests/server/mcpserver/test_func_metadata.py | 24 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/mcp/server/mcpserver/resources/templates.py b/src/mcp/server/mcpserver/resources/templates.py index 621b2e9448..4964974cb7 100644 --- a/src/mcp/server/mcpserver/resources/templates.py +++ b/src/mcp/server/mcpserver/resources/templates.py @@ -151,7 +151,10 @@ def from_function( # Only the argument model is needed; a resource has no output schema to derive func_arg_metadata = func_metadata( - fn, skip_names=[context_kwarg] if context_kwarg is not None else [], structured_output=False + fn, + skip_names=[context_kwarg] if context_kwarg is not None else [], + structured_output=False, + allow_variadic=True, ) parameters = func_arg_metadata.arg_model.model_json_schema() diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index 0ffac07c4e..f25165224a 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -276,6 +276,8 @@ def func_metadata( func: Callable[..., Any], skip_names: Sequence[str] = (), structured_output: bool | None = None, + *, + allow_variadic: bool = False, ) -> FuncMetadata: """Given a function, return metadata including a Pydantic model representing its signature. @@ -293,6 +295,11 @@ def func_metadata( func: The function to convert to a Pydantic model skip_names: A list of parameter names to skip. These will not be included in the model. + allow_variadic: If False (the default, used for tools), ``*args`` and + ``**kwargs`` parameters raise :class:`InvalidSignature` because JSON + Schema cannot express them and the resulting tool would be uncallable. + Resource templates pass True so a catch-all ``**kwargs`` can receive + URI variables that are only known at match time. structured_output: Controls whether the tool's output is structured or unstructured - If None, auto-detects based on the function's return type annotation - If True, creates a structured tool (return type annotation permitting) @@ -331,6 +338,18 @@ def func_metadata( raise InvalidSignature(f"Parameter {param.name} of {func.__name__} cannot start with '_'") if param.name in skip_names: continue + if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): + if allow_variadic: + continue + kind = ( + "variadic positional (*args)" + if param.kind is inspect.Parameter.VAR_POSITIONAL + else "variadic keyword (**kwargs)" + ) + raise InvalidSignature( + f"Parameter {param.name} of {func.__name__} is a {kind} parameter; " + "JSON Schema cannot express *args or **kwargs" + ) annotation = param.annotation if param.annotation is not inspect.Parameter.empty else Any field_name = param.name diff --git a/tests/server/mcpserver/test_func_metadata.py b/tests/server/mcpserver/test_func_metadata.py index dba0637ded..556a8927d9 100644 --- a/tests/server/mcpserver/test_func_metadata.py +++ b/tests/server/mcpserver/test_func_metadata.py @@ -1520,3 +1520,27 @@ def fn() -> StepA | StepB: ... # pragma: no branch meta = func_metadata(fn) assert meta.output_schema is None + + +def test_variadic_kwargs_rejected_for_tools(): + def with_kwargs(x: int, **kwargs: str) -> str: # pragma: no cover + return f"{x} {kwargs}" + + with pytest.raises(InvalidSignature, match=r"\*\*kwargs"): + func_metadata(with_kwargs) + + schema = func_metadata(with_kwargs, allow_variadic=True).arg_model.model_json_schema() + assert "kwargs" not in schema.get("properties", {}) + assert "x" in schema.get("properties", {}) + + +def test_variadic_args_rejected_for_tools(): + def with_args(x: int, *args: str) -> str: # pragma: no cover + return f"{x} {args}" + + with pytest.raises(InvalidSignature, match=r"\*args"): + func_metadata(with_args) + + schema = func_metadata(with_args, allow_variadic=True).arg_model.model_json_schema() + assert "args" not in schema.get("properties", {}) + assert "x" in schema.get("properties", {})