Fix: Correct MCP catalog JSON parsing to handle special characters

The catalog_search() and catalog_list() methods in MySQL_Catalog.cpp
were manually building JSON strings by concatenating raw TEXT from
SQLite without proper escaping. This caused parse errors when stored
JSON contained quotes, backslashes, or newlines.

Changes:
- MySQL_Catalog.cpp: Use nlohmann::json to build proper nested JSON
  in search() and list() methods instead of manual concatenation
- MySQL_Tool_Handler.cpp: Add try-catch for JSON parsing in catalog_get()
- test_catalog.sh: Fix MCP URL path, add jq extraction for MCP protocol
  responses, add 3 special character tests (CAT013-CAT015)

Test Results: All 15 catalog tests pass, including new tests that
verify special characters (quotes, backslashes) are preserved.
pull/5310/head
Rene Cannao 3 months ago
parent 1d046148d4
commit f852900365

@ -3,6 +3,7 @@
#include "proxysql.h"
#include <sstream>
#include <algorithm>
#include "../deps/json/json.hpp"
MySQL_Catalog::MySQL_Catalog(const std::string& path)
: db(NULL), db_path(path)
@ -220,31 +221,40 @@ std::string MySQL_Catalog::search(
return "[]";
}
// Build JSON result
std::ostringstream json;
json << "[";
bool first = true;
// Build JSON result using nlohmann::json
nlohmann::json results = nlohmann::json::array();
if (resultset) {
for (std::vector<SQLite3_row*>::iterator it = resultset->rows.begin();
it != resultset->rows.end(); ++it) {
SQLite3_row* row = *it;
if (!first) json << ",";
first = false;
json << "{"
<< "\"kind\":\"" << (row->fields[0] ? row->fields[0] : "") << "\","
<< "\"key\":\"" << (row->fields[1] ? row->fields[1] : "") << "\","
<< "\"document\":" << (row->fields[2] ? row->fields[2] : "null") << ","
<< "\"tags\":\"" << (row->fields[3] ? row->fields[3] : "") << "\","
<< "\"links\":\"" << (row->fields[4] ? row->fields[4] : "") << "\""
<< "}";
nlohmann::json entry;
entry["kind"] = std::string(row->fields[0] ? row->fields[0] : "");
entry["key"] = std::string(row->fields[1] ? row->fields[1] : "");
// Parse the stored JSON document - nlohmann::json handles escaping
const char* doc_str = row->fields[2];
if (doc_str) {
try {
entry["document"] = nlohmann::json::parse(doc_str);
} catch (const nlohmann::json::parse_error& e) {
// If document is not valid JSON, store as string
entry["document"] = std::string(doc_str);
}
} else {
entry["document"] = nullptr;
}
entry["tags"] = std::string(row->fields[3] ? row->fields[3] : "");
entry["links"] = std::string(row->fields[4] ? row->fields[4] : "");
results.push_back(entry);
}
delete resultset;
}
json << "]";
return json.str();
return results.dump();
}
std::string MySQL_Catalog::list(
@ -282,31 +292,42 @@ std::string MySQL_Catalog::list(
resultset = NULL;
db->execute_statement(sql.str().c_str(), &error, &cols, &affected, &resultset);
// Build JSON result with total count
std::ostringstream json;
json << "{\"total\":" << total << ",\"results\":[";
// Build JSON result using nlohmann::json
nlohmann::json result;
result["total"] = total;
nlohmann::json results = nlohmann::json::array();
bool first = true;
if (resultset) {
for (std::vector<SQLite3_row*>::iterator it = resultset->rows.begin();
it != resultset->rows.end(); ++it) {
SQLite3_row* row = *it;
if (!first) json << ",";
first = false;
json << "{"
<< "\"kind\":\"" << (row->fields[0] ? row->fields[0] : "") << "\","
<< "\"key\":\"" << (row->fields[1] ? row->fields[1] : "") << "\","
<< "\"document\":" << (row->fields[2] ? row->fields[2] : "null") << ","
<< "\"tags\":\"" << (row->fields[3] ? row->fields[3] : "") << "\","
<< "\"links\":\"" << (row->fields[4] ? row->fields[4] : "") << "\""
<< "}";
nlohmann::json entry;
entry["kind"] = std::string(row->fields[0] ? row->fields[0] : "");
entry["key"] = std::string(row->fields[1] ? row->fields[1] : "");
// Parse the stored JSON document
const char* doc_str = row->fields[2];
if (doc_str) {
try {
entry["document"] = nlohmann::json::parse(doc_str);
} catch (const nlohmann::json::parse_error& e) {
entry["document"] = std::string(doc_str);
}
} else {
entry["document"] = nullptr;
}
entry["tags"] = std::string(row->fields[3] ? row->fields[3] : "");
entry["links"] = std::string(row->fields[4] ? row->fields[4] : "");
results.push_back(entry);
}
delete resultset;
}
json << "]}";
return json.str();
result["results"] = results;
return result.dump();
}
int MySQL_Catalog::merge(

@ -910,7 +910,13 @@ std::string MySQL_Tool_Handler::catalog_get(const std::string& kind, const std::
if (rc == 0) {
result["kind"] = kind;
result["key"] = key;
result["document"] = json::parse(document);
// Parse as raw JSON value to preserve nested structure
try {
result["document"] = json::parse(document);
} catch (const json::parse_error& e) {
// If not valid JSON, store as string
result["document"] = document;
}
} else {
result["error"] = "Entry not found";
}

@ -15,7 +15,7 @@ set -e
# Configuration
MCP_HOST="${MCP_HOST:-127.0.0.1}"
MCP_PORT="${MCP_PORT:-6071}"
MCP_URL="https://${MCP_HOST}:${MCP_PORT}/query"
MCP_URL="https://${MCP_HOST}:${MCP_PORT}/mcp/query"
# Test options
VERBOSE=false
@ -39,7 +39,7 @@ log_test() {
echo -e "${BLUE}[TEST]${NC} $1"
}
# Execute MCP request
# Execute MCP request and unwrap response
mcp_request() {
local payload="$1"
@ -48,7 +48,16 @@ mcp_request() {
-H "Content-Type: application/json" \
-d "${payload}" 2>/dev/null)
echo "${response}"
# Extract content from MCP protocol wrapper if present
# MCP format: {"result":{"content":[{"text":"..."}]}}
local extracted
extracted=$(echo "${response}" | jq -r 'if .result.content[0].text then .result.content[0].text else . end' 2>/dev/null)
if [ -n "${extracted}" ] && [ "${extracted}" != "null" ]; then
echo "${extracted}"
else
echo "${response}"
fi
}
# Test catalog operations
@ -290,6 +299,72 @@ run_catalog_tests() {
failed=$((failed + 1))
fi
# Test 13: Special characters in document (JSON parsing bug test)
local payload13
payload13='{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "catalog_upsert",
"arguments": {
"kind": "test",
"key": "special_chars",
"document": "{\"description\": \"Test with \\\"quotes\\\" and \\\\backslashes\\\\\"}",
"tags": "test,special",
"links": ""
}
},
"id": 13
}'
if test_catalog "CAT013" "Upsert special characters" "${payload13}" '"success"[[:space:]]*:[[:space:]]*true'; then
passed=$((passed + 1))
else
failed=$((failed + 1))
fi
# Test 14: Verify special characters can be read back
local payload14
payload14='{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "catalog_get",
"arguments": {
"kind": "test",
"key": "special_chars"
}
},
"id": 14
}'
if test_catalog "CAT014" "Get special chars entry" "${payload14}" 'quotes'; then
passed=$((passed + 1))
else
failed=$((failed + 1))
fi
# Test 15: Cleanup special chars entry
local payload15
payload15='{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "catalog_delete",
"arguments": {
"kind": "test",
"key": "special_chars"
}
},
"id": 15
}'
if test_catalog "CAT015" "Cleanup special chars" "${payload15}" '"success"[[:space:]]*:[[:space:]]*true'; then
passed=$((passed + 1))
else
failed=$((failed + 1))
fi
# Test 10: Delete entry
local payload10
payload10='{

Loading…
Cancel
Save