mirror of https://github.com/sysown/proxysql
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 authpull/5310/head
parent
9d6a2173bf
commit
01c182ccac
@ -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
|
||||
@ -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())
|
||||
Loading…
Reference in new issue