Skip to content

HTTP Transport

Chris & Mike edited this page Mar 12, 2026 · 6 revisions

HTTP Transport

postgres-mcp supports HTTP transport for remote access, web-based clients, and multi-client scenarios. Both modern (Streamable HTTP) and legacy (SSE) MCP protocols are served simultaneously from a single server instance.


When to Use HTTP Transport

Scenario Transport
IDE integration (Cursor, Claude Desktop, AntiGravity) --transport stdio (default)
Remote/networked access --transport http
Web-based MCP clients --transport http
Multiple concurrent clients --transport http
Serverless/stateless deployments --transport http --stateless
Python mcp.client.sse SDK --transport http (legacy SSE)

Starting the Server

CLI

node dist/cli.js \
  --transport http \
  --port 3000 \
  --postgres "postgres://user:pass@localhost:5432/db"

Docker

docker run --rm -p 3000:3000 \
  -e POSTGRES_URL=postgres://user:pass@host:5432/db \
  writenotenow/postgres-mcp:latest \
  --transport http --port 3000

With Simple Bearer Auth

docker run --rm -p 3000:3000 \
  -e POSTGRES_URL=postgres://user:pass@host:5432/db \
  -e MCP_AUTH_TOKEN=my-secret-token \
  writenotenow/postgres-mcp:latest \
  --transport http --port 3000

Stateless Mode

node dist/cli.js --transport http --port 3000 --stateless --postgres "postgres://..."

Docker Note: Use host.docker.internal to connect to PostgreSQL running on your host machine.


Dual Protocol Support

The server exposes two MCP transport protocols simultaneously on the same port:

Protocol Spec Version Use Case
Streamable HTTP MCP 2025-03-26 Modern clients, recommended
Legacy SSE MCP 2024-11-05 Backward compatibility (e.g., Python mcp.client.sse)

Both protocols share the same MCP server instance, tool registrations, and database connection pool.


Streamable HTTP (Recommended)

Single-endpoint protocol using the Mcp-Session-Id header for session management.

Endpoints

Method Endpoint Purpose
POST /mcp JSON-RPC requests (initialize, tools/list, tools/call, etc.)
GET /mcp SSE stream for server-initiated notifications
DELETE /mcp Session termination

In stateless mode (--stateless): GET /mcp returns 405, DELETE /mcp returns 204, /sse and /messages return 404. Each POST /mcp creates a fresh transport with no session persistence.

Session Lifecycle

  1. InitializePOST /mcp with initialize request (no session header)
  2. Receive Session ID — Server responds with Mcp-Session-Id header
  3. Send NotificationPOST /mcp with notifications/initialized (include session header)
  4. Use ToolsPOST /mcp with tools/list, tools/call, etc. (include session header)
  5. TerminateDELETE /mcp (include session header)

Example: Initialize

curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-03-26",
      "capabilities": {},
      "clientInfo": {"name": "my-client", "version": "1.0"}
    }
  }'

Legacy SSE (Backward Compatibility)

Two-endpoint protocol using Server-Sent Events for server-to-client communication.

Endpoints

Method Endpoint Purpose
GET /sse Opens SSE stream, returns /messages?sessionId=<id> endpoint event
POST /messages?sessionId=<id> Send JSON-RPC messages to the session

Session Lifecycle

  1. Open SSE StreamGET /sse → Server sends an endpoint event with the POST URL
  2. Parse Endpoint — Extract /messages?sessionId=<id> from the SSE event data
  3. InitializePOST /messages?sessionId=<id> with initialize request
  4. Read Response — Server pushes the response through the SSE stream
  5. Use Tools — Continue sending JSON-RPC requests via POST, reading responses from SSE

Example: Connect with Python

from mcp import ClientSession
from mcp.client.sse import sse_client

async with sse_client("http://localhost:3000/sse") as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await session.list_tools()
        print(f"Connected! {len(tools.tools)} tools available")

Health Endpoint

Method Endpoint Purpose
GET /health Health check (bypasses rate limiting, always available for monitoring)
curl http://localhost:3000/health
{
  "status": "healthy",
  "database": "connected",
  "version": "2.0.0"
}

Docker HEALTHCHECK: When running with --transport http, the Docker image uses native node -e "fetch(...)" to verify the /health endpoint (confirming both HTTP server and database connectivity). In stdio mode, it falls back to a Node.js process check.


Configuration

Environment Variables

Variable Default Description
MCP_HOST localhost Server bind host (0.0.0.0 for containers)
MCP_PORT 3000 Server listen port
LOG_LEVEL info Log verbosity (debug, info, warning, error)

Security Options

Feature Default Description
Rate limiting Enabled Per-IP request throttling with Retry-After header on 429
Body size limit 1 MB Maximum request body size
CORS Enabled Cross-origin request headers (supports wildcard subdomains)
HSTS Disabled HTTP Strict Transport Security (opt-in via enableHSTS)
Trust proxy Disabled X-Forwarded-For client IP extraction (opt-in via trustProxy)
Server timeouts Enabled Request (120s), keep-alive (65s), and headers (66s) timeouts
Security headers Enabled 7 headers: X-Content-Type-Options, X-Frame-Options, CSP, Cache-Control, Referrer-Policy (no-referrer), Permissions-Policy, opt-in HSTS
Health check bypass Enabled /health always serves regardless of rate-limit state

See OAuth-and-Security for OAuth 2.1 authentication configuration.

Testing & Coverage

The HTTP transport is verified through a dual-testing model to ensure complete protocol compliance without sacrificing test stability:

  1. Unit Testing (Vitest): Core adapter logic, input validation, schemas, and routing are rigorously unit-tested, generating the repository's primary LCOV coverage metrics (>90%).
  2. E2E Validation (Playwright): End-to-end integration is verified using Playwright. This suite launches a standalone dist/cli.js background process and uses the official @modelcontextprotocol/sdk to connect to it acting as a live client.

Because the E2E server runs in an isolated Node process, its execution paths do not artificially inflate the Vitest line coverage numbers. However, the E2E suite provides absolute pass/fail functional guarantees over the server's network boundaries, including:

  • CORS and security header injection (7 headers)
  • Body parsing limit enforcement (413 Payload Too Large)
  • Streamable HTTP (2025-03-26) session tracking limits
  • Legacy SSE (2024-11-05) URL parameter session validation
  • Rate limiting with Retry-After and health-check bypass
  • HSTS opt-in behavior
  • Referrer-Policy enforcement (no-referrer)
  • trustProxy X-Forwarded-For handling
  • Live tool dispatching through the active socket
  • Payload contract validation (response shapes for all 22 tool groups)
  • Bearer token authentication (--auth-token) with public path exemptions
  • Stateless mode (--stateless) endpoint restrictions

This yields an effective ~100% functional coverage safety net for the HTTP transport layer in real-world conditions.


Related

Clone this wiki locally