From 01c182ccac1ef03e759fd6574e4d31147184a1e0 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 13 Jan 2026 11:05:54 +0000 Subject: [PATCH] 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 000000000..f6aff7ee8 --- /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 000000000..24d901554 --- /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())