From 9f5a9f34d829e7af87217c1aaf3546f3373f819c Mon Sep 17 00:00:00 2001 From: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:42:14 +0000 Subject: [PATCH] Fix nondeterministic required order in transformed tool schemas Co-authored-by: Bill Easton <6384545+strawgate@users.noreply.github.com> --- fastmcp_slim/fastmcp/tools/tool_transform.py | 8 +++++-- tests/tools/tool_transform/test_schemas.py | 23 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/fastmcp_slim/fastmcp/tools/tool_transform.py b/fastmcp_slim/fastmcp/tools/tool_transform.py index 910b89262..c11f58eda 100644 --- a/fastmcp_slim/fastmcp/tools/tool_transform.py +++ b/fastmcp_slim/fastmcp/tools/tool_transform.py @@ -707,7 +707,9 @@ class TransformedTool(Tool): schema = { "type": "object", "properties": new_props, - "required": list(new_required), + # Order by property insertion order so the schema is deterministic + # across processes (a bare set() order depends on PYTHONHASHSEED). + "required": [name for name in new_props if name in new_required], "additionalProperties": False, } @@ -899,7 +901,9 @@ class TransformedTool(Tool): result = { "type": "object", "properties": merged_props, - "required": list(final_required), + # Order by property insertion order so the schema is deterministic + # across processes (a bare set() order depends on PYTHONHASHSEED). + "required": [name for name in merged_props if name in final_required], "additionalProperties": False, } diff --git a/tests/tools/tool_transform/test_schemas.py b/tests/tools/tool_transform/test_schemas.py index 703dd7955..8b0d8069a 100644 --- a/tests/tools/tool_transform/test_schemas.py +++ b/tests/tools/tool_transform/test_schemas.py @@ -553,3 +553,26 @@ class TestInputSchema: "additionalProperties": False, } ) + + +class TestRequiredOrdering: + def test_required_follows_property_order(self): + """Transformed `required` should match property order deterministically. + + Regression test for a bug where `required` was built from a set, so its + order depended on PYTHONHASHSEED and differed across processes. + """ + + def base(alpha: int, beta: str, gamma: float, delta: bool, epsilon: int) -> str: + return "x" + + transformed = Tool.from_tool( + Tool.from_function(base), + transform_args={"alpha": ArgTransform(name="a")}, + ) + + properties = list(transformed.parameters["properties"]) + required = transformed.parameters["required"] + + assert required == [name for name in properties if name in set(required)] + assert required == ["a", "beta", "gamma", "delta", "epsilon"]