Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
77b11ea
1
Parent(s): 35d4f60
Update tests for server input validation
Browse files
tests/server/test_server_interactions.py
CHANGED
|
@@ -554,12 +554,12 @@ class TestToolParameters:
|
|
| 554 |
async with Client(mcp) as client:
|
| 555 |
with pytest.raises(
|
| 556 |
ToolError,
|
| 557 |
-
match="
|
| 558 |
):
|
| 559 |
await client.call_tool("my_tool", {"x": "not an int"})
|
| 560 |
|
| 561 |
async def test_tool_int_coercion(self):
|
| 562 |
-
"""Test
|
| 563 |
mcp = FastMCP()
|
| 564 |
|
| 565 |
@mcp.tool
|
|
@@ -567,12 +567,15 @@ class TestToolParameters:
|
|
| 567 |
return x + 1
|
| 568 |
|
| 569 |
async with Client(mcp) as client:
|
| 570 |
-
# String
|
| 571 |
-
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
| 573 |
|
| 574 |
async def test_tool_bool_coercion(self):
|
| 575 |
-
"""Test
|
| 576 |
mcp = FastMCP()
|
| 577 |
|
| 578 |
@mcp.tool
|
|
@@ -580,12 +583,18 @@ class TestToolParameters:
|
|
| 580 |
return not flag
|
| 581 |
|
| 582 |
async with Client(mcp) as client:
|
| 583 |
-
# String
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
| 586 |
|
| 587 |
-
|
| 588 |
-
|
|
|
|
|
|
|
|
|
|
| 589 |
|
| 590 |
async def test_annotated_field_validation(self):
|
| 591 |
mcp = FastMCP()
|
|
@@ -595,7 +604,10 @@ class TestToolParameters:
|
|
| 595 |
pass
|
| 596 |
|
| 597 |
async with Client(mcp) as client:
|
| 598 |
-
with pytest.raises(
|
|
|
|
|
|
|
|
|
|
| 599 |
await client.call_tool("analyze", {"x": 0})
|
| 600 |
|
| 601 |
async def test_default_field_validation(self):
|
|
@@ -606,7 +618,10 @@ class TestToolParameters:
|
|
| 606 |
pass
|
| 607 |
|
| 608 |
async with Client(mcp) as client:
|
| 609 |
-
with pytest.raises(
|
|
|
|
|
|
|
|
|
|
| 610 |
await client.call_tool("analyze", {"x": 0})
|
| 611 |
|
| 612 |
async def test_default_field_is_still_required_if_no_default_specified(self):
|
|
@@ -617,7 +632,9 @@ class TestToolParameters:
|
|
| 617 |
pass
|
| 618 |
|
| 619 |
async with Client(mcp) as client:
|
| 620 |
-
with pytest.raises(
|
|
|
|
|
|
|
| 621 |
await client.call_tool("analyze", {})
|
| 622 |
|
| 623 |
async def test_literal_type_validation_error(self):
|
|
@@ -628,7 +645,10 @@ class TestToolParameters:
|
|
| 628 |
pass
|
| 629 |
|
| 630 |
async with Client(mcp) as client:
|
| 631 |
-
with pytest.raises(
|
|
|
|
|
|
|
|
|
|
| 632 |
await client.call_tool("analyze", {"x": "c"})
|
| 633 |
|
| 634 |
async def test_literal_type_validation_success(self):
|
|
@@ -655,7 +675,10 @@ class TestToolParameters:
|
|
| 655 |
return x.value
|
| 656 |
|
| 657 |
async with Client(mcp) as client:
|
| 658 |
-
with pytest.raises(
|
|
|
|
|
|
|
|
|
|
| 659 |
await client.call_tool("analyze", {"x": "some-color"})
|
| 660 |
|
| 661 |
async def test_enum_type_validation_success(self):
|
|
@@ -688,7 +711,10 @@ class TestToolParameters:
|
|
| 688 |
result = await client.call_tool("analyze", {"x": 1.0})
|
| 689 |
assert result[0].text == "1.0" # type: ignore[attr-defined]
|
| 690 |
|
| 691 |
-
with pytest.raises(
|
|
|
|
|
|
|
|
|
|
| 692 |
await client.call_tool("analyze", {"x": "not a number"})
|
| 693 |
|
| 694 |
async def test_path_type(self):
|
|
@@ -714,7 +740,9 @@ class TestToolParameters:
|
|
| 714 |
return str(path)
|
| 715 |
|
| 716 |
async with Client(mcp) as client:
|
| 717 |
-
with pytest.raises(
|
|
|
|
|
|
|
| 718 |
await client.call_tool("send_path", {"path": 1})
|
| 719 |
|
| 720 |
async def test_uuid_type(self):
|
|
@@ -815,6 +843,7 @@ class TestToolParameters:
|
|
| 815 |
assert result[0].text == "1 day, 0:00:00" # type: ignore[attr-defined]
|
| 816 |
|
| 817 |
async def test_timedelta_type_parse_int(self):
|
|
|
|
| 818 |
mcp = FastMCP()
|
| 819 |
|
| 820 |
@mcp.tool
|
|
@@ -822,8 +851,12 @@ class TestToolParameters:
|
|
| 822 |
return str(x)
|
| 823 |
|
| 824 |
async with Client(mcp) as client:
|
| 825 |
-
|
| 826 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
|
| 828 |
|
| 829 |
class TestToolContextInjection:
|
|
|
|
| 554 |
async with Client(mcp) as client:
|
| 555 |
with pytest.raises(
|
| 556 |
ToolError,
|
| 557 |
+
match="Input validation error: 'not an int' is not of type 'integer'",
|
| 558 |
):
|
| 559 |
await client.call_tool("my_tool", {"x": "not an int"})
|
| 560 |
|
| 561 |
async def test_tool_int_coercion(self):
|
| 562 |
+
"""Test that invalid int input raises validation error."""
|
| 563 |
mcp = FastMCP()
|
| 564 |
|
| 565 |
@mcp.tool
|
|
|
|
| 567 |
return x + 1
|
| 568 |
|
| 569 |
async with Client(mcp) as client:
|
| 570 |
+
# String input should raise validation error (no coercion)
|
| 571 |
+
with pytest.raises(
|
| 572 |
+
ToolError,
|
| 573 |
+
match="Input validation error: '42' is not of type 'integer'",
|
| 574 |
+
):
|
| 575 |
+
await client.call_tool("add_one", {"x": "42"})
|
| 576 |
|
| 577 |
async def test_tool_bool_coercion(self):
|
| 578 |
+
"""Test that invalid bool input raises validation error."""
|
| 579 |
mcp = FastMCP()
|
| 580 |
|
| 581 |
@mcp.tool
|
|
|
|
| 583 |
return not flag
|
| 584 |
|
| 585 |
async with Client(mcp) as client:
|
| 586 |
+
# String input should raise validation error (no coercion)
|
| 587 |
+
with pytest.raises(
|
| 588 |
+
ToolError,
|
| 589 |
+
match="Input validation error: 'true' is not of type 'boolean'",
|
| 590 |
+
):
|
| 591 |
+
await client.call_tool("toggle", {"flag": "true"})
|
| 592 |
|
| 593 |
+
with pytest.raises(
|
| 594 |
+
ToolError,
|
| 595 |
+
match="Input validation error: 'false' is not of type 'boolean'",
|
| 596 |
+
):
|
| 597 |
+
await client.call_tool("toggle", {"flag": "false"})
|
| 598 |
|
| 599 |
async def test_annotated_field_validation(self):
|
| 600 |
mcp = FastMCP()
|
|
|
|
| 604 |
pass
|
| 605 |
|
| 606 |
async with Client(mcp) as client:
|
| 607 |
+
with pytest.raises(
|
| 608 |
+
ToolError,
|
| 609 |
+
match="Input validation error: 0 is less than the minimum of 1",
|
| 610 |
+
):
|
| 611 |
await client.call_tool("analyze", {"x": 0})
|
| 612 |
|
| 613 |
async def test_default_field_validation(self):
|
|
|
|
| 618 |
pass
|
| 619 |
|
| 620 |
async with Client(mcp) as client:
|
| 621 |
+
with pytest.raises(
|
| 622 |
+
ToolError,
|
| 623 |
+
match="Input validation error: 0 is less than the minimum of 1",
|
| 624 |
+
):
|
| 625 |
await client.call_tool("analyze", {"x": 0})
|
| 626 |
|
| 627 |
async def test_default_field_is_still_required_if_no_default_specified(self):
|
|
|
|
| 632 |
pass
|
| 633 |
|
| 634 |
async with Client(mcp) as client:
|
| 635 |
+
with pytest.raises(
|
| 636 |
+
ToolError, match="Input validation error: 'x' is a required property"
|
| 637 |
+
):
|
| 638 |
await client.call_tool("analyze", {})
|
| 639 |
|
| 640 |
async def test_literal_type_validation_error(self):
|
|
|
|
| 645 |
pass
|
| 646 |
|
| 647 |
async with Client(mcp) as client:
|
| 648 |
+
with pytest.raises(
|
| 649 |
+
ToolError,
|
| 650 |
+
match=r"Input validation error: 'c' is not one of \['a', 'b'\]",
|
| 651 |
+
):
|
| 652 |
await client.call_tool("analyze", {"x": "c"})
|
| 653 |
|
| 654 |
async def test_literal_type_validation_success(self):
|
|
|
|
| 675 |
return x.value
|
| 676 |
|
| 677 |
async with Client(mcp) as client:
|
| 678 |
+
with pytest.raises(
|
| 679 |
+
ToolError,
|
| 680 |
+
match=r"Input validation error: 'some-color' is not one of \['red', 'green', 'blue'\]",
|
| 681 |
+
):
|
| 682 |
await client.call_tool("analyze", {"x": "some-color"})
|
| 683 |
|
| 684 |
async def test_enum_type_validation_success(self):
|
|
|
|
| 711 |
result = await client.call_tool("analyze", {"x": 1.0})
|
| 712 |
assert result[0].text == "1.0" # type: ignore[attr-defined]
|
| 713 |
|
| 714 |
+
with pytest.raises(
|
| 715 |
+
ToolError,
|
| 716 |
+
match="Input validation error: 'not a number' is not valid under any of the given schemas",
|
| 717 |
+
):
|
| 718 |
await client.call_tool("analyze", {"x": "not a number"})
|
| 719 |
|
| 720 |
async def test_path_type(self):
|
|
|
|
| 740 |
return str(path)
|
| 741 |
|
| 742 |
async with Client(mcp) as client:
|
| 743 |
+
with pytest.raises(
|
| 744 |
+
ToolError, match="Input validation error: 1 is not of type 'string'"
|
| 745 |
+
):
|
| 746 |
await client.call_tool("send_path", {"path": 1})
|
| 747 |
|
| 748 |
async def test_uuid_type(self):
|
|
|
|
| 843 |
assert result[0].text == "1 day, 0:00:00" # type: ignore[attr-defined]
|
| 844 |
|
| 845 |
async def test_timedelta_type_parse_int(self):
|
| 846 |
+
"""Test that invalid timedelta input raises validation error."""
|
| 847 |
mcp = FastMCP()
|
| 848 |
|
| 849 |
@mcp.tool
|
|
|
|
| 851 |
return str(x)
|
| 852 |
|
| 853 |
async with Client(mcp) as client:
|
| 854 |
+
# Int input should raise validation error (no conversion)
|
| 855 |
+
with pytest.raises(
|
| 856 |
+
ToolError,
|
| 857 |
+
match="Input validation error: 1000 is not of type 'string'",
|
| 858 |
+
):
|
| 859 |
+
await client.call_tool("send_timedelta", {"x": 1000})
|
| 860 |
|
| 861 |
|
| 862 |
class TestToolContextInjection:
|