Quickstart

Get mcpgw running and see your first traced MCP tool call.

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 the Community license page — 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:
  # v0.7.0+ defaults to deny when this key is omitted. The quickstart uses an
  # explicit allow default so the first test call succeeds. Change this to
  # deny and add narrow allow rules before production use.
  default_action: allow
  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 public 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 behind a proxy

By default mcpgw derives IP rate-limit buckets from RemoteAddr and ignores forwarding headers. If an ingress or load balancer terminates the connection, configure its narrow source CIDR so clients do not collapse into one shared bucket:

trusted_proxies:
  - 10.42.0.0/16

mcpgw then resolves the first untrusted address by walking RFC Forwarded (preferred) or X-Forwarded-For right-to-left. Headers remain ignored when the immediate TCP peer is not trusted. Restrict direct gateway access with firewall/security-group rules and never use a broader CIDR than the actual proxy network. See Rate-limit identity and Configuration: trusted_proxies.

Going to production

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

  • Inbound auth. Generate keys with mcpgw key generate and enable auth.enabled: true. See How-to: enable API-key authentication.
  • Allowlist mode. Replace the quickstart’s explicit policy.default_action: allow with deny and enumerate narrow allow rules. See How-to: enable default-deny.
  • Durable audit. Add an audit.sinks[] entry shipping to S3 (with Object Lock for tamper-evidence), GCS, Kafka, or your SIEM via webhook. See How-to: ship audit to S3, GCS, Kafka, or a SIEM webhook.
  • Flood protection. Configure input_rate_limit to cap per-IP request volume before parse and auth.

Troubleshooting

Symptom Likely cause Fix
GET /readyz returns 503 License expired beyond its grace period Renew the license at rivano.ai and replace license.jwt
POST /mcp returns 504 Upstream MCP server unreachable Check the url values under upstreams in your config; confirm the upstream is running and reachable from inside the container
No spans appear in Datadog OTLP receiver not enabled, or wrong endpoint Confirm DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT is set on the Datadog Agent; double-check telemetry.customer.endpoint in mcpgw.yaml
POST /mcp returns 403 Policy denied the request Inspect audit.jsonl for lines with "action":"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 .