Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/mcp/server/mcpserver/tools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def from_function(
fn,
skip_names=skip_names,
structured_output=structured_output,
allow_var_params=False,
)
parameters = func_arg_metadata.arg_model.model_json_schema(by_alias=True)

Expand Down
12 changes: 12 additions & 0 deletions src/mcp/server/mcpserver/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ def func_metadata(
func: Callable[..., Any],
skip_names: Sequence[str] = (),
structured_output: bool | None = None,
allow_var_params: bool = True,
) -> FuncMetadata:
"""Given a function, return metadata including a Pydantic model representing its signature.

Expand All @@ -293,6 +294,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_var_params: When False, a `*args` or `**kwargs` parameter raises
`InvalidSignature`; a variadic parameter has no scalar schema and would
otherwise be modelled as an unusable normal field. Tools pass False;
resource templates leave it True since they use `**kwargs` for
runtime-determined URI variables.
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)
Expand Down Expand Up @@ -329,6 +335,12 @@ def func_metadata(
for param in params.values():
if param.name.startswith("_"): # pragma: no cover
raise InvalidSignature(f"Parameter {param.name} of {func.__name__} cannot start with '_'")
if not allow_var_params and param.kind in (
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD,
):
variadic = "*args" if param.kind is inspect.Parameter.VAR_POSITIONAL else "**kwargs"
raise InvalidSignature(f"Function {func.__name__} cannot have a {variadic} parameter")
if param.name in skip_names:
continue

Expand Down
33 changes: 33 additions & 0 deletions tests/server/mcpserver/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1520,3 +1520,36 @@ def fn() -> StepA | StepB: ... # pragma: no branch

meta = func_metadata(fn)
assert meta.output_schema is None


def test_var_positional_param_is_rejected_for_tools():
"""A tool signature with *args has no scalar schema and is rejected at registration."""

def func_with_var_args(a: int, *args: int) -> int: # pragma: no cover
return a

with pytest.raises(InvalidSignature) as exc_info:
func_metadata(func_with_var_args, allow_var_params=False)
assert "*args" in str(exc_info.value)


def test_var_keyword_param_is_rejected_for_tools():
"""A tool signature with **kwargs has no scalar schema and is rejected at registration."""

def func_with_var_kwargs(a: int, **kwargs: int) -> int: # pragma: no cover
return a

with pytest.raises(InvalidSignature) as exc_info:
func_metadata(func_with_var_kwargs, allow_var_params=False)
assert "**kwargs" in str(exc_info.value)


def test_var_keyword_param_is_allowed_by_default():
"""Resource templates keep using **kwargs for runtime-determined URI variables, so the
default (allow_var_params=True) must not reject a variadic parameter."""

def func_with_var_kwargs(**kwargs: str) -> str: # pragma: no cover
return ""

meta = func_metadata(func_with_var_kwargs)
assert isinstance(meta, FuncMetadata)
Loading