Run the MCP Gateway#

Scope: advanced template only. The gateway (tools/mcp-gateway.py) is generated by pgmi init --template advanced and becomes application code you own. New to this subsystem? Start with the overview .

Quick Start#

1. Deploy the Advanced Template#

pgmi init --template advanced myproject
cd myproject
pgmi deploy --connection "postgresql://user:pass@localhost:5432/mydb"

2. Start the HTTP Gateway#

The pgmi init --template advanced command generates a tools/ directory with the Python gateway:

cd tools
pip install -r requirements.txt
export DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"
python mcp-gateway.py

Requirements:

  • Python 3.8+
  • psycopg[binary]>=3.0 (psycopg 3) — the only dependency, pinned in tools/requirements.txt

The gateway uses the Python standard library’s http.server; it does not use Flask, gunicorn, or psycopg2.

3. Test the Connection#

# Initialize handshake
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocolVersion":"2024-11-05"}}'

# List available tools
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"2","method":"tools/list"}'

# Call a tool (register your own first — see MCP-HANDLERS.md)
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"your_tool","arguments":{}}}'

4. Configure Your AI Client#

For Claude Desktop, add to ~/.config/claude/claude_desktop_config.json:

{
  "mcpServers": {
    "my-database": {
      "url": "http://localhost:8080/mcp"
    }
  }
}

Endpoints#

EndpointMethodPurpose
/mcpPOSTMCP JSON-RPC endpoint. Honors Accept (responds application/json; no SSE), validates and echoes the MCP-Protocol-Version header
/mcpGET405 Method Not Allowed — the gateway offers no server-initiated SSE stream
/healthGETHealth check (for load balancers)

Transport-level behavior (version negotiation, Accept handling, what is intentionally not implemented) is specified on the protocol page .

Authentication Headers#

The gateway extracts authentication from HTTP headers:

HeaderMaps to
X-User-Idcontext.user_id
X-Tenant-Idcontext.tenant_id

How handlers consume that context — and how to require it — is covered in Author MCP handlers .

Transaction Policy#

A handler may declare a minimum transaction isolation level with the minTransactionIsolation metadata key (read committed | repeatable read | serializable; case- and separator-insensitive) and a read-only policy with readOnly: true.

The bundled HTTP gateway resolves the policy before opening the dispatch transaction (api.mcp_request_policy(request, requested_level) on a short autocommit connection) and opens with the resolved characteristics: max(route floor, client-requested) isolation, READ ONLY when the route declares it, and DEFERRABLE for SERIALIZABLE READ ONLY (which can never abort with 40001 and needs no retries). Routes therefore just work for callers that send nothing; the X-PGMI-Transaction-Isolation header is escalation, not obligation — an unsupported value is rejected client-side with HTTP 400 (pgmi.transaction_isolation_unsupported).

The database gateway can only validate the characteristics — SET TRANSACTION is illegal inside functions — so its checks remain as the fail-closed invariant for clients that skip the lookup: a too-weak transaction is rejected with a -32600 error carrying data.code = 'pgmi.transaction_isolation_too_weak' (or pgmi.transaction_read_only_required for a read-only route dispatched in a read-write transaction). See lib/api/00-transaction-isolation.sql for the full contract.

Production Deployment#

The shipped gateway is a single-process http.server reference implementation. For production, consider:

  1. Reverse Proxy: Place behind nginx/Caddy that validates JWTs and injects X-User-Id
  2. Connection Pooling: Use PgBouncer for connection management
  3. TLS: Terminate SSL at the load balancer
  4. Scaling: run multiple gateway processes behind the proxy, or adapt the handler to your own ASGI/WSGI server — the gateway exposes no WSGI app object, so it cannot be served with gunicorn as-is.

Server Configuration#

Configure server identity via session settings:

SET mcp.server_name = 'my-database-server';
SET mcp.server_version = '2.0.0';

Or set in postgresql.conf for persistence:

mcp.server_name = 'production-db'
mcp.server_version = '1.0.0'

Troubleshooting#

Common Issues#

“Method not found” error

  • Check that your handler is registered: SELECT * FROM api.mcp_route;
  • Verify the handler type matches the method (tool for tools/call, etc.)

“Authentication required” error

  • Pass context with user_id: '{"user_id":"test|123"}'::jsonb
  • Or set requiresAuth: false for public tools

Handler not appearing in discovery

  • Check the handler was created: SELECT * FROM api.handler WHERE handler_type IN ('mcp_tool', 'mcp_resource', 'mcp_prompt');
  • Verify MCP route exists: SELECT * FROM api.mcp_route;

Debugging#

Enable debug logging:

SET client_min_messages = DEBUG;
SELECT (api.mcp_handle_request('{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"my_tool","arguments":{}}}'::jsonb)).envelope;

Check exchange logs:

SELECT * FROM api.mcp_exchange ORDER BY enqueued_at DESC LIMIT 10;

See Also#