Quickstart

Get mcpgw running and see your first traced MCP tool call in under 10 minutes.

Prerequisites

  • Docker installed and running
  • A Datadog Agent reachable from the Docker host with OTLP/HTTP receiver enabled on port 4318
    • Enable it by setting DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT=0.0.0.0:4318 on the agent (or the equivalent in datadog.yaml)
  • A license JWT obtained from mcpgw.dev — required at startup; the gateway fails closed without one

1. Pull the image

docker pull ghcr.io/seanfraserio/mcpgw:latest

2. Place your license JWT

echo "<paste-your-jwt-here>" > ./license.jwt
chmod 0600 ./license.jwt

The file must be readable only by the process owner. mcpgw will refuse to start if it cannot read a valid license.


3. Create mcpgw.yaml

Copy configs/example.yaml from the repo and edit it for your environment. Minimum required changes:

  • upstreams: point each entry at your actual MCP server(s)
  • telemetry.customer.endpoint: set to http://<your-datadog-agent-host>:4318

Example (replace http://mcp-fs:9000 and the endpoint with real values):

listen: 0.0.0.0:7332
license:
  path: /etc/mcpgw/license.jwt

# Catches initialize, tools/list, ping, resources/list — MCP messages
# without a tool name. Required for spec-compliant clients.
default_upstream: filesystem

upstreams:
  - name: filesystem
    url: http://mcp-fs:9000

routes:
  - match: { tool_prefix: "fs_" }
    upstream: filesystem

policy:
  rules: []

telemetry:
  customer:
    enabled: true
    # Bare host:port is fine — mcpgw appends /v1/traces automatically.
    # Override the path explicitly only for OTLP-compatible proxies that
    # mount the receiver under a non-default path.
    endpoint: http://192.168.1.10:4318   # your Datadog Agent
    service_name: mcpgw

audit:
  path: /var/log/mcpgw/audit.jsonl
  max_size_mb: 100
  compress_rotated: true

4. Run

docker run --rm -p 7332:7332 \
  -v $PWD/license.jwt:/etc/mcpgw/license.jwt:ro \
  -v $PWD/mcpgw.yaml:/etc/mcpgw/mcpgw.yaml:ro \
  -v $PWD/audit.jsonl:/var/log/mcpgw/audit.jsonl \
  ghcr.io/seanfraserio/mcpgw:latest

Verify the gateway is healthy:

curl -s http://localhost:7332/healthz   # 200 OK
curl -s http://localhost:7332/readyz    # 200 OK (503 means license expired beyond grace)

5. Point your MCP client

Network clients (anything that accepts an HTTP MCP URL):

Set the MCP server URL to http://localhost:7332/mcp.

stdio clients (Claude Desktop, Cursor, and similar tools that spawn a subprocess):

Use the mcpgw stdio bridge. It reads/writes the MCP stdio protocol on stdin/stdout and forwards to the gateway over HTTP. Install the binary from the releases page and add it to your PATH.

Claude Desktop — add to claude_desktop_config.json:

{
  "mcpServers": {
    "filesystem-via-mcpgw": {
      "command": "mcpgw",
      "args": ["stdio", "--upstream=http://localhost:7332"]
    }
  }
}

The mcpgw stdio command accepts --upstream=<url> where the URL is your gateway’s /mcp endpoint. The /mcp path suffix is added automatically by the stdio bridge; pass the base URL only (http://localhost:7332, not http://localhost:7332/mcp).

Cursor — equivalent pattern: add an MCP server entry with command mcpgw and args ["stdio", "--upstream=http://localhost:7332"].


6. Make a tool call

Trigger any tool call through the gateway (e.g. list a directory via the filesystem server). Then open Datadog APM and verify:

  • A span named mcp.tools.call appears
  • It carries the attribute mcp.tool.name with the tool’s name
  • mcp.policy.decision is allow

Deployment notes

Rate-limit identity (no XFF trust)

mcpgw derives the rate-limit bucket key from RemoteAddr only — the host portion of the TCP peer. X-Forwarded-For and Forwarded headers are intentionally ignored. This is the safe default: a malicious client cannot spoof headers to evade or amplify rate limits.

Operational consequence: if you front mcpgw with a reverse proxy or load balancer that terminates TLS, every request appears to come from the LB’s IP and all clients share one bucket. Two supported topologies:

  • mcpgw is the TLS edge (recommended). Configure tls.cert_file and tls.key_file so mcpgw terminates TLS directly on :7332. RemoteAddr is the real client. Cert/key are re-read on SIGHUP, so renewals are zero-downtime. mTLS is available via tls.client_ca. See Configuration: tls.
  • PROXY protocol front. Put HAProxy or NGINX in PROXY-protocol mode in front; native PROXY-protocol parsing is on the roadmap.

Header-based forwarding (X-Forwarded-For) is not supported as a rate-limit input by design. A trusted_proxies allowlist is on the roadmap for operators with mature LB configs.

Going to production

After the basic loop above, the most common production-readiness steps:


Troubleshooting

SymptomLikely causeFix
GET /readyz returns 503License expired beyond its grace periodRenew license at mcpgw.dev and replace license.jwt
POST /mcp returns 504Upstream MCP server unreachableCheck the url values under upstreams in your config; confirm the upstream is running and reachable from inside the container
No spans appear in DatadogOTLP receiver not enabled, or wrong endpointConfirm DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT is set on the Datadog Agent; double-check telemetry.customer.endpoint in mcpgw.yaml
POST /mcp returns 403Policy denied the requestInspect audit.jsonl for lines with "decision":"deny" and note the rule_id; adjust policy rules accordingly

For structured audit entries, tail the log while making calls:

tail -f audit.jsonl | jq .