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:4318on the agent (or the equivalent indatadog.yaml)
- Enable it by setting
- 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 tohttp://<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.callappears - It carries the attribute
mcp.tool.namewith the tool’s name mcp.policy.decisionisallow
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_fileandtls.key_fileso mcpgw terminates TLS directly on:7332.RemoteAddris the real client. Cert/key are re-read onSIGHUP, so renewals are zero-downtime. mTLS is available viatls.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:
- Inbound auth. Generate keys with
mcpgw key generateand enableauth.enabled: true. See How-to: enable API-key authentication. - Allowlist mode. Set
policy.default_action: denyand enumerate 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_limitto 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 license at mcpgw.dev 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 "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 .