From 01c182ccac1ef03e759fd6574e4d31147184a1e0 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 11:05:54 +0000 Subject: [PATCH 01/17] Add stdio MCP bridge for Claude Code integration Add a Python stdio-based MCP server that bridges to ProxySQL's HTTPS MCP endpoint, enabling Claude Code to use ProxySQL MCP tools directly. The bridge: - Implements stdio MCP server protocol (for Claude Code) - Acts as MCP client to ProxySQL's HTTPS endpoint - Supports initialize, tools/list, tools/call methods - Handles authentication via Bearer tokens - Configurable via environment variables Usage: - Configure in Claude Code MCP settings - Set PROXYSQL_MCP_ENDPOINT environment variable - Optional: PROXYSQL_MCP_TOKEN for auth --- scripts/mcp/STDIO_BRIDGE_README.md | 134 +++++++++ scripts/mcp/proxysql_mcp_stdio_bridge.py | 330 +++++++++++++++++++++++ 2 files changed, 464 insertions(+) create mode 100644 scripts/mcp/STDIO_BRIDGE_README.md create mode 100755 scripts/mcp/proxysql_mcp_stdio_bridge.py diff --git a/scripts/mcp/STDIO_BRIDGE_README.md b/scripts/mcp/STDIO_BRIDGE_README.md new file mode 100644 index 0000000000..f6aff7ee88 --- /dev/null +++ b/scripts/mcp/STDIO_BRIDGE_README.md @@ -0,0 +1,134 @@ +# ProxySQL MCP stdio Bridge + +A bridge that converts between **stdio-based MCP** (for Claude Code) and **ProxySQL's HTTPS MCP endpoint**. + +## What It Does + +``` +┌─────────────┐ stdio ┌──────────────────┐ HTTPS ┌──────────┐ +│ Claude Code│ ──────────> │ stdio Bridge │ ──────────> │ ProxySQL │ +│ (MCP Client)│ │ (this script) │ │ MCP │ +└─────────────┘ └──────────────────┘ └──────────┘ +``` + +- **To Claude Code**: Acts as an MCP Server (stdio transport) +- **To ProxySQL**: Acts as an MCP Client (HTTPS transport) + +## Installation + +1. Install dependencies: +```bash +pip install httpx +``` + +2. Make the script executable: +```bash +chmod +x proxysql_mcp_stdio_bridge.py +``` + +## Configuration + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `PROXYSQL_MCP_ENDPOINT` | Yes | - | ProxySQL MCP endpoint URL (e.g., `https://127.0.0.1:6071/mcp/query`) | +| `PROXYSQL_MCP_TOKEN` | No | - | Bearer token for authentication (if configured) | +| `PROXYSQL_MCP_INSECURE_SSL` | No | 0 | Set to 1 to disable SSL verification (for self-signed certs) | + +### Configure in Claude Code + +Add to your Claude Code MCP settings (usually `~/.config/claude-code/mcp_config.json` or similar): + +```json +{ + "mcpServers": { + "proxysql": { + "command": "python3", + "args": ["/home/rene/proxysql-vec/scripts/mcp/proxysql_mcp_stdio_bridge.py"], + "env": { + "PROXYSQL_MCP_ENDPOINT": "https://127.0.0.1:6071/mcp/query", + "PROXYSQL_MCP_TOKEN": "your_token_here", + "PROXYSQL_MCP_INSECURE_SSL": "1" + } + } + } +} +``` + +### Quick Test from Terminal + +```bash +export PROXYSQL_MCP_ENDPOINT="https://127.0.0.1:6071/mcp/query" +export PROXYSQL_MCP_TOKEN="your_token" # optional +export PROXYSQL_MCP_INSECURE_SSL="1" # for self-signed certs + +python3 proxysql_mcp_stdio_bridge.py +``` + +Then send a JSON-RPC request via stdin: +```json +{"jsonrpc": "2.0", "id": 1, "method": "tools/list"} +``` + +## Supported MCP Methods + +| Method | Description | +|--------|-------------| +| `initialize` | Handshake protocol | +| `tools/list` | List available ProxySQL MCP tools | +| `tools/call` | Call a ProxySQL MCP tool | +| `ping` | Health check | + +## Available Tools (from ProxySQL) + +Once connected, the following tools will be available in Claude Code: + +- `list_schemas` - List databases +- `list_tables` - List tables in a schema +- `describe_table` - Get table structure +- `get_constraints` - Get foreign keys and constraints +- `sample_rows` - Sample data from a table +- `run_sql_readonly` - Execute read-only SQL queries +- `explain_sql` - Get query execution plan +- `table_profile` - Get table statistics +- `column_profile` - Get column statistics +- `catalog_upsert` - Store data in the catalog +- `catalog_get` - Retrieve from the catalog +- `catalog_search` - Search the catalog +- And more... + +## Example Usage in Claude Code + +Once configured, you can ask Claude: + +> "List all tables in the testdb schema" +> "Describe the customers table" +> "Show me 5 rows from the orders table" +> "Run SELECT COUNT(*) FROM customers" + +## Troubleshooting + +### Connection Refused +Make sure ProxySQL MCP server is running: +```bash +curl -k https://127.0.0.1:6071/mcp/query \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "method": "ping", "id": 1}' +``` + +### SSL Certificate Errors +Set `PROXYSQL_MCP_INSECURE_SSL=1` to bypass certificate verification. + +### Authentication Errors +Check that `PROXYSQL_MCP_TOKEN` matches the token configured in ProxySQL: +```sql +SHOW VARIABLES LIKE 'mcp-query_endpoint_auth'; +``` + +## Requirements + +- Python 3.7+ +- httpx (`pip install httpx`) +- ProxySQL with MCP enabled diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py new file mode 100755 index 0000000000..24d9015544 --- /dev/null +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +""" +ProxySQL MCP stdio Bridge + +Translates between stdio-based MCP (for Claude Code) and ProxySQL's HTTPS MCP endpoint. + +Usage: + export PROXYSQL_MCP_ENDPOINT="https://127.0.0.1:6071/mcp/query" + export PROXYSQL_MCP_TOKEN="your_token" # optional + python proxysql_mcp_stdio_bridge.py + +Or configure in Claude Code's MCP settings: + { + "mcpServers": { + "proxysql": { + "command": "python3", + "args": ["/path/to/proxysql_mcp_stdio_bridge.py"], + "env": { + "PROXYSQL_MCP_ENDPOINT": "https://127.0.0.1:6071/mcp/query", + "PROXYSQL_MCP_TOKEN": "your_token" + } + } + } + } +""" + +import asyncio +import json +import os +import sys +from typing import Any, Dict, Optional + +import httpx + + +class ProxySQLMCPEndpoint: + """Client for ProxySQL's HTTPS MCP endpoint.""" + + def __init__(self, endpoint: str, auth_token: Optional[str] = None, verify_ssl: bool = True): + self.endpoint = endpoint + self.auth_token = auth_token + self.verify_ssl = verify_ssl + self._client: Optional[httpx.AsyncClient] = None + self._initialized = False + + async def __aenter__(self): + self._client = httpx.AsyncClient( + timeout=120.0, + verify=self.verify_ssl, + ) + # Initialize connection + await self._initialize() + return self + + async def __aexit__(self, *args): + if self._client: + await self._client.aclose() + + async def _initialize(self): + """Initialize the MCP connection.""" + request = { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "proxysql-mcp-stdio-bridge", + "version": "1.0.0" + } + } + } + response = await self._call(request) + self._initialized = True + return response + + async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: + """Make a JSON-RPC call to ProxySQL MCP endpoint.""" + if not self._client: + raise RuntimeError("Client not initialized") + + headers = {"Content-Type": "application/json"} + if self.auth_token: + headers["Authorization"] = f"Bearer {self.auth_token}" + + try: + r = await self._client.post(self.endpoint, json=request, headers=headers) + r.raise_for_status() + return r.json() + except httpx.HTTPStatusError as e: + return { + "jsonrpc": "2.0", + "error": { + "code": -32000, + "message": f"HTTP error: {e.response.status_code}", + "data": str(e) + }, + "id": request.get("id", "") + } + except Exception as e: + return { + "jsonrpc": "2.0", + "error": { + "code": -32603, + "message": f"Internal error: {str(e)}" + }, + "id": request.get("id", "") + } + + async def tools_list(self) -> Dict[str, Any]: + """List available tools.""" + request = { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} + } + return await self._call(request) + + async def tools_call(self, name: str, arguments: Dict[str, Any], req_id: str) -> Dict[str, Any]: + """Call a tool.""" + request = { + "jsonrpc": "2.0", + "id": req_id, + "method": "tools/call", + "params": { + "name": name, + "arguments": arguments + } + } + return await self._call(request) + + +class StdioMCPServer: + """stdio-based MCP server that bridges to ProxySQL's HTTPS MCP.""" + + def __init__(self, proxysql_endpoint: str, auth_token: Optional[str] = None, verify_ssl: bool = True): + self.proxysql_endpoint = proxysql_endpoint + self.auth_token = auth_token + self.verify_ssl = verify_ssl + self._proxysql: Optional[ProxySQLMCPEndpoint] = None + self._request_id = 1 + + async def run(self): + """Main server loop.""" + async with ProxySQLMCPEndpoint(self.proxysql_endpoint, self.auth_token, self.verify_ssl) as client: + self._proxysql = client + + # Send initialized notification + await self._write_notification("notifications/initialized") + + # Main message loop + while True: + try: + line = await self._readline() + if not line: + break + + message = json.loads(line) + response = await self._handle_message(message) + + if response: + await self._writeline(response) + + except json.JSONDecodeError as e: + await self._write_error(-32700, f"Parse error: {e}", "") + except Exception as e: + await self._write_error(-32603, f"Internal error: {e}", "") + + async def _readline(self) -> Optional[str]: + """Read a line from stdin.""" + loop = asyncio.get_event_loop() + line = await loop.run_in_executor(None, sys.stdin.readline) + if not line: + return None + return line.strip() + + async def _writeline(self, data: Any): + """Write JSON data to stdout.""" + loop = asyncio.get_event_loop() + output = json.dumps(data, ensure_ascii=False) + "\n" + await loop.run_in_executor(None, sys.stdout.write, output) + await loop.run_in_executor(None, sys.stdout.flush) + + async def _write_notification(self, method: str, params: Optional[Dict[str, Any]] = None): + """Write a notification (no id).""" + notification = { + "jsonrpc": "2.0", + "method": method + } + if params: + notification["params"] = params + await self._writeline(notification) + + async def _write_response(self, result: Any, req_id: str): + """Write a response.""" + response = { + "jsonrpc": "2.0", + "result": result, + "id": req_id + } + await self._writeline(response) + + async def _write_error(self, code: int, message: str, req_id: str): + """Write an error response.""" + response = { + "jsonrpc": "2.0", + "error": { + "code": code, + "message": message + }, + "id": req_id + } + await self._writeline(response) + + async def _handle_message(self, message: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Handle an incoming message.""" + method = message.get("method") + req_id = message.get("id", "") + params = message.get("params", {}) + + if method == "initialize": + return await self._handle_initialize(req_id, params) + elif method == "tools/list": + return await self._handle_tools_list(req_id) + elif method == "tools/call": + return await self._handle_tools_call(req_id, params) + elif method == "ping": + return {"jsonrpc": "2.0", "result": {"status": "ok"}, "id": req_id} + else: + await self._write_error(-32601, f"Method not found: {method}", req_id) + return None + + async def _handle_initialize(self, req_id: str, params: Dict[str, Any]) -> Dict[str, Any]: + """Handle initialize request.""" + return { + "jsonrpc": "2.0", + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "proxysql-mcp-stdio-bridge", + "version": "1.0.0" + } + }, + "id": req_id + } + + async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: + """Handle tools/list request - forward to ProxySQL.""" + if not self._proxysql: + return { + "jsonrpc": "2.0", + "error": {"code": -32000, "message": "ProxySQL client not initialized"}, + "id": req_id + } + + response = await self._proxysql.tools_list() + + # The response from ProxySQL is the full JSON-RPC response + # We need to extract the result and return it in our format + if "error" in response: + return { + "jsonrpc": "2.0", + "error": response["error"], + "id": req_id + } + + return { + "jsonrpc": "2.0", + "result": response.get("result", {}), + "id": req_id + } + + async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[str, Any]: + """Handle tools/call request - forward to ProxySQL.""" + if not self._proxysql: + return { + "jsonrpc": "2.0", + "error": {"code": -32000, "message": "ProxySQL client not initialized"}, + "id": req_id + } + + name = params.get("name", "") + arguments = params.get("arguments", {}) + + response = await self._proxysql.tools_call(name, arguments, req_id) + + if "error" in response: + return { + "jsonrpc": "2.0", + "error": response["error"], + "id": req_id + } + + return { + "jsonrpc": "2.0", + "result": response.get("result", {}), + "id": req_id + } + + +async def main(): + # Get configuration from environment + endpoint = os.getenv("PROXYSQL_MCP_ENDPOINT", "https://127.0.0.1:6071/mcp/query") + token = os.getenv("PROXYSQL_MCP_TOKEN", "") + insecure_ssl = os.getenv("PROXYSQL_MCP_INSECURE_SSL", "0").lower() in ("1", "true", "yes") + + # Validate endpoint + if not endpoint: + sys.stderr.write("Error: PROXYSQL_MCP_ENDPOINT environment variable is required\n") + sys.exit(1) + + # Run the server + server = StdioMCPServer(endpoint, token or None, verify_ssl=not insecure_ssl) + + try: + await server.run() + except KeyboardInterrupt: + pass + except Exception as e: + sys.stderr.write(f"Error: {e}\n") + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) From 4491f3ce0b3e40607061b1611984f466d585bea2 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 12:33:51 +0000 Subject: [PATCH 02/17] Add debug logging to MCP bridge for troubleshooting Add PROXYSQL_MCP_DEBUG environment variable to enable verbose logging of all stdio communication and ProxySQL HTTP requests/responses. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 28 +++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 24d9015544..40aa613aa1 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -32,6 +32,14 @@ import httpx +# Debug logging to stderr (doesn't interfere with stdio protocol) +DEBUG = os.getenv("PROXYSQL_MCP_DEBUG", "0").lower() in ("1", "true", "yes") + +def debug_log(msg: str): + if DEBUG: + sys.stderr.write(f"[DEBUG] {msg}\n") + sys.stderr.flush() + class ProxySQLMCPEndpoint: """Client for ProxySQL's HTTPS MCP endpoint.""" @@ -84,12 +92,16 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.auth_token: headers["Authorization"] = f"Bearer {self.auth_token}" + debug_log(f"ProxySQL Request: {json.dumps(request)}") + try: r = await self._client.post(self.endpoint, json=request, headers=headers) r.raise_for_status() - return r.json() + response = r.json() + debug_log(f"ProxySQL Response: {json.dumps(response)}") + return response except httpx.HTTPStatusError as e: - return { + error_resp = { "jsonrpc": "2.0", "error": { "code": -32000, @@ -98,8 +110,10 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: }, "id": request.get("id", "") } + debug_log(f"ProxySQL HTTP Error: {json.dumps(error_resp)}") + return error_resp except Exception as e: - return { + error_resp = { "jsonrpc": "2.0", "error": { "code": -32603, @@ -107,6 +121,8 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: }, "id": request.get("id", "") } + debug_log(f"ProxySQL Exception: {json.dumps(error_resp)}") + return error_resp async def tools_list(self) -> Dict[str, Any]: """List available tools.""" @@ -157,15 +173,21 @@ async def run(self): if not line: break + debug_log(f"Received from Claude: {line}") message = json.loads(line) response = await self._handle_message(message) if response: + debug_log(f"Sending to Claude: {json.dumps(response)}") await self._writeline(response) except json.JSONDecodeError as e: + debug_log(f"JSON decode error: {e}") await self._write_error(-32700, f"Parse error: {e}", "") except Exception as e: + debug_log(f"Handler error: {e}") + import traceback + traceback.print_exc(file=sys.stderr) await self._write_error(-32603, f"Internal error: {e}", "") async def _readline(self) -> Optional[str]: From fc6b462be1071cf8fb9ee6d524ac9e54684b93fd Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 12:37:42 +0000 Subject: [PATCH 03/17] Fix: unwrap ProxySQL nested response format ProxySQL MCP wraps tool responses in {"result": {...}, "success": true}. The bridge now unwraps this to return just the actual result to Claude Code. This fixes the LLM error 'The prompt parameter was not received normally' which occurred because the LLM was receiving the malformed nested structure. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 51 ++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 40aa613aa1..fff388da41 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -282,8 +282,10 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: response = await self._proxysql.tools_list() + debug_log(f"tools_list raw response: {json.dumps(response)}") + # The response from ProxySQL is the full JSON-RPC response - # We need to extract the result and return it in our format + # ProxySQL wraps results in {"result": {...}, "success": true} if "error" in response: return { "jsonrpc": "2.0", @@ -291,9 +293,29 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: "id": req_id } + # Extract the actual result from ProxySQL's wrapped format + proxysql_result = response.get("result", {}) + if isinstance(proxysql_result, dict) and "result" in proxysql_result: + # ProxySQL format: {"result": {...}, "success": true} + actual_result = proxysql_result.get("result", {}) + success = proxysql_result.get("success", True) + if not success: + return { + "jsonrpc": "2.0", + "error": {"code": -32000, "message": "ProxySQL tool call failed"}, + "id": req_id + } + debug_log(f"tools_list unwrapped result: {json.dumps(actual_result)}") + return { + "jsonrpc": "2.0", + "result": actual_result, + "id": req_id + } + + # Fallback: return result as-is return { "jsonrpc": "2.0", - "result": response.get("result", {}), + "result": proxysql_result, "id": req_id } @@ -311,6 +333,8 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ response = await self._proxysql.tools_call(name, arguments, req_id) + debug_log(f"tools_call({name}) raw response: {json.dumps(response)}") + if "error" in response: return { "jsonrpc": "2.0", @@ -318,9 +342,30 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ "id": req_id } + # Extract the actual result from ProxySQL's wrapped format + # ProxySQL wraps results in {"result": {...}, "success": true} + proxysql_result = response.get("result", {}) + if isinstance(proxysql_result, dict) and "result" in proxysql_result: + # ProxySQL format: {"result": {...}, "success": true} + actual_result = proxysql_result.get("result", {}) + success = proxysql_result.get("success", True) + if not success: + return { + "jsonrpc": "2.0", + "error": {"code": -32000, "message": "ProxySQL tool call failed"}, + "id": req_id + } + debug_log(f"tools_call({name}) unwrapped result: {json.dumps(actual_result)}") + return { + "jsonrpc": "2.0", + "result": actual_result, + "id": req_id + } + + # Fallback: return result as-is return { "jsonrpc": "2.0", - "result": response.get("result", {}), + "result": proxysql_result, "id": req_id } From 6d83ff1680112581b5eb49c7c670862cee4a21ef Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 15:36:03 +0000 Subject: [PATCH 04/17] Fix: unwrap ProxySQL response format in MCP tools and fix config syntax - Unwrap ProxySQL's {"success": ..., "result": ...} wrapper in tool responses for MCP protocol compliance - Fix proxysql.cfg missing closing brace for mcp_variables section --- lib/MCP_Endpoint.cpp | 26 +++++++++++++++++++++++++- src/proxysql.cfg | 2 ++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index f5484a94a9..70371e67d0 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -341,5 +341,29 @@ json MCP_JSONRPC_Resource::handle_tools_call(const json& req_json) { proxy_debug(PROXY_DEBUG_GENERIC, 2, "MCP tool call: %s with args: %s\n", tool_name.c_str(), arguments.dump().c_str()); - return tool_handler->execute_tool(tool_name, arguments); + json response = tool_handler->execute_tool(tool_name, arguments); + + // Unwrap ProxySQL's {"success": ..., "result": ...} format for MCP compliance + // Tool handlers use create_success_response() which adds this wrapper + if (response.is_object() && response.contains("success") && response.contains("result")) { + bool success = response["success"].get(); + if (!success) { + // Tool execution failed - return error + json error_result; + if (response.contains("error")) { + error_result["error"] = response["error"]; + } else { + error_result["error"] = "Tool execution failed"; + } + if (response.contains("code")) { + error_result["code"] = response["code"]; + } + return error_result; + } + // Success - extract and return the actual result + return response["result"]; + } + + // Fallback: return response as-is (for compatibility with non-standard handlers) + return response; } diff --git a/src/proxysql.cfg b/src/proxysql.cfg index 8ffee0b7fd..aada833802 100644 --- a/src/proxysql.cfg +++ b/src/proxysql.cfg @@ -67,6 +67,8 @@ mcp_variables= mcp_admin_endpoint_auth="" mcp_cache_endpoint_auth="" mcp_timeout_ms=30000 +} + # GenAI module configuration genai_variables= { From edac8eb5e00be0cd0e5ab41cd3978d7221f91309 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 15:51:39 +0000 Subject: [PATCH 05/17] Fix: Add verbose logging and fix stdout buffering issue in MCP stdio bridge - Redirect stderr to /tmp/proxysql_mcp_bridge.log for debugging - Add extreme verbosity with timestamps for all stdin/stdout/HTTP traffic - CRITICAL FIX: Set stdout to line-buffered mode to prevent responses from being buffered and never reaching Claude Code (causing timeouts) - Log all HTTP requests/responses to ProxySQL MCP server - Log all message handling and unwrapping operations --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 171 +++++++++++++++++++---- 1 file changed, 147 insertions(+), 24 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index fff388da41..eaf4ed2d68 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -29,16 +29,35 @@ import os import sys from typing import Any, Dict, Optional +from datetime import datetime import httpx -# Debug logging to stderr (doesn't interfere with stdio protocol) -DEBUG = os.getenv("PROXYSQL_MCP_DEBUG", "0").lower() in ("1", "true", "yes") +# Redirect stderr to a log file in /tmp +LOG_FILE = "/tmp/proxysql_mcp_bridge.log" +stderr_log_file = open(LOG_FILE, "a", buffering=1) +sys.stderr = stderr_log_file +sys.__stderr__ = stderr_log_file + +# CRITICAL: Ensure stdout is line-buffered for stdio MCP protocol +# Without this, responses may be buffered and never sent to Claude Code +sys.stdout.reconfigure(line_buffering=True) + +# Debug logging - ALWAYS ON for extreme verbosity +VERBOSE = True # Always verbose logging + +def log_timestamp(): + return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] def debug_log(msg: str): - if DEBUG: - sys.stderr.write(f"[DEBUG] {msg}\n") - sys.stderr.flush() + """Always log everything for extreme verbosity.""" + timestamp = log_timestamp() + sys.stderr.write(f"[{timestamp}] {msg}\n") + sys.stderr.flush() + +def log_separator(char="=", length=80): + sys.stderr.write(char * length + "\n") + sys.stderr.flush() class ProxySQLMCPEndpoint: @@ -66,6 +85,10 @@ async def __aexit__(self, *args): async def _initialize(self): """Initialize the MCP connection.""" + log_separator("=") + debug_log("[ProxySQLMCPEndpoint] Initializing connection to ProxySQL MCP server") + log_separator("=") + request = { "jsonrpc": "2.0", "id": 1, @@ -81,6 +104,10 @@ async def _initialize(self): } response = await self._call(request) self._initialized = True + + log_separator("=") + debug_log("[ProxySQLMCPEndpoint] Initialization complete") + log_separator("=") return response async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: @@ -92,13 +119,25 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.auth_token: headers["Authorization"] = f"Bearer {self.auth_token}" - debug_log(f"ProxySQL Request: {json.dumps(request)}") + log_separator("-") + debug_log(f"[HTTP REQUEST TO PROXYSQL MCP SERVER]") + debug_log(f" URL: {self.endpoint}") + debug_log(f" Headers: {json.dumps(headers)}") + debug_log(f" Body: {json.dumps(request, indent=2)}") + log_separator("-") try: r = await self._client.post(self.endpoint, json=request, headers=headers) r.raise_for_status() response = r.json() - debug_log(f"ProxySQL Response: {json.dumps(response)}") + + log_separator("-") + debug_log(f"[HTTP RESPONSE FROM PROXYSQL MCP SERVER]") + debug_log(f" Status: {r.status_code}") + debug_log(f" Headers: {dict(r.headers)}") + debug_log(f" Body: {json.dumps(response, indent=2)}") + log_separator("-") + return response except httpx.HTTPStatusError as e: error_resp = { @@ -110,7 +149,12 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: }, "id": request.get("id", "") } - debug_log(f"ProxySQL HTTP Error: {json.dumps(error_resp)}") + log_separator("-") + debug_log(f"[HTTP ERROR FROM PROXYSQL MCP SERVER]") + debug_log(f" Status: {e.response.status_code}") + debug_log(f" Response: {e.response.text}") + debug_log(f" Error Response: {json.dumps(error_resp, indent=2)}") + log_separator("-") return error_resp except Exception as e: error_resp = { @@ -121,7 +165,11 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: }, "id": request.get("id", "") } - debug_log(f"ProxySQL Exception: {json.dumps(error_resp)}") + log_separator("-") + debug_log(f"[EXCEPTION DURING HTTP REQUEST]") + debug_log(f" Exception: {type(e).__name__}: {e}") + debug_log(f" Error Response: {json.dumps(error_resp, indent=2)}") + log_separator("-") return error_resp async def tools_list(self) -> Dict[str, Any]: @@ -160,6 +208,13 @@ def __init__(self, proxysql_endpoint: str, auth_token: Optional[str] = None, ver async def run(self): """Main server loop.""" + log_separator("=") + debug_log("[PROXYSQL MCP STDIO BRIDGE STARTING]") + debug_log(f" Endpoint: {self.proxysql_endpoint}") + debug_log(f" Auth Token: {'***SET***' if self.auth_token else 'NONE'}") + debug_log(f" Verify SSL: {self.verify_ssl}") + log_separator("=") + async with ProxySQLMCPEndpoint(self.proxysql_endpoint, self.auth_token, self.verify_ssl) as client: self._proxysql = client @@ -167,25 +222,45 @@ async def run(self): await self._write_notification("notifications/initialized") # Main message loop + msg_count = 0 while True: try: line = await self._readline() if not line: + debug_log("[STDIN CLOSED - RECEIVED EOF]") break - debug_log(f"Received from Claude: {line}") - message = json.loads(line) + msg_count += 1 + log_separator("=") + debug_log(f"[MESSAGE #{msg_count} - RECEIVED FROM STDIN]") + debug_log(f" Raw line: {repr(line)}") + debug_log(f" Parsed JSON:") + try: + message = json.loads(line) + debug_log(f" {json.dumps(message, indent=4)}") + except json.JSONDecodeError as e: + debug_log(f" [INVALID JSON - {e}]") + raise + log_separator("=") + response = await self._handle_message(message) if response: - debug_log(f"Sending to Claude: {json.dumps(response)}") + log_separator("=") + debug_log(f"[MESSAGE #{msg_count} - SENDING TO STDOUT]") + debug_log(f" Response JSON:") + debug_log(f" {json.dumps(response, indent=4)}") + log_separator("=") await self._writeline(response) + else: + debug_log(f"[MESSAGE #{msg_count} - NO RESPONSE (notification only)]") except json.JSONDecodeError as e: - debug_log(f"JSON decode error: {e}") + debug_log(f"[JSON DECODE ERROR]: {e}") + debug_log(f" Invalid line: {repr(line)}") await self._write_error(-32700, f"Parse error: {e}", "") except Exception as e: - debug_log(f"Handler error: {e}") + debug_log(f"[HANDLER ERROR]: {e}") import traceback traceback.print_exc(file=sys.stderr) await self._write_error(-32603, f"Internal error: {e}", "") @@ -213,6 +288,7 @@ async def _write_notification(self, method: str, params: Optional[Dict[str, Any] } if params: notification["params"] = params + debug_log(f"[NOTIFICATION] Sending: {json.dumps(notification, indent=4)}") await self._writeline(notification) async def _write_response(self, result: Any, req_id: str): @@ -242,6 +318,8 @@ async def _handle_message(self, message: Dict[str, Any]) -> Optional[Dict[str, A req_id = message.get("id", "") params = message.get("params", {}) + debug_log(f"[HANDLE MESSAGE] method='{method}', id='{req_id}'") + if method == "initialize": return await self._handle_initialize(req_id, params) elif method == "tools/list": @@ -249,14 +327,19 @@ async def _handle_message(self, message: Dict[str, Any]) -> Optional[Dict[str, A elif method == "tools/call": return await self._handle_tools_call(req_id, params) elif method == "ping": + debug_log(f"[ping] Responding with status=ok") return {"jsonrpc": "2.0", "result": {"status": "ok"}, "id": req_id} else: + debug_log(f"[HANDLE MESSAGE] Unknown method: {method}") await self._write_error(-32601, f"Method not found: {method}", req_id) return None async def _handle_initialize(self, req_id: str, params: Dict[str, Any]) -> Dict[str, Any]: """Handle initialize request.""" - return { + debug_log(f"[initialize] Handling request with id={req_id}") + debug_log(f"[initialize] Client params: {json.dumps(params, indent=4)}") + + result = { "jsonrpc": "2.0", "result": { "protocolVersion": "2024-11-05", @@ -270,10 +353,15 @@ async def _handle_initialize(self, req_id: str, params: Dict[str, Any]) -> Dict[ }, "id": req_id } + debug_log(f"[initialize] Sending response: {json.dumps(result['result'], indent=4)}") + return result async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: """Handle tools/list request - forward to ProxySQL.""" + debug_log(f"[tools/list] Handling request with id={req_id}") + if not self._proxysql: + debug_log(f"[tools/list] ERROR - ProxySQL client not initialized") return { "jsonrpc": "2.0", "error": {"code": -32000, "message": "ProxySQL client not initialized"}, @@ -282,11 +370,15 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: response = await self._proxysql.tools_list() - debug_log(f"tools_list raw response: {json.dumps(response)}") + log_separator("-") + debug_log(f"[tools/list] Raw response from ProxySQL:") + debug_log(f" {json.dumps(response, indent=4)}") + log_separator("-") # The response from ProxySQL is the full JSON-RPC response # ProxySQL wraps results in {"result": {...}, "success": true} if "error" in response: + debug_log(f"[tools/list] Returning error to client") return { "jsonrpc": "2.0", "error": response["error"], @@ -299,13 +391,18 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: # ProxySQL format: {"result": {...}, "success": true} actual_result = proxysql_result.get("result", {}) success = proxysql_result.get("success", True) + debug_log(f"[tools/list] Detected ProxySQL wrapped format, success={success}") if not success: + debug_log(f"[tools/list] ERROR - ProxySQL reported failure") return { "jsonrpc": "2.0", "error": {"code": -32000, "message": "ProxySQL tool call failed"}, "id": req_id } - debug_log(f"tools_list unwrapped result: {json.dumps(actual_result)}") + log_separator("-") + debug_log(f"[tools/list] Unwrapped result:") + debug_log(f" {json.dumps(actual_result, indent=4)}") + log_separator("-") return { "jsonrpc": "2.0", "result": actual_result, @@ -313,6 +410,7 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: } # Fallback: return result as-is + debug_log(f"[tools/list] No wrapping detected, returning result as-is") return { "jsonrpc": "2.0", "result": proxysql_result, @@ -321,21 +419,28 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[str, Any]: """Handle tools/call request - forward to ProxySQL.""" + name = params.get("name", "") + arguments = params.get("arguments", {}) + debug_log(f"[tools/call] Handling request: tool='{name}', id={req_id}") + debug_log(f"[tools/call] Arguments: {json.dumps(arguments, indent=4)}") + if not self._proxysql: + debug_log(f"[tools/call] ERROR - ProxySQL client not initialized") return { "jsonrpc": "2.0", "error": {"code": -32000, "message": "ProxySQL client not initialized"}, "id": req_id } - name = params.get("name", "") - arguments = params.get("arguments", {}) - response = await self._proxysql.tools_call(name, arguments, req_id) - debug_log(f"tools_call({name}) raw response: {json.dumps(response)}") + log_separator("-") + debug_log(f"[tools/call] Raw response from ProxySQL:") + debug_log(f" {json.dumps(response, indent=4)}") + log_separator("-") if "error" in response: + debug_log(f"[tools/call] Returning error to client") return { "jsonrpc": "2.0", "error": response["error"], @@ -349,13 +454,18 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ # ProxySQL format: {"result": {...}, "success": true} actual_result = proxysql_result.get("result", {}) success = proxysql_result.get("success", True) + debug_log(f"[tools/call] Detected ProxySQL wrapped format, success={success}") if not success: + debug_log(f"[tools/call] ERROR - ProxySQL reported failure") return { "jsonrpc": "2.0", "error": {"code": -32000, "message": "ProxySQL tool call failed"}, "id": req_id } - debug_log(f"tools_call({name}) unwrapped result: {json.dumps(actual_result)}") + log_separator("-") + debug_log(f"[tools/call] Unwrapped result:") + debug_log(f" {json.dumps(actual_result, indent=4)}") + log_separator("-") return { "jsonrpc": "2.0", "result": actual_result, @@ -363,6 +473,7 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ } # Fallback: return result as-is + debug_log(f"[tools/call] No wrapping detected, returning result as-is") return { "jsonrpc": "2.0", "result": proxysql_result, @@ -371,11 +482,21 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ async def main(): + log_separator("=") + debug_log("[PROXYSQL MCP STDIO BRIDGE - MAIN STARTING]") + log_separator("=") + # Get configuration from environment endpoint = os.getenv("PROXYSQL_MCP_ENDPOINT", "https://127.0.0.1:6071/mcp/query") token = os.getenv("PROXYSQL_MCP_TOKEN", "") insecure_ssl = os.getenv("PROXYSQL_MCP_INSECURE_SSL", "0").lower() in ("1", "true", "yes") + debug_log(f"[CONFIG] PROXYSQL_MCP_ENDPOINT: {endpoint}") + debug_log(f"[CONFIG] PROXYSQL_MCP_TOKEN: {'***SET***' if token else 'NOT SET'}") + debug_log(f"[CONFIG] PROXYSQL_MCP_INSECURE_SSL: {insecure_ssl}") + debug_log(f"[CONFIG] LOG_FILE: {LOG_FILE}") + log_separator("=") + # Validate endpoint if not endpoint: sys.stderr.write("Error: PROXYSQL_MCP_ENDPOINT environment variable is required\n") @@ -387,9 +508,11 @@ async def main(): try: await server.run() except KeyboardInterrupt: - pass + debug_log("[MAIN] Interrupted by KeyboardInterrupt") except Exception as e: - sys.stderr.write(f"Error: {e}\n") + debug_log(f"[MAIN] ERROR: {e}") + import traceback + traceback.print_exc(file=sys.stderr) sys.exit(1) From f5606986ff3b89e14b5c30dfc6bce7ba5b8b4e76 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:06:33 +0000 Subject: [PATCH 06/17] Fix: Replace stdout with truly unbuffered wrapper to prevent response buffering The previous sys.stdout.reconfigure(line_buffering=True) didn't work when stderr is redirected. Now we create a new io.TextIOWrapper around sys.stdout.buffer with line_buffering=False, ensuring immediate flush. Also sets PYTHONUNBUFFERED=1 for extra safety. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index eaf4ed2d68..0235d6c24f 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -25,6 +25,7 @@ """ import asyncio +import io import json import os import sys @@ -33,15 +34,29 @@ import httpx +# CRITICAL: Ensure unbuffered stdout for MCP stdio protocol +# Also set PYTHONUNBUFFERED=1 in environment for extra safety +os.environ['PYTHONUNBUFFERED'] = '1' + # Redirect stderr to a log file in /tmp LOG_FILE = "/tmp/proxysql_mcp_bridge.log" stderr_log_file = open(LOG_FILE, "a", buffering=1) sys.stderr = stderr_log_file sys.__stderr__ = stderr_log_file -# CRITICAL: Ensure stdout is line-buffered for stdio MCP protocol -# Without this, responses may be buffered and never sent to Claude Code -sys.stdout.reconfigure(line_buffering=True) +# CRITICAL: Force stdout to be unbuffered +# Reconfigure doesn't work reliably when stderr is redirected, so we +# need to replace stdout with an unbuffered wrapper +unbuffered_stdout = io.TextIOWrapper( + sys.stdout.buffer, + encoding='utf-8', + errors='strict', + newline='\n', + line_buffering=False # Explicitly disable line buffering too +) +sys.stdout = unbuffered_stdout +# Also update __stdout__ for completeness +sys.__stdout__ = unbuffered_stdout # Debug logging - ALWAYS ON for extreme verbosity VERBOSE = True # Always verbose logging From 55dd5ba574dc3268b5477b40318c69e4928873a5 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:11:34 +0000 Subject: [PATCH 07/17] Debug: Add detailed stdout write logging to troubleshoot Claude Code timeout - Revert the stdout replacement changes (was probably not the issue) - Add detailed logging to _writeline to see exactly what's happening when writing to stdout --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 28 ++++++++---------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 0235d6c24f..21bb9e75ce 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -25,7 +25,6 @@ """ import asyncio -import io import json import os import sys @@ -34,30 +33,12 @@ import httpx -# CRITICAL: Ensure unbuffered stdout for MCP stdio protocol -# Also set PYTHONUNBUFFERED=1 in environment for extra safety -os.environ['PYTHONUNBUFFERED'] = '1' - # Redirect stderr to a log file in /tmp LOG_FILE = "/tmp/proxysql_mcp_bridge.log" stderr_log_file = open(LOG_FILE, "a", buffering=1) sys.stderr = stderr_log_file sys.__stderr__ = stderr_log_file -# CRITICAL: Force stdout to be unbuffered -# Reconfigure doesn't work reliably when stderr is redirected, so we -# need to replace stdout with an unbuffered wrapper -unbuffered_stdout = io.TextIOWrapper( - sys.stdout.buffer, - encoding='utf-8', - errors='strict', - newline='\n', - line_buffering=False # Explicitly disable line buffering too -) -sys.stdout = unbuffered_stdout -# Also update __stdout__ for completeness -sys.__stdout__ = unbuffered_stdout - # Debug logging - ALWAYS ON for extreme verbosity VERBOSE = True # Always verbose logging @@ -292,9 +273,18 @@ async def _writeline(self, data: Any): """Write JSON data to stdout.""" loop = asyncio.get_event_loop() output = json.dumps(data, ensure_ascii=False) + "\n" + + debug_log(f"[_writeline] Writing {len(output)} bytes to stdout") + debug_log(f"[_writeline] sys.stdout: {sys.stdout}") + debug_log(f"[_writeline] sys.stdout.fileno(): {sys.stdout.fileno() if hasattr(sys.stdout, 'fileno') else 'N/A'}") + await loop.run_in_executor(None, sys.stdout.write, output) + + debug_log(f"[_writeline] Data written, now flushing...") await loop.run_in_executor(None, sys.stdout.flush) + debug_log(f"[_writeline] Flush complete") + async def _write_notification(self, method: str, params: Optional[Dict[str, Any]] = None): """Write a notification (no id).""" notification = { From 2b5134632c5efdf21489e02f6aed9da9aff4152e Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:17:00 +0000 Subject: [PATCH 08/17] Fix: Wrap tool results in TextContent format for MCP protocol compliance The MCP protocol requires tool call results to be wrapped in content items with type and text fields. This matches what other MCP servers do. Before: {"result": [{"name": "testdb", ...}]} After: {"result": [{"type": "text", "text": "[{\"name\": \"testdb\", ...}]"}]} This should fix the issue where Claude Code was timing out waiting for responses. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 21bb9e75ce..5ac29a6459 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -471,17 +471,22 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ debug_log(f"[tools/call] Unwrapped result:") debug_log(f" {json.dumps(actual_result, indent=4)}") log_separator("-") + # Wrap in TextContent for MCP protocol compliance + wrapped_result = [{"type": "text", "text": json.dumps(actual_result, indent=2)}] + debug_log(f"[tools/call] Wrapped in TextContent: {json.dumps(wrapped_result, indent=4)}") return { "jsonrpc": "2.0", - "result": actual_result, + "result": wrapped_result, "id": req_id } - # Fallback: return result as-is - debug_log(f"[tools/call] No wrapping detected, returning result as-is") + # Fallback: return result as-is, wrapped in TextContent + debug_log(f"[tools/call] No wrapping detected, wrapping result in TextContent") + wrapped_result = [{"type": "text", "text": json.dumps(proxysql_result, indent=2)}] + debug_log(f"[tools/call] Wrapped result: {json.dumps(wrapped_result, indent=4)}") return { "jsonrpc": "2.0", - "result": proxysql_result, + "result": wrapped_result, "id": req_id } From ad54f92dc59ed446b913a84263716a366660e195 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:21:26 +0000 Subject: [PATCH 09/17] Revert: Simplify tool handlers back to original pass-through Remove all the unwrapping and TextContent wrapping logic that was added. Go back to the original simple pass-through that just returns the result from ProxySQL directly. The original format was correct. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 93 +++--------------------- 1 file changed, 10 insertions(+), 83 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 5ac29a6459..d1cecd2274 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -363,10 +363,7 @@ async def _handle_initialize(self, req_id: str, params: Dict[str, Any]) -> Dict[ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: """Handle tools/list request - forward to ProxySQL.""" - debug_log(f"[tools/list] Handling request with id={req_id}") - if not self._proxysql: - debug_log(f"[tools/list] ERROR - ProxySQL client not initialized") return { "jsonrpc": "2.0", "error": {"code": -32000, "message": "ProxySQL client not initialized"}, @@ -375,118 +372,48 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: response = await self._proxysql.tools_list() - log_separator("-") - debug_log(f"[tools/list] Raw response from ProxySQL:") - debug_log(f" {json.dumps(response, indent=4)}") - log_separator("-") - - # The response from ProxySQL is the full JSON-RPC response - # ProxySQL wraps results in {"result": {...}, "success": true} if "error" in response: - debug_log(f"[tools/list] Returning error to client") return { "jsonrpc": "2.0", "error": response["error"], "id": req_id } - # Extract the actual result from ProxySQL's wrapped format - proxysql_result = response.get("result", {}) - if isinstance(proxysql_result, dict) and "result" in proxysql_result: - # ProxySQL format: {"result": {...}, "success": true} - actual_result = proxysql_result.get("result", {}) - success = proxysql_result.get("success", True) - debug_log(f"[tools/list] Detected ProxySQL wrapped format, success={success}") - if not success: - debug_log(f"[tools/list] ERROR - ProxySQL reported failure") - return { - "jsonrpc": "2.0", - "error": {"code": -32000, "message": "ProxySQL tool call failed"}, - "id": req_id - } - log_separator("-") - debug_log(f"[tools/list] Unwrapped result:") - debug_log(f" {json.dumps(actual_result, indent=4)}") - log_separator("-") - return { - "jsonrpc": "2.0", - "result": actual_result, - "id": req_id - } - - # Fallback: return result as-is - debug_log(f"[tools/list] No wrapping detected, returning result as-is") return { "jsonrpc": "2.0", - "result": proxysql_result, + "result": response.get("result", {}), "id": req_id } async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[str, Any]: """Handle tools/call request - forward to ProxySQL.""" - name = params.get("name", "") - arguments = params.get("arguments", {}) - debug_log(f"[tools/call] Handling request: tool='{name}', id={req_id}") - debug_log(f"[tools/call] Arguments: {json.dumps(arguments, indent=4)}") - if not self._proxysql: - debug_log(f"[tools/call] ERROR - ProxySQL client not initialized") return { "jsonrpc": "2.0", "error": {"code": -32000, "message": "ProxySQL client not initialized"}, "id": req_id } - response = await self._proxysql.tools_call(name, arguments, req_id) + name = params.get("name", "") + arguments = params.get("arguments", {}) - log_separator("-") - debug_log(f"[tools/call] Raw response from ProxySQL:") - debug_log(f" {json.dumps(response, indent=4)}") - log_separator("-") + debug_log(f"[tools/call] Calling tool='{name}' with args: {json.dumps(arguments)}") + + response = await self._proxysql.tools_call(name, arguments, req_id) if "error" in response: - debug_log(f"[tools/call] Returning error to client") + debug_log(f"[tools/call] Error from ProxySQL: {response['error']}") return { "jsonrpc": "2.0", "error": response["error"], "id": req_id } - # Extract the actual result from ProxySQL's wrapped format - # ProxySQL wraps results in {"result": {...}, "success": true} - proxysql_result = response.get("result", {}) - if isinstance(proxysql_result, dict) and "result" in proxysql_result: - # ProxySQL format: {"result": {...}, "success": true} - actual_result = proxysql_result.get("result", {}) - success = proxysql_result.get("success", True) - debug_log(f"[tools/call] Detected ProxySQL wrapped format, success={success}") - if not success: - debug_log(f"[tools/call] ERROR - ProxySQL reported failure") - return { - "jsonrpc": "2.0", - "error": {"code": -32000, "message": "ProxySQL tool call failed"}, - "id": req_id - } - log_separator("-") - debug_log(f"[tools/call] Unwrapped result:") - debug_log(f" {json.dumps(actual_result, indent=4)}") - log_separator("-") - # Wrap in TextContent for MCP protocol compliance - wrapped_result = [{"type": "text", "text": json.dumps(actual_result, indent=2)}] - debug_log(f"[tools/call] Wrapped in TextContent: {json.dumps(wrapped_result, indent=4)}") - return { - "jsonrpc": "2.0", - "result": wrapped_result, - "id": req_id - } - - # Fallback: return result as-is, wrapped in TextContent - debug_log(f"[tools/call] No wrapping detected, wrapping result in TextContent") - wrapped_result = [{"type": "text", "text": json.dumps(proxysql_result, indent=2)}] - debug_log(f"[tools/call] Wrapped result: {json.dumps(wrapped_result, indent=4)}") + # Simply pass through the result - no wrapping, no unwrapping + debug_log(f"[tools/call] Returning result: {json.dumps(response.get('result', {}))}") return { "jsonrpc": "2.0", - "result": wrapped_result, + "result": response.get("result", {}), "id": req_id } From f4a4af8d828207defb9c9c464a9731bcc331aee4 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:27:52 +0000 Subject: [PATCH 10/17] Fix: Write directly to stdout.buffer to bypass TextIOWrapper issues The TextIOWrapper may have buffering issues when stderr is redirected. Writing directly to the binary buffer with encoded bytes ensures immediate delivery of responses to Claude Code. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index d1cecd2274..849c449373 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -271,17 +271,18 @@ async def _readline(self) -> Optional[str]: async def _writeline(self, data: Any): """Write JSON data to stdout.""" - loop = asyncio.get_event_loop() output = json.dumps(data, ensure_ascii=False) + "\n" + output_bytes = output.encode('utf-8') - debug_log(f"[_writeline] Writing {len(output)} bytes to stdout") + debug_log(f"[_writeline] Writing {len(output_bytes)} bytes to stdout") debug_log(f"[_writeline] sys.stdout: {sys.stdout}") - debug_log(f"[_writeline] sys.stdout.fileno(): {sys.stdout.fileno() if hasattr(sys.stdout, 'fileno') else 'N/A'}") - - await loop.run_in_executor(None, sys.stdout.write, output) + debug_log(f"[_writeline] sys.stdout.buffer: {sys.stdout.buffer}") - debug_log(f"[_writeline] Data written, now flushing...") - await loop.run_in_executor(None, sys.stdout.flush) + # Write directly to the binary buffer to avoid any TextIOWrapper issues + # This bypasses Python's text encoding layer and writes raw bytes + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, sys.stdout.buffer.write, output_bytes) + await loop.run_in_executor(None, sys.stdout.buffer.flush) debug_log(f"[_writeline] Flush complete") From 23e5efca5a19560eca5380b9a07bf2970a9ceba4 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:30:38 +0000 Subject: [PATCH 11/17] Test: Don't redirect sys.stderr, write logs directly to file The sys.stderr redirection may be interfering with stdout/stdin pipes. Now logging writes directly to a file handle instead of through sys.stderr. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 26 +++++++++++++----------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 849c449373..fc00b35935 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -33,27 +33,29 @@ import httpx -# Redirect stderr to a log file in /tmp +# DON'T redirect stderr - it may interfere with stdout/stdin pipes +# Commented out to test if this is causing the issue +# LOG_FILE = "/tmp/proxysql_mcp_bridge.log" +# stderr_log_file = open(LOG_FILE, "a", buffering=1) +# sys.stderr = stderr_log_file +# sys.__stderr__ = stderr_log_file + +# Debug logging - write to file instead of stderr to avoid pipe interference LOG_FILE = "/tmp/proxysql_mcp_bridge.log" -stderr_log_file = open(LOG_FILE, "a", buffering=1) -sys.stderr = stderr_log_file -sys.__stderr__ = stderr_log_file - -# Debug logging - ALWAYS ON for extreme verbosity -VERBOSE = True # Always verbose logging +_log_file = open(LOG_FILE, "a", buffering=1) def log_timestamp(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] def debug_log(msg: str): - """Always log everything for extreme verbosity.""" + """Write to log file instead of stderr.""" timestamp = log_timestamp() - sys.stderr.write(f"[{timestamp}] {msg}\n") - sys.stderr.flush() + _log_file.write(f"[{timestamp}] {msg}\n") + _log_file.flush() def log_separator(char="=", length=80): - sys.stderr.write(char * length + "\n") - sys.stderr.flush() + _log_file.write(char * length + "\n") + _log_file.flush() class ProxySQLMCPEndpoint: From a47567fee7ac1c7d65228264822f773b1528fca2 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:35:49 +0000 Subject: [PATCH 12/17] Revert: Restore original bridge completely Restore to exact original code from commit 01c182cc. The original code is clean and simple - just passes through responses. Only added minimal file-based logging for debugging. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 155 +++-------------------- 1 file changed, 16 insertions(+), 139 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index fc00b35935..1da7732381 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -33,28 +33,10 @@ import httpx -# DON'T redirect stderr - it may interfere with stdout/stdin pipes -# Commented out to test if this is causing the issue -# LOG_FILE = "/tmp/proxysql_mcp_bridge.log" -# stderr_log_file = open(LOG_FILE, "a", buffering=1) -# sys.stderr = stderr_log_file -# sys.__stderr__ = stderr_log_file - -# Debug logging - write to file instead of stderr to avoid pipe interference -LOG_FILE = "/tmp/proxysql_mcp_bridge.log" -_log_file = open(LOG_FILE, "a", buffering=1) - -def log_timestamp(): - return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] - -def debug_log(msg: str): - """Write to log file instead of stderr.""" - timestamp = log_timestamp() - _log_file.write(f"[{timestamp}] {msg}\n") - _log_file.flush() - -def log_separator(char="=", length=80): - _log_file.write(char * length + "\n") +# Minimal logging to file for debugging +_log_file = open("/tmp/proxysql_mcp_bridge.log", "a", buffering=1) +def _log(msg): + _log_file.write(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] {msg}\n") _log_file.flush() @@ -83,10 +65,6 @@ async def __aexit__(self, *args): async def _initialize(self): """Initialize the MCP connection.""" - log_separator("=") - debug_log("[ProxySQLMCPEndpoint] Initializing connection to ProxySQL MCP server") - log_separator("=") - request = { "jsonrpc": "2.0", "id": 1, @@ -102,10 +80,6 @@ async def _initialize(self): } response = await self._call(request) self._initialized = True - - log_separator("=") - debug_log("[ProxySQLMCPEndpoint] Initialization complete") - log_separator("=") return response async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: @@ -117,28 +91,12 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: if self.auth_token: headers["Authorization"] = f"Bearer {self.auth_token}" - log_separator("-") - debug_log(f"[HTTP REQUEST TO PROXYSQL MCP SERVER]") - debug_log(f" URL: {self.endpoint}") - debug_log(f" Headers: {json.dumps(headers)}") - debug_log(f" Body: {json.dumps(request, indent=2)}") - log_separator("-") - try: r = await self._client.post(self.endpoint, json=request, headers=headers) r.raise_for_status() - response = r.json() - - log_separator("-") - debug_log(f"[HTTP RESPONSE FROM PROXYSQL MCP SERVER]") - debug_log(f" Status: {r.status_code}") - debug_log(f" Headers: {dict(r.headers)}") - debug_log(f" Body: {json.dumps(response, indent=2)}") - log_separator("-") - - return response + return r.json() except httpx.HTTPStatusError as e: - error_resp = { + return { "jsonrpc": "2.0", "error": { "code": -32000, @@ -147,15 +105,8 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: }, "id": request.get("id", "") } - log_separator("-") - debug_log(f"[HTTP ERROR FROM PROXYSQL MCP SERVER]") - debug_log(f" Status: {e.response.status_code}") - debug_log(f" Response: {e.response.text}") - debug_log(f" Error Response: {json.dumps(error_resp, indent=2)}") - log_separator("-") - return error_resp except Exception as e: - error_resp = { + return { "jsonrpc": "2.0", "error": { "code": -32603, @@ -163,12 +114,6 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: }, "id": request.get("id", "") } - log_separator("-") - debug_log(f"[EXCEPTION DURING HTTP REQUEST]") - debug_log(f" Exception: {type(e).__name__}: {e}") - debug_log(f" Error Response: {json.dumps(error_resp, indent=2)}") - log_separator("-") - return error_resp async def tools_list(self) -> Dict[str, Any]: """List available tools.""" @@ -206,13 +151,6 @@ def __init__(self, proxysql_endpoint: str, auth_token: Optional[str] = None, ver async def run(self): """Main server loop.""" - log_separator("=") - debug_log("[PROXYSQL MCP STDIO BRIDGE STARTING]") - debug_log(f" Endpoint: {self.proxysql_endpoint}") - debug_log(f" Auth Token: {'***SET***' if self.auth_token else 'NONE'}") - debug_log(f" Verify SSL: {self.verify_ssl}") - log_separator("=") - async with ProxySQLMCPEndpoint(self.proxysql_endpoint, self.auth_token, self.verify_ssl) as client: self._proxysql = client @@ -220,47 +158,21 @@ async def run(self): await self._write_notification("notifications/initialized") # Main message loop - msg_count = 0 while True: try: line = await self._readline() if not line: - debug_log("[STDIN CLOSED - RECEIVED EOF]") break - msg_count += 1 - log_separator("=") - debug_log(f"[MESSAGE #{msg_count} - RECEIVED FROM STDIN]") - debug_log(f" Raw line: {repr(line)}") - debug_log(f" Parsed JSON:") - try: - message = json.loads(line) - debug_log(f" {json.dumps(message, indent=4)}") - except json.JSONDecodeError as e: - debug_log(f" [INVALID JSON - {e}]") - raise - log_separator("=") - + message = json.loads(line) response = await self._handle_message(message) if response: - log_separator("=") - debug_log(f"[MESSAGE #{msg_count} - SENDING TO STDOUT]") - debug_log(f" Response JSON:") - debug_log(f" {json.dumps(response, indent=4)}") - log_separator("=") await self._writeline(response) - else: - debug_log(f"[MESSAGE #{msg_count} - NO RESPONSE (notification only)]") except json.JSONDecodeError as e: - debug_log(f"[JSON DECODE ERROR]: {e}") - debug_log(f" Invalid line: {repr(line)}") await self._write_error(-32700, f"Parse error: {e}", "") except Exception as e: - debug_log(f"[HANDLER ERROR]: {e}") - import traceback - traceback.print_exc(file=sys.stderr) await self._write_error(-32603, f"Internal error: {e}", "") async def _readline(self) -> Optional[str]: @@ -273,20 +185,10 @@ async def _readline(self) -> Optional[str]: async def _writeline(self, data: Any): """Write JSON data to stdout.""" - output = json.dumps(data, ensure_ascii=False) + "\n" - output_bytes = output.encode('utf-8') - - debug_log(f"[_writeline] Writing {len(output_bytes)} bytes to stdout") - debug_log(f"[_writeline] sys.stdout: {sys.stdout}") - debug_log(f"[_writeline] sys.stdout.buffer: {sys.stdout.buffer}") - - # Write directly to the binary buffer to avoid any TextIOWrapper issues - # This bypasses Python's text encoding layer and writes raw bytes loop = asyncio.get_event_loop() - await loop.run_in_executor(None, sys.stdout.buffer.write, output_bytes) - await loop.run_in_executor(None, sys.stdout.buffer.flush) - - debug_log(f"[_writeline] Flush complete") + output = json.dumps(data, ensure_ascii=False) + "\n" + await loop.run_in_executor(None, sys.stdout.write, output) + await loop.run_in_executor(None, sys.stdout.flush) async def _write_notification(self, method: str, params: Optional[Dict[str, Any]] = None): """Write a notification (no id).""" @@ -296,7 +198,6 @@ async def _write_notification(self, method: str, params: Optional[Dict[str, Any] } if params: notification["params"] = params - debug_log(f"[NOTIFICATION] Sending: {json.dumps(notification, indent=4)}") await self._writeline(notification) async def _write_response(self, result: Any, req_id: str): @@ -326,8 +227,6 @@ async def _handle_message(self, message: Dict[str, Any]) -> Optional[Dict[str, A req_id = message.get("id", "") params = message.get("params", {}) - debug_log(f"[HANDLE MESSAGE] method='{method}', id='{req_id}'") - if method == "initialize": return await self._handle_initialize(req_id, params) elif method == "tools/list": @@ -335,19 +234,14 @@ async def _handle_message(self, message: Dict[str, Any]) -> Optional[Dict[str, A elif method == "tools/call": return await self._handle_tools_call(req_id, params) elif method == "ping": - debug_log(f"[ping] Responding with status=ok") return {"jsonrpc": "2.0", "result": {"status": "ok"}, "id": req_id} else: - debug_log(f"[HANDLE MESSAGE] Unknown method: {method}") await self._write_error(-32601, f"Method not found: {method}", req_id) return None async def _handle_initialize(self, req_id: str, params: Dict[str, Any]) -> Dict[str, Any]: """Handle initialize request.""" - debug_log(f"[initialize] Handling request with id={req_id}") - debug_log(f"[initialize] Client params: {json.dumps(params, indent=4)}") - - result = { + return { "jsonrpc": "2.0", "result": { "protocolVersion": "2024-11-05", @@ -361,8 +255,6 @@ async def _handle_initialize(self, req_id: str, params: Dict[str, Any]) -> Dict[ }, "id": req_id } - debug_log(f"[initialize] Sending response: {json.dumps(result['result'], indent=4)}") - return result async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: """Handle tools/list request - forward to ProxySQL.""" @@ -375,6 +267,8 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: response = await self._proxysql.tools_list() + # The response from ProxySQL is the full JSON-RPC response + # We need to extract the result and return it in our format if "error" in response: return { "jsonrpc": "2.0", @@ -400,20 +294,15 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ name = params.get("name", "") arguments = params.get("arguments", {}) - debug_log(f"[tools/call] Calling tool='{name}' with args: {json.dumps(arguments)}") - response = await self._proxysql.tools_call(name, arguments, req_id) if "error" in response: - debug_log(f"[tools/call] Error from ProxySQL: {response['error']}") return { "jsonrpc": "2.0", "error": response["error"], "id": req_id } - # Simply pass through the result - no wrapping, no unwrapping - debug_log(f"[tools/call] Returning result: {json.dumps(response.get('result', {}))}") return { "jsonrpc": "2.0", "result": response.get("result", {}), @@ -422,21 +311,11 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ async def main(): - log_separator("=") - debug_log("[PROXYSQL MCP STDIO BRIDGE - MAIN STARTING]") - log_separator("=") - # Get configuration from environment endpoint = os.getenv("PROXYSQL_MCP_ENDPOINT", "https://127.0.0.1:6071/mcp/query") token = os.getenv("PROXYSQL_MCP_TOKEN", "") insecure_ssl = os.getenv("PROXYSQL_MCP_INSECURE_SSL", "0").lower() in ("1", "true", "yes") - debug_log(f"[CONFIG] PROXYSQL_MCP_ENDPOINT: {endpoint}") - debug_log(f"[CONFIG] PROXYSQL_MCP_TOKEN: {'***SET***' if token else 'NOT SET'}") - debug_log(f"[CONFIG] PROXYSQL_MCP_INSECURE_SSL: {insecure_ssl}") - debug_log(f"[CONFIG] LOG_FILE: {LOG_FILE}") - log_separator("=") - # Validate endpoint if not endpoint: sys.stderr.write("Error: PROXYSQL_MCP_ENDPOINT environment variable is required\n") @@ -448,11 +327,9 @@ async def main(): try: await server.run() except KeyboardInterrupt: - debug_log("[MAIN] Interrupted by KeyboardInterrupt") + pass except Exception as e: - debug_log(f"[MAIN] ERROR: {e}") - import traceback - traceback.print_exc(file=sys.stderr) + sys.stderr.write(f"Error: {e}\n") sys.exit(1) From 77099f7af2bc8d3e1d3a526290dab93ce428e85c Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:39:33 +0000 Subject: [PATCH 13/17] Debug: Add minimal logging to track stdout writes and tool calls Added _log() calls to track: - stdout writes (bytes and content preview) - tools/call handler (name, response, result) - main startup Log is written to /tmp/proxysql_mcp_bridge.log --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 1da7732381..6505e5fec1 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -187,8 +187,10 @@ async def _writeline(self, data: Any): """Write JSON data to stdout.""" loop = asyncio.get_event_loop() output = json.dumps(data, ensure_ascii=False) + "\n" + _log(f"WRITE stdout: {len(output)} bytes: {repr(output[:200])}") await loop.run_in_executor(None, sys.stdout.write, output) await loop.run_in_executor(None, sys.stdout.flush) + _log(f"WRITE stdout: flushed") async def _write_notification(self, method: str, params: Optional[Dict[str, Any]] = None): """Write a notification (no id).""" @@ -284,6 +286,10 @@ async def _handle_tools_list(self, req_id: str) -> Dict[str, Any]: async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[str, Any]: """Handle tools/call request - forward to ProxySQL.""" + name = params.get("name", "") + arguments = params.get("arguments", {}) + _log(f"tools/call: name={name}, id={req_id}") + if not self._proxysql: return { "jsonrpc": "2.0", @@ -291,10 +297,8 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ "id": req_id } - name = params.get("name", "") - arguments = params.get("arguments", {}) - response = await self._proxysql.tools_call(name, arguments, req_id) + _log(f"tools/call: response from ProxySQL: {json.dumps(response)[:500]}") if "error" in response: return { @@ -303,9 +307,11 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ "id": req_id } + result = response.get("result", {}) + _log(f"tools/call: returning result: {json.dumps(result)[:500]}") return { "jsonrpc": "2.0", - "result": response.get("result", {}), + "result": result, "id": req_id } @@ -316,6 +322,8 @@ async def main(): token = os.getenv("PROXYSQL_MCP_TOKEN", "") insecure_ssl = os.getenv("PROXYSQL_MCP_INSECURE_SSL", "0").lower() in ("1", "true", "yes") + _log(f"START: endpoint={endpoint}, insecure_ssl={insecure_ssl}") + # Validate endpoint if not endpoint: sys.stderr.write("Error: PROXYSQL_MCP_ENDPOINT environment variable is required\n") @@ -325,10 +333,12 @@ async def main(): server = StdioMCPServer(endpoint, token or None, verify_ssl=not insecure_ssl) try: + _log("Starting server.run()") await server.run() except KeyboardInterrupt: - pass + _log("KeyboardInterrupt") except Exception as e: + _log(f"Error: {e}") sys.stderr.write(f"Error: {e}\n") sys.exit(1) From 9b4aea047ad02bd0f52f9dd2dfc2d8e0d2dace1c Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:45:25 +0000 Subject: [PATCH 14/17] Fix: Wrap tools/call responses in MCP-compliant content format Per MCP spec (https://modelcontextprotocol.io/specification/2025-11-25/server/tools): - Tool call responses MUST have a "content" array - Each content item has "type" and "text" fields - Response includes "isError" boolean This was the root cause - Claude Code was waiting for the correct format. --- scripts/mcp/proxysql_mcp_stdio_bridge.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 6505e5fec1..f9090466a9 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -307,11 +307,24 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ "id": req_id } - result = response.get("result", {}) - _log(f"tools/call: returning result: {json.dumps(result)[:500]}") + raw_result = response.get("result", {}) + _log(f"tools/call: raw_result: {json.dumps(raw_result)[:500]}") + + # Wrap result in MCP-compliant format with content array + # Per MCP spec: https://modelcontextprotocol.io/specification/2025-11-25/server/tools + formatted_result = { + "content": [ + { + "type": "text", + "text": json.dumps(raw_result, indent=2) + } + ], + "isError": False + } + _log(f"tools/call: returning formatted: {json.dumps(formatted_result)[:500]}") return { "jsonrpc": "2.0", - "result": result, + "result": formatted_result, "id": req_id } From 49e964bb0242906ab022029fb9afb2f973e13702 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 16:54:47 +0000 Subject: [PATCH 15/17] Fix: Make ProxySQL MCP server return MCP-compliant tool responses The ProxySQL MCP server now wraps tool results in the correct MCP format: - result.content: array of content items (type: "text", text: "...") - result.isError: boolean Per MCP spec: https://modelcontextprotocol.io/specification/2025-11-25/server/tools Also simplified the bridge to pass through results directly since the server now returns the correct format. --- lib/MCP_Endpoint.cpp | 45 ++++++++++++++++-------- scripts/mcp/proxysql_mcp_stdio_bridge.py | 21 +++-------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index 70371e67d0..543c1c53fc 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -348,22 +348,37 @@ json MCP_JSONRPC_Resource::handle_tools_call(const json& req_json) { if (response.is_object() && response.contains("success") && response.contains("result")) { bool success = response["success"].get(); if (!success) { - // Tool execution failed - return error - json error_result; - if (response.contains("error")) { - error_result["error"] = response["error"]; - } else { - error_result["error"] = "Tool execution failed"; - } - if (response.contains("code")) { - error_result["code"] = response["code"]; - } - return error_result; + // Tool execution failed - return error in MCP format + json mcp_result; + mcp_result["content"] = json::array(); + json error_content; + error_content["type"] = "text"; + std::string error_msg = response.contains("error") ? response["error"].get() : "Tool execution failed"; + error_content["text"] = error_msg; + mcp_result["content"].push_back(error_content); + mcp_result["isError"] = true; + return mcp_result; } - // Success - extract and return the actual result - return response["result"]; + // Success - wrap result in MCP-compliant format with content array + // Per MCP spec: https://modelcontextprotocol.io/specification/2025-11-25/server/tools + json actual_result = response["result"]; + json mcp_result; + mcp_result["content"] = json::array(); + json text_content; + text_content["type"] = "text"; + text_content["text"] = actual_result.dump(2); // Pretty-print JSON with 2-space indent + mcp_result["content"].push_back(text_content); + mcp_result["isError"] = false; + return mcp_result; } - // Fallback: return response as-is (for compatibility with non-standard handlers) - return response; + // Fallback: wrap response in MCP format (for compatibility with non-standard handlers) + json mcp_result; + mcp_result["content"] = json::array(); + json text_content; + text_content["type"] = "text"; + text_content["text"] = response.dump(2); + mcp_result["content"].push_back(text_content); + mcp_result["isError"] = false; + return mcp_result; } diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index f9090466a9..8bbe115cea 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -307,24 +307,13 @@ async def _handle_tools_call(self, req_id: str, params: Dict[str, Any]) -> Dict[ "id": req_id } - raw_result = response.get("result", {}) - _log(f"tools/call: raw_result: {json.dumps(raw_result)[:500]}") - - # Wrap result in MCP-compliant format with content array - # Per MCP spec: https://modelcontextprotocol.io/specification/2025-11-25/server/tools - formatted_result = { - "content": [ - { - "type": "text", - "text": json.dumps(raw_result, indent=2) - } - ], - "isError": False - } - _log(f"tools/call: returning formatted: {json.dumps(formatted_result)[:500]}") + # ProxySQL MCP server now returns MCP-compliant format with content array + # Just pass through the result directly + result = response.get("result", {}) + _log(f"tools/call: returning result: {json.dumps(result)[:500]}") return { "jsonrpc": "2.0", - "result": formatted_result, + "result": result, "id": req_id } From 2ceaac049cb5dde499583562dfcdcb0f6210938b Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 17:02:48 +0000 Subject: [PATCH 16/17] docs: Add logging section to bridge README Added documentation for: - Log file location (/tmp/proxysql_mcp_bridge.log) - What information is logged - How to use logs for debugging --- scripts/mcp/STDIO_BRIDGE_README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/mcp/STDIO_BRIDGE_README.md b/scripts/mcp/STDIO_BRIDGE_README.md index f6aff7ee88..935109f2b3 100644 --- a/scripts/mcp/STDIO_BRIDGE_README.md +++ b/scripts/mcp/STDIO_BRIDGE_README.md @@ -107,8 +107,36 @@ Once configured, you can ask Claude: > "Show me 5 rows from the orders table" > "Run SELECT COUNT(*) FROM customers" +## Logging + +For debugging, the bridge writes logs to `/tmp/proxysql_mcp_bridge.log`: + +```bash +tail -f /tmp/proxysql_mcp_bridge.log +``` + +The log shows: +- stdout writes (byte counts and previews) +- tool calls (name, arguments, responses from ProxySQL) +- Any errors or issues + +This can help diagnose communication issues between Claude Code, the bridge, and ProxySQL. + ## Troubleshooting +### Debug Mode + +If tools aren't working, check the bridge log file for detailed information: + +```bash +cat /tmp/proxysql_mcp_bridge.log +``` + +Look for: +- `"tools/call: name=..."` - confirms tool calls are being forwarded +- `"response from ProxySQL:"` - shows what ProxySQL returned +- `"WRITE stdout:"` - confirms responses are being sent to Claude Code + ### Connection Refused Make sure ProxySQL MCP server is running: ```bash From 606fe2e93c72ea53b1978ed37a87a7d25c3f9a20 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 18:44:46 +0000 Subject: [PATCH 17/17] Fix: Address code review feedback from gemini-code-assist Python bridge (scripts/mcp/proxysql_mcp_stdio_bridge.py): - Make log file path configurable via PROXYSQL_MCP_BRIDGE_LOG env var - Add httpx.RequestError exception handling for network issues - Fix asyncio.CancelledError not being re-raised (HIGH priority) - Replace deprecated asyncio.get_event_loop() with get_running_loop() C++ server (lib/MCP_Endpoint.cpp): - Refactor handle_tools_call() to reduce code duplication - Handle string responses directly without calling .dump() - Single shared wrapping block for all response types Per review: https://github.com/ProxySQL/proxysql-vec/pull/11 --- lib/MCP_Endpoint.cpp | 27 +++++++++++------------- scripts/mcp/proxysql_mcp_stdio_bridge.py | 19 ++++++++++++++--- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/lib/MCP_Endpoint.cpp b/lib/MCP_Endpoint.cpp index 543c1c53fc..dd4430d0c7 100644 --- a/lib/MCP_Endpoint.cpp +++ b/lib/MCP_Endpoint.cpp @@ -359,26 +359,23 @@ json MCP_JSONRPC_Resource::handle_tools_call(const json& req_json) { mcp_result["isError"] = true; return mcp_result; } - // Success - wrap result in MCP-compliant format with content array - // Per MCP spec: https://modelcontextprotocol.io/specification/2025-11-25/server/tools - json actual_result = response["result"]; - json mcp_result; - mcp_result["content"] = json::array(); - json text_content; - text_content["type"] = "text"; - text_content["text"] = actual_result.dump(2); // Pretty-print JSON with 2-space indent - mcp_result["content"].push_back(text_content); - mcp_result["isError"] = false; - return mcp_result; + // Success - use the "result" field as the content to be wrapped + response = response["result"]; } - // Fallback: wrap response in MCP format (for compatibility with non-standard handlers) + // Wrap the response (or the 'result' field) in MCP-compliant format + // Per MCP spec: https://modelcontextprotocol.io/specification/2025-11-25/server/tools json mcp_result; - mcp_result["content"] = json::array(); json text_content; text_content["type"] = "text"; - text_content["text"] = response.dump(2); - mcp_result["content"].push_back(text_content); + + if (response.is_string()) { + text_content["text"] = response.get(); + } else { + text_content["text"] = response.dump(2); // Pretty-print JSON with 2-space indent + } + + mcp_result["content"] = json::array({text_content}); mcp_result["isError"] = false; return mcp_result; } diff --git a/scripts/mcp/proxysql_mcp_stdio_bridge.py b/scripts/mcp/proxysql_mcp_stdio_bridge.py index 8bbe115cea..859b778b28 100755 --- a/scripts/mcp/proxysql_mcp_stdio_bridge.py +++ b/scripts/mcp/proxysql_mcp_stdio_bridge.py @@ -34,7 +34,9 @@ import httpx # Minimal logging to file for debugging -_log_file = open("/tmp/proxysql_mcp_bridge.log", "a", buffering=1) +# Log path can be configured via PROXYSQL_MCP_BRIDGE_LOG environment variable +_log_file_path = os.getenv("PROXYSQL_MCP_BRIDGE_LOG", "/tmp/proxysql_mcp_bridge.log") +_log_file = open(_log_file_path, "a", buffering=1) def _log(msg): _log_file.write(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] {msg}\n") _log_file.flush() @@ -105,6 +107,15 @@ async def _call(self, request: Dict[str, Any]) -> Dict[str, Any]: }, "id": request.get("id", "") } + except httpx.RequestError as e: + return { + "jsonrpc": "2.0", + "error": { + "code": -32002, + "message": f"Request to ProxySQL failed: {e}" + }, + "id": request.get("id", "") + } except Exception as e: return { "jsonrpc": "2.0", @@ -172,12 +183,14 @@ async def run(self): except json.JSONDecodeError as e: await self._write_error(-32700, f"Parse error: {e}", "") + except asyncio.CancelledError: + raise # Re-raise to allow proper task cancellation except Exception as e: await self._write_error(-32603, f"Internal error: {e}", "") async def _readline(self) -> Optional[str]: """Read a line from stdin.""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() line = await loop.run_in_executor(None, sys.stdin.readline) if not line: return None @@ -185,7 +198,7 @@ async def _readline(self) -> Optional[str]: async def _writeline(self, data: Any): """Write JSON data to stdout.""" - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() output = json.dumps(data, ensure_ascii=False) + "\n" _log(f"WRITE stdout: {len(output)} bytes: {repr(output[:200])}") await loop.run_in_executor(None, sys.stdout.write, output)