Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 14 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,17 @@ 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:
return topic

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