From 52a1eca4d99f9eae8110cd4742be766f29db0c76 Mon Sep 17 00:00:00 2001 From: Redamancy <848238014@qq.com> Date: Wed, 9 Sep 2026 02:03:37 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20fix(mcp):=20=E7=BB=9F=E4=B8=80=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=93=8D=E5=BA=94=20JSON=20=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dbjavagenix/database/mcp_tools.py | 23 ++-- tests/unit/test_mcp_error_json.py | 149 ++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 10 deletions(-) diff --git a/src/dbjavagenix/database/mcp_tools.py b/src/dbjavagenix/database/mcp_tools.py index fdf35a3..554a26d 100644 --- a/src/dbjavagenix/database/mcp_tools.py +++ b/src/dbjavagenix/database/mcp_tools.py @@ -75,6 +75,11 @@ def _query_result_json_default(value: object) -> object: raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable") +def _json_dumps(value: object) -> str: + """Serialize MCP payloads with the same driver-value rules as query results.""" + return json.dumps(value, ensure_ascii=False, default=_query_result_json_default) + + def _resolve_codegen_output_path(base_dir: Path, relative_path: object) -> Path: """Resolve a generated filename while keeping it inside its output directory.""" if not isinstance(relative_path, str) or not relative_path.strip(): @@ -1047,9 +1052,7 @@ async def handle_db_query_execute(arguments: Dict[str, Any]) -> List[TextContent else: result_text = "Query executed successfully. No rows returned." - result_text += "\n\nRaw Response: " + json.dumps( - response, ensure_ascii=False, default=_query_result_json_default - ) + result_text += "\n\nRaw Response: " + _json_dumps(response) return [TextContent( type="text", @@ -1238,7 +1241,7 @@ async def handle_db_table_describe(arguments: Dict[str, Any]) -> List[TextConten for imp in sorted(java_imports): result_text += f"import {imp};\n" - result_text += f"\nRaw Response: {json.dumps(response, ensure_ascii=False)}" + result_text += f"\nRaw Response: {_json_dumps(response)}" return [TextContent( type="text", @@ -1374,7 +1377,7 @@ async def handle_db_table_columns(arguments: Dict[str, Any]) -> List[TextContent result_text += f" Comment: {row['COLUMN_COMMENT']}\n" result_text += "\n" - result_text += f"Raw Response: {json.dumps(response, ensure_ascii=False)}" + result_text += f"Raw Response: {_json_dumps(response)}" return [TextContent( type="text", @@ -1476,7 +1479,7 @@ async def handle_db_table_primary_keys(arguments: Dict[str, Any]) -> List[TextCo else: result_text = f"No primary keys found for table {database}.{table}\n" - result_text += f"\nRaw Response: {json.dumps(response, ensure_ascii=False)}" + result_text += f"\nRaw Response: {_json_dumps(response)}" return [TextContent( type="text", @@ -1618,7 +1621,7 @@ async def handle_db_table_foreign_keys(arguments: Dict[str, Any]) -> List[TextCo else: result_text = f"No foreign keys found for table {database}.{table}\n" - result_text += f"Raw Response: {json.dumps(response, ensure_ascii=False)}" + result_text += f"Raw Response: {_json_dumps(response)}" return [TextContent( type="text", @@ -1774,7 +1777,7 @@ async def handle_db_table_indexes(arguments: Dict[str, Any]) -> List[TextContent else: result_text = f"No indexes found for table {database}.{table}\n" - result_text += f"Raw Response: {json.dumps(response, ensure_ascii=False)}" + result_text += f"Raw Response: {_json_dumps(response)}" return [TextContent( type="text", @@ -2040,8 +2043,8 @@ async def handle_db_codegen_analyze(arguments: Dict[str, Any]) -> List[TextConte result_text += f" Has Primary Key: {'Yes' if context.get('primaryKey') else 'No'}\n" # Keep a structured payload for non-MCP callers such as the CLI. - result_text += "\n\nRaw Response: " + json.dumps( - {"success": True, **analysis_result}, ensure_ascii=False + result_text += "\n\nRaw Response: " + _json_dumps( + {"success": True, **analysis_result} ) return [TextContent( diff --git a/tests/unit/test_mcp_error_json.py b/tests/unit/test_mcp_error_json.py index b550ea4..a64375a 100644 --- a/tests/unit/test_mcp_error_json.py +++ b/tests/unit/test_mcp_error_json.py @@ -140,6 +140,155 @@ async def test_query_execute_serializes_real_driver_value_types(monkeypatch): } +@pytest.mark.asyncio +async def test_metadata_handlers_serialize_driver_value_types(monkeypatch): + config = SimpleNamespace(type=DatabaseType.POSTGRESQL, database="app") + + class Introspector: + def __init__(self, _manager): + pass + + def get_config(self, _connection_id): + return config + + def describe_table(self, _connection_id, _table, _schema): + return { + "name": "events", + "schema": "public", + "comment": datetime(2026, 9, 8, 10, 0, tzinfo=timezone.utc), + "columns": [ + { + "name": "amount", + "type": "numeric", + "column_type": "numeric(10,2)", + "nullable": False, + "default_value": Decimal("12.30"), + "comment": UUID("12345678-1234-5678-1234-567812345678"), + "primary_key": False, + "precision": 10, + "scale": 2, + "max_length": None, + } + ], + } + + def get_columns(self, _connection_id, _table, _schema): + return [ + { + "name": "amount", + "type": "numeric", + "column_type": "numeric(10,2)", + "nullable": False, + "default_value": memoryview(b"view"), + "comment": "created", + "precision": 10, + "scale": 2, + "max_length": None, + } + ] + + def get_primary_keys(self, _connection_id, _table, _schema): + return ["id"] + + def get_foreign_keys(self, _connection_id, _table, _schema): + return [ + { + "column_name": "owner_id", + "referenced_table": "users", + "referenced_column": "id", + "constraint_name": "events_owner_fk", + } + ] + + def get_indexes(self, _connection_id, _table, _schema): + return [ + { + "key_name": "events_amount_idx", + "column_name": "amount", + "seq_in_index": 1, + "unique": False, + "index_type": "btree", + } + ] + + monkeypatch.setattr(mcp_tools, "DatabaseIntrospector", Introspector) + monkeypatch.setattr( + mcp_tools.connection_manager, + "get_connection_info", + lambda _id: config, + ) + common = { + "connection_id": "pg-1", + "database": "app", + "table": "events", + "schema": "public", + } + + describe = await mcp_tools.handle_db_table_describe(common) + columns = await mcp_tools.handle_db_table_columns(common) + primary_keys = await mcp_tools.handle_db_table_primary_keys(common) + foreign_keys = await mcp_tools.handle_db_table_foreign_keys(common) + indexes = await mcp_tools.handle_db_table_indexes(common) + + assert _raw_payload(describe)["columns"][0]["default_value"] == "12.30" + assert _raw_payload(describe)["comment"] == "2026-09-08T10:00:00+00:00" + assert _raw_payload(columns)["columns"][0]["COLUMN_DEFAULT"] == { + "encoding": "base64", + "data": "dmlldw==", + } + assert _raw_payload(columns)["columns"][0]["COLUMN_COMMENT"] == "created" + assert _raw_payload(primary_keys)["primary_keys"] == ["id"] + assert _raw_payload(foreign_keys)["foreign_keys"][0]["references_table"] == "users" + assert _raw_payload(indexes)["indexes"][0]["name"] == "events_amount_idx" + + +@pytest.mark.asyncio +async def test_codegen_analysis_raw_response_serializes_driver_values(monkeypatch): + class Analyzer: + def __init__(self, _manager): + pass + + async def analyze_table_for_codegen(self, *_args, **_kwargs): + return { + "table_name": "events", + "table_info": { + "name": "events", + "comment": "event table", + "columns": [ + { + "name": "amount", + "type": "numeric", + "default_value": Decimal("12.30"), + } + ], + }, + "template_context": { + "columns": [{"javaType": "BigDecimal"}], + "className": "Event", + "lowerCaseName": "event", + "hasDateField": False, + "hasBigDecimalField": True, + "primaryKey": "id", + }, + "java_types": ["BigDecimal"], + "imports_needed": [], + "relationships": {"primary_keys": ["id"], "foreign_keys": [], "indexes": []}, + } + + monkeypatch.setattr("dbjavagenix.database.codegen_tools.CodegenAnalyzer", Analyzer) + monkeypatch.setattr( + mcp_tools.connection_manager, + "get_connection_info", + lambda _id: SimpleNamespace(type=DatabaseType.POSTGRESQL, database="app"), + ) + + response = await mcp_tools.handle_db_codegen_analyze( + {"connection_id": "pg-1", "table_name": "events"} + ) + + assert _raw_payload(response)["table_info"]["columns"][0]["default_value"] == "12.30" + + def test_query_result_json_default_rejects_unknown_object(): class UnknownDriverValue: pass