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
5 changes: 4 additions & 1 deletion src/mcp/server/mcpserver/resources/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
19 changes: 19 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,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.

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 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,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", {})
Loading