diff --git a/mcp_server.py b/mcp_server.py index f124039..10d04f9 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -57,28 +57,16 @@ def create_server() -> Any: @server.tool() def analyze_transport_capacity( + transport_preset: TransportPresetKey, roll_diameter_mm: float, core_diameter_mm: float, roll_width_m: float, roll_weight_kg: float, product_length_m: float, - transport_preset: TransportPresetKey | None = None, - length_m: float | None = None, - width_m: float | None = None, - height_m: float | None = None, - max_weight_kg: float | None = None, - margin_side_m: float | None = None, - margin_ceiling_m: float | None = None, ) -> dict[str, Any]: - """Analyze transport capacity for calculated rolls. For a chained request, first call get_article and calculate_roll_diameter, then pass every field from its transport_roll_inputs bundle unchanged: roll_diameter_mm, core_diameter_mm, roll_width_m, roll_weight_kg, and product_length_m. Do not choose among minimum, average, or maximum diameter; the bundle contains the browser-equivalent nominal transport diameter. product_length_m is used for reported m², not placement geometry. transport_preset must be one of the listed canonical identifiers: lkw_sattelzug means LKW Sattelzug / semi-trailer and lkw_tandem means LKW Tandem. Alternatively, supply all custom transport dimensions in m and max_weight_kg. This stateless tool does not recalculate a roll; roll_weight_kg is consumed exactly as supplied.""" + """Use this tool when the user asks how many rolls fit on a known RollCalc transport type. A named transport type is already fully defined by its RollCalc preset: do not ask the user for vehicle dimensions or payload, and do not estimate or invent vehicle data. Use these canonical mappings: LKW-Sattelzug -> lkw_sattelzug; LKW-Tandem -> lkw_tandem; 20ft Container -> container_20ft; 40ft Container -> container_40ft; 40ft High Cube -> container_40ft_hc. If the user asks how many rolls fit on an LKW-Sattelzug, call this tool with transport_preset=\"lkw_sattelzug\". For a chained request, first call get_article and calculate_roll_diameter, then pass every field from its transport_roll_inputs bundle unchanged: roll_diameter_mm, core_diameter_mm, roll_width_m, roll_weight_kg, and product_length_m. Do not choose among minimum, average, or maximum diameter; the bundle contains the browser-equivalent nominal transport diameter. product_length_m is used for reported m², not placement geometry. This stateless tool does not recalculate a roll; roll_weight_kg is consumed exactly as supplied.""" return analyze_transport_capacity_result( transport_preset=transport_preset, - length_m=length_m, - width_m=width_m, - height_m=height_m, - max_weight_kg=max_weight_kg, - margin_side_m=margin_side_m, - margin_ceiling_m=margin_ceiling_m, roll_diameter_mm=roll_diameter_mm, core_diameter_mm=core_diameter_mm, roll_width_m=roll_width_m, diff --git a/rollcalc_mcp_tools.py b/rollcalc_mcp_tools.py index 7c18b5f..5479f3d 100644 --- a/rollcalc_mcp_tools.py +++ b/rollcalc_mcp_tools.py @@ -59,28 +59,16 @@ def calculate_roll_diameter_result( def analyze_transport_capacity_result( *, - transport_preset: str | None = None, - length_m: float | None = None, - width_m: float | None = None, - height_m: float | None = None, - max_weight_kg: float | None = None, - margin_side_m: float | None = None, - margin_ceiling_m: float | None = None, - roll_diameter_mm: float | None = None, - core_diameter_mm: float | None = None, - roll_width_m: float | None = None, - roll_weight_kg: float | None = None, - product_length_m: float | None = None, + transport_preset: str, + roll_diameter_mm: float, + core_diameter_mm: float, + roll_width_m: float, + roll_weight_kg: float, + product_length_m: float, ) -> dict[str, Any]: - """Map MCP arguments to the shared transport-analysis input shape.""" + """Map the preset-only MCP contract to the shared transport domain.""" return analyze_transport({ "transport_preset": transport_preset, - "length_m": length_m, - "width_m": width_m, - "height_m": height_m, - "max_weight_kg": max_weight_kg, - "margin_side_m": margin_side_m, - "margin_ceiling_m": margin_ceiling_m, "roll_diameter_mm": roll_diameter_mm, "core_diameter_mm": core_diameter_mm, "roll_width_m": roll_width_m, diff --git a/tests/test_mcp_adapter.py b/tests/test_mcp_adapter.py index 0c24e32..ef27d55 100644 --- a/tests/test_mcp_adapter.py +++ b/tests/test_mcp_adapter.py @@ -1,3 +1,4 @@ +import inspect import sys import types import unittest @@ -90,33 +91,25 @@ class McpAdapterTests(unittest.TestCase): helper.assert_called_once() self.assertEqual(result["transport_roll_inputs"], expected_inputs) - def test_transport_tool_delegates_for_preset_custom_and_invalid_cases(self): - for arguments in ( - { - "transport_preset": "container_20ft", - "roll_diameter_mm": 1000.0, - "core_diameter_mm": 150.0, - "roll_width_m": 2.0, - "roll_weight_kg": 1000.0, - "product_length_m": 50.0, - }, - { - "length_m": 10.0, - "width_m": 5.0, - "height_m": 5.0, - "max_weight_kg": 10000.0, - "roll_diameter_mm": 1000.0, - "core_diameter_mm": 150.0, - "roll_width_m": 1.0, - "roll_weight_kg": 1000.0, - }, - {"length_m": 0.0}, - ): - with self.subTest(arguments=arguments): - self.assertEqual( - analyze_transport_capacity_result(**arguments), - analyze_transport(arguments), - ) + def test_transport_tool_delegates_to_domain_with_lkw_sattelzug_preset(self): + arguments = { + "transport_preset": "lkw_sattelzug", + "roll_diameter_mm": 1000.0, + "core_diameter_mm": 150.0, + "roll_width_m": 2.0, + "roll_weight_kg": 1000.0, + "product_length_m": 50.0, + } + + self.assertEqual( + analyze_transport_capacity_result(**arguments), + analyze_transport(arguments), + ) + result = analyze_transport_capacity_result(**arguments) + self.assertEqual(result["status"], "success") + self.assertEqual(result["analysis"]["transport_preset"], "lkw_sattelzug") + self.assertEqual(result["analysis"]["final_rolls"], 24) + self.assertEqual(result["analysis"]["limiting"]["name"], "Weight") def test_server_registers_exactly_the_three_stdio_poc_tools(self): mcp = types.ModuleType("mcp") @@ -150,12 +143,13 @@ class McpAdapterTests(unittest.TestCase): self.assertNotIn("include_roll_weight", roll_schema["properties"]) preset_schema = transport_schema["properties"]["transport_preset"] self.assertEqual( - preset_schema["anyOf"][0]["enum"], + preset_schema["enum"], [preset.key for preset in transport_presets()], ) self.assertEqual( transport_schema["required"], [ + "transport_preset", "roll_diameter_mm", "core_diameter_mm", "roll_width_m", @@ -164,15 +158,39 @@ class McpAdapterTests(unittest.TestCase): ], ) for field in transport_schema["required"]: + if field == "transport_preset": + continue self.assertEqual( transport_schema["properties"][field]["type"], "number" ) + for field in ( + "length_m", + "width_m", + "height_m", + "max_weight_kg", + "margin_side_m", + "margin_ceiling_m", + ): + self.assertNotIn(field, transport_schema["properties"]) + self.assertNotIn( + field, + inspect.signature(analyze_transport_capacity_result).parameters, + ) description = transport_tool.description for text in ( + "Use this tool when the user asks how many rolls fit on a known RollCalc transport type", + "do not ask the user for vehicle dimensions or payload", + "LKW-Sattelzug -> lkw_sattelzug", + "LKW-Tandem -> lkw_tandem", + "20ft Container -> container_20ft", + "40ft Container -> container_40ft", + "40ft High Cube -> container_40ft_hc", + 'transport_preset=\"lkw_sattelzug\"', "first call get_article and calculate_roll_diameter", "transport_roll_inputs bundle unchanged", "Do not choose among minimum, average, or maximum diameter", "product_length_m", + "do not estimate or invent", "stateless tool does not recalculate a roll", ): self.assertIn(text, description)