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:4318on the agent (or the equivalent indatadog.yaml)
- Enable it by setting
- 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 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:
# 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.callappears - It carries the attribute
mcp.tool.namewith the tool’s name mcp.policy.decisionisallow
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 generateand enableauth.enabled: true. See How-to: enable API-key authentication. - Allowlist mode. Replace the quickstart’s explicit
policy.default_action: allowwithdenyand 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_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 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 .