From a10d3b32dbd72296eb29bc1293fb76047d5f9d02 Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Tue, 26 Aug 2025 17:46:50 +0200 Subject: [PATCH 1/7] Add test for integer parameter conversion from JSON float values --- .../integration/python/test_endpoints.py | 37 ++++++ .../fixtures/integration/test_params.json | 1 + .../tools/test_integer_no_schema.yml | 10 ++ .../tools/test_integer_parameter.yml | 26 ++++ tests/server/test_integer_conversion_bug.py | 124 ++++++++++++++++++ tests/server/test_integration.py | 30 +++++ 6 files changed, 228 insertions(+) create mode 100644 tests/server/fixtures/integration/test_params.json create mode 100644 tests/server/fixtures/integration/tools/test_integer_no_schema.yml create mode 100644 tests/server/fixtures/integration/tools/test_integer_parameter.yml create mode 100644 tests/server/test_integer_conversion_bug.py diff --git a/tests/server/fixtures/integration/python/test_endpoints.py b/tests/server/fixtures/integration/python/test_endpoints.py index 3bbd3b1b..a390047d 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 test_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/test_params.json b/tests/server/fixtures/integration/test_params.json new file mode 100644 index 00000000..ba66466c --- /dev/null +++ b/tests/server/fixtures/integration/test_params.json @@ -0,0 +1 @@ +0.0 diff --git a/tests/server/fixtures/integration/tools/test_integer_no_schema.yml b/tests/server/fixtures/integration/tools/test_integer_no_schema.yml new file mode 100644 index 00000000..a2d29b56 --- /dev/null +++ b/tests/server/fixtures/integration/tools/test_integer_no_schema.yml @@ -0,0 +1,10 @@ +mxcp: 1 +tool: + name: test_integer_no_schema + description: Test function without proper parameter schema + language: python + source: + file: ../python/test_endpoints.py + # Note: No parameters section - this might bypass validation + return: + type: object diff --git a/tests/server/fixtures/integration/tools/test_integer_parameter.yml b/tests/server/fixtures/integration/tools/test_integer_parameter.yml new file mode 100644 index 00000000..4a54b234 --- /dev/null +++ b/tests/server/fixtures/integration/tools/test_integer_parameter.yml @@ -0,0 +1,26 @@ +mxcp: 1 +tool: + name: test_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_integer_conversion_bug.py b/tests/server/test_integer_conversion_bug.py new file mode 100644 index 00000000..d2a2f581 --- /dev/null +++ b/tests/server/test_integer_conversion_bug.py @@ -0,0 +1,124 @@ +"""Test for integer parameter conversion bug. + +This test specifically reproduces the bug described in the bug report where +JSON float values like 0.0 are not converted to integers before being passed +to Python functions. +""" + +import pytest +from mxcp.sdk.executor import ExecutionEngine, ExecutionContext +from mxcp.sdk.executor.plugins.python import PythonExecutor + + +class TestIntegerConversionBug: + """Test cases for integer parameter conversion bug.""" + + @pytest.mark.asyncio + async def test_direct_sdk_executor_integer_conversion(self): + """Test integer conversion directly through SDK executor.""" + + # Create a Python executor + python_executor = PythonExecutor() + + # Create execution engine + engine = ExecutionEngine() + engine.register_executor(python_executor) + + # Python function that expects integer + source_code = ''' +def test_function(top_n: int) -> dict: + """Test function that expects an integer.""" + if not isinstance(top_n, int): + return { + "error": f"Expected int, got {type(top_n)}: {top_n}", + "type_received": str(type(top_n)), + "test_passed": False + } + return { + "top_n": top_n, + "type_received": str(type(top_n)), + "test_passed": True + } + +return test_function(top_n) +''' + + # Input schema that specifies integer type + input_schema = [ + { + "name": "top_n", + "type": "integer", + "description": "Number of items", + "minimum": 0, + "default": 0 + } + ] + + # Test with float value 0.0 - this should be converted to int + context = ExecutionContext() + params = {"top_n": 0.0} # JSON would send this as float + + try: + result = await engine.execute( + language="python", + source_code=source_code, + params=params, + context=context, + input_schema=input_schema + ) + + # Check if conversion worked + assert result["test_passed"] is True, f"Integer conversion failed: {result.get('error', 'Unknown error')}" + assert result["type_received"] == "" + assert result["top_n"] == 0 + + finally: + engine.shutdown() + + @pytest.mark.asyncio + async def test_sdk_executor_without_schema(self): + """Test what happens when no input schema is provided.""" + + # Create a Python executor + python_executor = PythonExecutor() + + # Create execution engine + engine = ExecutionEngine() + engine.register_executor(python_executor) + + # Python function that expects integer + source_code = ''' +def test_function(top_n: int) -> dict: + """Test function that expects an integer.""" + return { + "top_n": top_n, + "type_received": str(type(top_n)), + "test_passed": isinstance(top_n, int) + } + +return test_function(top_n) +''' + + # Test with float value 0.0 - without schema, this should remain float + context = ExecutionContext() + params = {"top_n": 0.0} # JSON would send this as float + + try: + result = await engine.execute( + language="python", + source_code=source_code, + params=params, + context=context, + input_schema=None # No schema - no conversion + ) + + # Without schema, the float should remain as float + # This would demonstrate the bug if it exists + print(f"Result without schema: {result}") + + # This should fail if no conversion happens + if not result["test_passed"]: + print(f"Bug reproduced: {result['type_received']}") + + finally: + engine.shutdown() diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index 8e590fe9..f5c21aaa 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -415,6 +415,36 @@ 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("test_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("test_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).""" From 0cecaffc62c4d8a0855f77ca33a6da6d25540bdb Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Tue, 26 Aug 2025 17:49:05 +0200 Subject: [PATCH 2/7] Fix integer parameter conversion in MCP server --- src/mxcp/server/interfaces/server/mcp.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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, From 4198494829f763a814a9fcabd40eef2d15f818d6 Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Tue, 26 Aug 2025 17:58:08 +0200 Subject: [PATCH 3/7] Remove unnecessary test file - integration test covers the scenario --- tests/server/test_integer_conversion_bug.py | 124 -------------------- 1 file changed, 124 deletions(-) delete mode 100644 tests/server/test_integer_conversion_bug.py diff --git a/tests/server/test_integer_conversion_bug.py b/tests/server/test_integer_conversion_bug.py deleted file mode 100644 index d2a2f581..00000000 --- a/tests/server/test_integer_conversion_bug.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Test for integer parameter conversion bug. - -This test specifically reproduces the bug described in the bug report where -JSON float values like 0.0 are not converted to integers before being passed -to Python functions. -""" - -import pytest -from mxcp.sdk.executor import ExecutionEngine, ExecutionContext -from mxcp.sdk.executor.plugins.python import PythonExecutor - - -class TestIntegerConversionBug: - """Test cases for integer parameter conversion bug.""" - - @pytest.mark.asyncio - async def test_direct_sdk_executor_integer_conversion(self): - """Test integer conversion directly through SDK executor.""" - - # Create a Python executor - python_executor = PythonExecutor() - - # Create execution engine - engine = ExecutionEngine() - engine.register_executor(python_executor) - - # Python function that expects integer - source_code = ''' -def test_function(top_n: int) -> dict: - """Test function that expects an integer.""" - if not isinstance(top_n, int): - return { - "error": f"Expected int, got {type(top_n)}: {top_n}", - "type_received": str(type(top_n)), - "test_passed": False - } - return { - "top_n": top_n, - "type_received": str(type(top_n)), - "test_passed": True - } - -return test_function(top_n) -''' - - # Input schema that specifies integer type - input_schema = [ - { - "name": "top_n", - "type": "integer", - "description": "Number of items", - "minimum": 0, - "default": 0 - } - ] - - # Test with float value 0.0 - this should be converted to int - context = ExecutionContext() - params = {"top_n": 0.0} # JSON would send this as float - - try: - result = await engine.execute( - language="python", - source_code=source_code, - params=params, - context=context, - input_schema=input_schema - ) - - # Check if conversion worked - assert result["test_passed"] is True, f"Integer conversion failed: {result.get('error', 'Unknown error')}" - assert result["type_received"] == "" - assert result["top_n"] == 0 - - finally: - engine.shutdown() - - @pytest.mark.asyncio - async def test_sdk_executor_without_schema(self): - """Test what happens when no input schema is provided.""" - - # Create a Python executor - python_executor = PythonExecutor() - - # Create execution engine - engine = ExecutionEngine() - engine.register_executor(python_executor) - - # Python function that expects integer - source_code = ''' -def test_function(top_n: int) -> dict: - """Test function that expects an integer.""" - return { - "top_n": top_n, - "type_received": str(type(top_n)), - "test_passed": isinstance(top_n, int) - } - -return test_function(top_n) -''' - - # Test with float value 0.0 - without schema, this should remain float - context = ExecutionContext() - params = {"top_n": 0.0} # JSON would send this as float - - try: - result = await engine.execute( - language="python", - source_code=source_code, - params=params, - context=context, - input_schema=None # No schema - no conversion - ) - - # Without schema, the float should remain as float - # This would demonstrate the bug if it exists - print(f"Result without schema: {result}") - - # This should fail if no conversion happens - if not result["test_passed"]: - print(f"Bug reproduced: {result['type_received']}") - - finally: - engine.shutdown() From f8c1022d1e69213a56227aece993665fba825dce Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Tue, 26 Aug 2025 17:59:28 +0200 Subject: [PATCH 4/7] Remove unused test JSON file --- tests/server/fixtures/integration/test_params.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/server/fixtures/integration/test_params.json diff --git a/tests/server/fixtures/integration/test_params.json b/tests/server/fixtures/integration/test_params.json deleted file mode 100644 index ba66466c..00000000 --- a/tests/server/fixtures/integration/test_params.json +++ /dev/null @@ -1 +0,0 @@ -0.0 From 63c4c1c03579882831861be493ac651a53f99904 Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Tue, 26 Aug 2025 17:59:44 +0200 Subject: [PATCH 5/7] Remove unused incomplete tool definition --- .../integration/tools/test_integer_no_schema.yml | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 tests/server/fixtures/integration/tools/test_integer_no_schema.yml diff --git a/tests/server/fixtures/integration/tools/test_integer_no_schema.yml b/tests/server/fixtures/integration/tools/test_integer_no_schema.yml deleted file mode 100644 index a2d29b56..00000000 --- a/tests/server/fixtures/integration/tools/test_integer_no_schema.yml +++ /dev/null @@ -1,10 +0,0 @@ -mxcp: 1 -tool: - name: test_integer_no_schema - description: Test function without proper parameter schema - language: python - source: - file: ../python/test_endpoints.py - # Note: No parameters section - this might bypass validation - return: - type: object From 8d667f80e13df249d7a5cb15a998b0cfd9d7f0ad Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Tue, 26 Aug 2025 18:14:36 +0200 Subject: [PATCH 6/7] Rename test function to avoid pytest naming conflict --- tests/server/fixtures/integration/python/test_endpoints.py | 2 +- ...test_integer_parameter.yml => check_integer_parameter.yml} | 2 +- tests/server/test_integration.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename tests/server/fixtures/integration/tools/{test_integer_parameter.yml => check_integer_parameter.yml} (94%) diff --git a/tests/server/fixtures/integration/python/test_endpoints.py b/tests/server/fixtures/integration/python/test_endpoints.py index a390047d..86a23ca1 100644 --- a/tests/server/fixtures/integration/python/test_endpoints.py +++ b/tests/server/fixtures/integration/python/test_endpoints.py @@ -123,7 +123,7 @@ def process_user_data(user_data: Dict[str, Any]) -> Dict[str, Any]: return {"original_data": user_data, "analysis": analysis, "processing_status": "success"} -def test_integer_parameter(top_n: int) -> Dict[str, Any]: +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 diff --git a/tests/server/fixtures/integration/tools/test_integer_parameter.yml b/tests/server/fixtures/integration/tools/check_integer_parameter.yml similarity index 94% rename from tests/server/fixtures/integration/tools/test_integer_parameter.yml rename to tests/server/fixtures/integration/tools/check_integer_parameter.yml index 4a54b234..5a3841d5 100644 --- a/tests/server/fixtures/integration/tools/test_integer_parameter.yml +++ b/tests/server/fixtures/integration/tools/check_integer_parameter.yml @@ -1,6 +1,6 @@ mxcp: 1 tool: - name: test_integer_parameter + name: check_integer_parameter description: Test function that expects an integer parameter language: python source: diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index f5c21aaa..3bd6f14e 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -423,7 +423,7 @@ async def test_integer_parameter_conversion(self, integration_fixture_dir): 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("test_integer_parameter", {"top_n": 0.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"]: @@ -435,7 +435,7 @@ async def test_integer_parameter_conversion(self, integration_fixture_dir): assert result["test_passed"] is True # Test with float value 2.0 - this should be converted to int(2) - result = await client.call_tool("test_integer_parameter", {"top_n": 2.0}) + 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')}") From 63c1a49f72512f80caa50411484372829b4dd5dd Mon Sep 17 00:00:00 2001 From: Benjamin Gaidioz Date: Tue, 26 Aug 2025 18:17:26 +0200 Subject: [PATCH 7/7] fixup! Rename test function to avoid pytest naming conflict wip --- .../integration/python/test_endpoints.py | 14 +++++++------- tests/server/test_integration.py | 18 +++++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/server/fixtures/integration/python/test_endpoints.py b/tests/server/fixtures/integration/python/test_endpoints.py index 86a23ca1..cc7ddfca 100644 --- a/tests/server/fixtures/integration/python/test_endpoints.py +++ b/tests/server/fixtures/integration/python/test_endpoints.py @@ -125,13 +125,13 @@ def process_user_data(user_data: Dict[str, Any]) -> Dict[str, Any]: 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): @@ -140,21 +140,21 @@ def check_integer_parameter(top_n: int) -> Dict[str, Any]: "type_received": str(actual_type), "selected_items": [], "test_passed": False, - "error": f"Expected int, got {actual_type}: {top_n}" + "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 + "test_passed": True, } diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index 3bd6f14e..99d17946 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -424,22 +424,26 @@ async def test_integer_parameter_conversion(self, integration_fixture_dir): 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')}") - + 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')}") - + 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"]