Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
01c182c
Add stdio MCP bridge for Claude Code integration
renecannao Jan 13, 2026
4491f3c
Add debug logging to MCP bridge for troubleshooting
renecannao Jan 13, 2026
fc6b462
Fix: unwrap ProxySQL nested response format
renecannao Jan 13, 2026
6d83ff1
Fix: unwrap ProxySQL response format in MCP tools and fix config syntax
renecannao Jan 13, 2026
edac8eb
Fix: Add verbose logging and fix stdout buffering issue in MCP stdio …
renecannao Jan 13, 2026
f560698
Fix: Replace stdout with truly unbuffered wrapper to prevent response…
renecannao Jan 13, 2026
55dd5ba
Debug: Add detailed stdout write logging to troubleshoot Claude Code …
renecannao Jan 13, 2026
2b51346
Fix: Wrap tool results in TextContent format for MCP protocol compliance
renecannao Jan 13, 2026
ad54f92
Revert: Simplify tool handlers back to original pass-through
renecannao Jan 13, 2026
f4a4af8
Fix: Write directly to stdout.buffer to bypass TextIOWrapper issues
renecannao Jan 13, 2026
23e5efc
Test: Don't redirect sys.stderr, write logs directly to file
renecannao Jan 13, 2026
a47567f
Revert: Restore original bridge completely
renecannao Jan 13, 2026
77099f7
Debug: Add minimal logging to track stdout writes and tool calls
renecannao Jan 13, 2026
9b4aea0
Fix: Wrap tools/call responses in MCP-compliant content format
renecannao Jan 13, 2026
49e964b
Fix: Make ProxySQL MCP server return MCP-compliant tool responses
renecannao Jan 13, 2026
2ceaac0
docs: Add logging section to bridge README
renecannao Jan 13, 2026
606fe2e
Fix: Address code review feedback from gemini-code-assist
renecannao Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion lib/MCP_Endpoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,5 +341,41 @@ 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<bool>();
if (!success) {
// 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<std::string>() : "Tool execution failed";
error_content["text"] = error_msg;
mcp_result["content"].push_back(error_content);
mcp_result["isError"] = true;
return mcp_result;
}
// Success - use the "result" field as the content to be wrapped
response = response["result"];
}

// 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;
json text_content;
text_content["type"] = "text";

if (response.is_string()) {
text_content["text"] = response.get<std::string>();
} 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;
Comment on lines +348 to +380

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for handling successful tool executions and the fallback case contains duplicated code for wrapping the response in the MCP format. This can be refactored to reduce redundancy and improve maintainability.

Additionally, if the tool's result is already a string, calling .dump() on it will add extra quotes, which is likely not the desired behavior. It's better to check if the result is a string and use its value directly in that case.

The suggested refactoring addresses both of these points by first determining the content to be wrapped and then performing the wrapping in a single, shared block of code.

	if (response.is_object() && response.contains("success") && response.contains("result")) {
		bool success = response["success"].get<bool>();
		if (!success) {
			// 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<std::string>() : "Tool execution failed";
			error_content["text"] = error_msg;
			mcp_result["content"].push_back(error_content);
			mcp_result["isError"] = true;
			return mcp_result;
		}
		// Success - use the "result" field as the content to be wrapped
		response = response["result"];
	}

	// 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;
	json text_content;
	text_content["type"] = "text";

	if (response.is_string()) {
		text_content["text"] = response.get<std::string>();
	} 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;

}
162 changes: 162 additions & 0 deletions scripts/mcp/STDIO_BRIDGE_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# 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"

## 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
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
Loading