diff --git a/src/mxcp/server/interfaces/server/mcp.py b/src/mxcp/server/interfaces/server/mcp.py index 079254f0..f079ac75 100644 --- a/src/mxcp/server/interfaces/server/mcp.py +++ b/src/mxcp/server/interfaces/server/mcp.py @@ -1249,6 +1249,22 @@ async def _body(**kwargs: Any) -> Any: f"Authenticated user: {user_context.username} (provider: {user_context.provider})" ) + # Apply parameter type conversion as defensive measure + # Even though SDK handles validation, ensure JSON float values are converted to integers + converted_params = kwargs.copy() + for param in parameters: + param_name = param.get("name") + param_type = param.get("type") + if param_name in converted_params and param_type: + try: + converted_params[param_name] = self._convert_param_type( + converted_params[param_name], param_type + ) + except Exception as e: + logger.debug(f"Parameter conversion failed for {param_name}: {e}") + # Continue with original value if conversion fails + pass + # run through new SDK executor (handles type conversion automatically) if self.execution_engine is None: raise RuntimeError("Execution engine not initialized") @@ -1274,7 +1290,7 @@ async def _body(**kwargs: Any) -> Any: result, policy_info = await execute_endpoint_with_engine_and_policy( endpoint_type=endpoint_type.value, name=name, - params=kwargs, # No manual conversion needed - SDK handles it + params=converted_params, # Use converted parameters user_config=self.user_config, site_config=self.site_config, execution_engine=self.execution_engine, diff --git a/tests/server/fixtures/integration/python/test_endpoints.py b/tests/server/fixtures/integration/python/test_endpoints.py index 3bbd3b1b..cc7ddfca 100644 --- a/tests/server/fixtures/integration/python/test_endpoints.py +++ b/tests/server/fixtures/integration/python/test_endpoints.py @@ -121,3 +121,40 @@ def process_user_data(user_data: Dict[str, Any]) -> Dict[str, Any]: } return {"original_data": user_data, "analysis": analysis, "processing_status": "success"} + + +def check_integer_parameter(top_n: int) -> Dict[str, Any]: + """Test function that expects an integer parameter and fails if it gets a float. + + This function reproduces the bug where JSON float values like 0.0 are not + converted to integers before being passed to Python functions. + """ + # Log the actual type and value received for debugging + actual_type = type(top_n) + + # This assertion should pass if type conversion is working correctly + # If this fails, it means the bug exists - float values are not being converted to int + if not isinstance(top_n, int): + return { + "top_n": top_n, + "type_received": str(actual_type), + "selected_items": [], + "test_passed": False, + "error": f"Expected int, got {actual_type}: {top_n}", + } + + # Use the parameter as an array index to demonstrate why integers are needed + test_array = ["first", "second", "third", "fourth", "fifth"] + + # This would fail with a float even if it's 0.0 + if top_n < 0 or top_n >= len(test_array): + selected_items = [] + else: + selected_items = test_array[:top_n] if top_n > 0 else [] + + return { + "top_n": top_n, + "type_received": str(actual_type), + "selected_items": selected_items, + "test_passed": True, + } diff --git a/tests/server/fixtures/integration/tools/check_integer_parameter.yml b/tests/server/fixtures/integration/tools/check_integer_parameter.yml new file mode 100644 index 00000000..5a3841d5 --- /dev/null +++ b/tests/server/fixtures/integration/tools/check_integer_parameter.yml @@ -0,0 +1,26 @@ +mxcp: 1 +tool: + name: check_integer_parameter + description: Test function that expects an integer parameter + language: python + source: + file: ../python/test_endpoints.py + parameters: + - name: top_n + type: integer + description: Number of top items to return + minimum: 0 + default: 0 + return: + type: object + properties: + top_n: + type: integer + type_received: + type: string + selected_items: + type: array + items: + type: string + test_passed: + type: boolean diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index 8e590fe9..99d17946 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -415,6 +415,40 @@ async def test_secret_access(self, integration_fixture_dir): assert result["api_key"] == "initial_key_123" assert result["endpoint"] == "https://api.example.com" + @pytest.mark.asyncio + async def test_integer_parameter_conversion(self, integration_fixture_dir): + """Test that integer parameters are properly converted from JSON float values.""" + with ServerProcess(integration_fixture_dir) as server: + server.start() + + async with MCPTestClient(server.port) as client: + # Test with float value 0.0 - this should be converted to int(0) + result = await client.call_tool("check_integer_parameter", {"top_n": 0.0}) + + # If the bug exists, test_passed will be False and we'll get an error + if not result["test_passed"]: + pytest.fail( + f"Integer conversion bug detected: {result.get('error', 'Unknown error')}" + ) + + assert result["top_n"] == 0 + assert result["type_received"] == "" + assert result["selected_items"] == [] + assert result["test_passed"] is True + + # Test with float value 2.0 - this should be converted to int(2) + result = await client.call_tool("check_integer_parameter", {"top_n": 2.0}) + + if not result["test_passed"]: + pytest.fail( + f"Integer conversion bug detected: {result.get('error', 'Unknown error')}" + ) + + assert result["top_n"] == 2 + assert result["type_received"] == "" + assert result["selected_items"] == ["first", "second"] + assert result["test_passed"] is True + @pytest.mark.asyncio async def test_reload_with_external_ref(self, integration_fixture_dir): """Test reload with external references (env vars, files)."""