Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/mcp/server/mcpserver/utilities/func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def model_dump_one_level(self) -> dict[str, Any]:
kwargs[output_name] = value
return kwargs

model_config = ConfigDict(arbitrary_types_allowed=True)
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")


class FuncMetadata(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions tests/client/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ async def test_client_list_tools(app: MCPServer):
description="Greet someone by name.",
input_schema={
"properties": {"name": {"title": "Name", "type": "string"}},
"additionalProperties": False,
"required": ["name"],
"title": "greetArguments",
"type": "object",
Expand Down
1 change: 1 addition & 0 deletions tests/docs_src/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def test_list_tools_returns_the_full_definition() -> None:
assert tool.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"},
Expand Down
3 changes: 2 additions & 1 deletion tests/docs_src/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ async def test_the_context_parameter_is_not_in_the_input_schema() -> None:
assert tool.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {"query": {"title": "Query", "type": "string"}},
"required": ["query"],
"title": "search_booksArguments",
Expand Down Expand Up @@ -56,7 +57,7 @@ async def test_a_context_only_tool_takes_no_arguments() -> None:
async with Client(tutorial002.mcp) as client:
tools = {tool.name: tool for tool in (await client.list_tools()).tools}
assert tools["describe_catalog"].input_schema == snapshot(
{"type": "object", "properties": {}, "title": "describe_catalogArguments"}
{"type": "object", "additionalProperties": False, "properties": {}, "title": "describe_catalogArguments"}
)


Expand Down
10 changes: 7 additions & 3 deletions tests/docs_src/test_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,23 @@ async def test_the_resolved_parameter_is_invisible_to_the_model() -> None:
assert tool.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {"title": {"title": "Title", "type": "string"}},
"required": ["title"],
"title": "reserve_bookArguments",
}
)


async def test_a_client_supplied_value_for_a_resolved_parameter_is_ignored() -> None:
"""tutorial001: the resolver's value is the only one the tool can receive."""
async def test_a_client_supplied_value_for_a_resolved_parameter_is_rejected() -> None:
"""tutorial001: resolved parameters are not in the schema, so extra arguments fail validation."""
async with Client(tutorial001.mcp) as client:
result = await client.call_tool("reserve_book", {"title": "Dune", "stock": {"title": "Dune", "copies": 999}})

assert result.content == [TextContent(type="text", text="Reserved 'Dune' (6 copies left).")]
assert result.is_error
assert isinstance(result.content[0], TextContent)
# pydantic's "Extra inputs are not permitted" wording changes across versions.
assert result.content[0].text.startswith("Error executing tool reserve_book:")


async def test_a_resolver_can_depend_on_another_resolver() -> None:
Expand Down
1 change: 1 addition & 0 deletions tests/docs_src/test_first_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ async def test_each_decorator_registers_one_primitive() -> None:
assert tool.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {
"a": {"title": "A", "type": "integer"},
"b": {"title": "B", "type": "integer"},
Expand Down
1 change: 1 addition & 0 deletions tests/docs_src/test_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async def test_context_parameter_never_reaches_the_input_schema() -> None:
assert tool.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {"genre": {"title": "Genre", "type": "string"}},
"required": ["genre"],
"title": "count_booksArguments",
Expand Down
1 change: 1 addition & 0 deletions tests/docs_src/test_real_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ async def test_the_host_sees_exactly_what_the_decorators_registered() -> None:
assert search.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {"query": {"title": "Query", "type": "string"}},
"required": ["query"],
"title": "search_booksArguments",
Expand Down
2 changes: 2 additions & 0 deletions tests/docs_src/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ async def test_signature_becomes_the_schema() -> None:
assert tool.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"title": "Limit", "type": "integer"},
Expand Down Expand Up @@ -49,6 +50,7 @@ async def test_default_value_makes_the_argument_optional() -> None:
assert tool.input_schema == snapshot(
{
"type": "object",
"additionalProperties": False,
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"},
Expand Down
14 changes: 13 additions & 1 deletion tests/server/mcpserver/test_func_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pytest
from dirty_equals import IsPartialDict
from mcp_types import CallToolResult, InputRequiredResult
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ValidationError

from mcp.server.mcpserver.exceptions import InvalidSignature
from mcp.server.mcpserver.utilities.func_metadata import func_metadata
Expand Down Expand Up @@ -275,6 +275,7 @@ async def test_lambda_function():

# Test schema
assert meta.arg_model.model_json_schema() == {
"additionalProperties": False,
"properties": {
"x": {"title": "x", "type": "string"},
"y": {"default": 5, "title": "y", "type": "string"},
Expand Down Expand Up @@ -464,6 +465,7 @@ def test_complex_function_json_schema():
"my_model_a_forward_ref",
"my_model_b",
],
"additionalProperties": False,
"title": "complex_arguments_fnArguments",
"type": "object",
}
Expand Down Expand Up @@ -1297,6 +1299,16 @@ def fn() -> CallToolResult | str | InputRequiredResult: ... # pragma: no branch
func_metadata(fn)


def test_unknown_argument_raises_validation_error():
"""SDK-defined: a tools/call argument name not on the tool schema fails validation."""

def read_doc(topic: str = "") -> str: ... # pragma: no branch

meta = func_metadata(read_doc)
with pytest.raises(ValidationError):
meta.arg_model.model_validate({"path": "something"})


def test_union_of_only_input_required_subclasses_yields_no_output_schema():
class StepA(InputRequiredResult):
pass
Expand Down
Loading