- Python 63.2%
- Dockerfile 36.8%
Packages mukul975/cve-mcp-server as a streamable-HTTP MCP server, fixing an upstream DNS-rebinding bug that otherwise returns 421 to any non-localhost Host header. A Forgejo workflow polls GitHub daily for new stable releases and builds, Trivy-scans, and pushes a matching image automatically. |
||
|---|---|---|
| .forgejo/workflows | ||
| docker | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
cve-mcp-server
Docker packaging and Forgejo CI for running mukul975/cve-mcp-server as an HTTP-accessible MCP server. Upstream is a stdio MCP server (Python, MIT licensed) that exposes 28 CVE/vulnerability-intelligence tools over NVD, EPSS, CISA KEV, OSV, VulnCheck, Shodan, VirusTotal, AbuseIPDB, GreyNoise, urlscan and other sources.
This repository does not vendor upstream source. The .forgejo/workflows/build-upstream.yml workflow polls GitHub daily for new upstream releases, clones the release tag at build time, and builds/scans/pushes an image to code.paulg.it/paulgit/cve-mcp-server.
Quick start
docker run --rm -p 8000:8000 \
--env-file .env \
-v cve-mcp-data:/home/app/.cve-mcp \
code.paulg.it/paulgit/cve-mcp-server:latest
Or with Compose (see docker-compose.yml):
docker compose up
The MCP endpoint is http://<host>:8000/mcp (streamable HTTP, stateless by default). Example remote MCP client configuration:
{
"mcpServers": {
"cve-mcp": {
"type": "http",
"url": "http://localhost:8000/mcp"
}
}
}
The upstream 421 bug, and how this image fixes it
Upstream constructs its FastMCP server with the SDK's default host="127.0.0.1". That causes the mcp Python SDK to auto-enable DNS-rebinding protection restricted to localhost/127.0.0.1/[::1], and upstream's own main() never overrides it. The practical effect: any request whose Host header isn't localhost (a Docker service name, a reverse-proxy hostname) is rejected with 421 Misdirected Request, even though the container's own healthcheck (which probes localhost) reports healthy throughout.
This repo's entrypoint, docker/serve.py, imports upstream's mcp instance directly and overrides mcp.settings.transport_security from the environment before serving, so the container works correctly behind Docker networking or a reverse proxy out of the box. See the ALLOWED_HOSTS / ALLOWED_ORIGINS variables below if you want the Host-header allowlist enforced instead.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
MCP_TRANSPORT |
http (image default) |
http/streamable-http serves MCP over HTTP at /mcp; anything else falls back to stdio |
HOST |
0.0.0.0 |
Bind address |
PORT |
8000 |
Bind port |
MCP_STATELESS_HTTP |
true |
Matches upstream's stateless behaviour: the whole server lifespan (new httpx client, a live NVD validation ping, a SQLite cache reopen) re-runs on every request. Set false for busy deployments to keep a persistent session and avoid that per-request cost. |
ALLOWED_HOSTS |
empty (protection off) | Comma-separated Host-header allowlist, e.g. cve-mcp.example.com:*,localhost:*. Empty disables DNS-rebinding protection entirely. |
ALLOWED_ORIGINS |
empty | Comma-separated Origin allowlist; only used when ALLOWED_HOSTS is set. |
CACHE_DB_PATH |
~/.cve-mcp/cache.db |
SQLite TTL cache path (/home/app/.cve-mcp/cache.db in the container) |
AUDIT_LOG_PATH |
~/.cve-mcp/audit.log |
JSONL audit log (rotates at 50MB, keeps 5 backups) |
REQUEST_TIMEOUT |
30 |
HTTP request timeout (seconds) for upstream API calls |
MAX_RETRIES |
3 |
Retries on transient network errors |
Plus the threat-intelligence API keys (NVD_API_KEY, GITHUB_TOKEN, VULNCHECK_TOKEN, ABUSEIPDB_KEY, VIRUSTOTAL_KEY, URLSCAN_KEY, SHODAN_KEY, GREYNOISE_API_KEY, CIRCL_PDNS_USER, CIRCL_PDNS_PASS) — all optional, see .env.example for details and sign-up links.
Note: startup makes outbound HTTPS calls (an NVD validation ping, the CISA KEV feed download). Failures are logged, not fatal.
CI: automatic release builds
.forgejo/workflows/build-upstream.yml runs daily (05:23 UTC) plus on manual dispatch:
- Resolves the latest stable (non-prerelease, non-draft) upstream GitHub release.
- Skips the build if
code.paulg.it/paulgit/cve-mcp-server:<tag>already exists in the registry (no-op, quiet — no notification sent). - Otherwise clones upstream at that tag, builds this repo's
Dockerfileagainst it, Trivy-scans the exact image (--severity HIGH,CRITICAL --exit-code 1, blocking the push on any unfixed high/critical vulnerability), and pushes:<tag>plus:latest. - Notifies via Pushover on every non-no-op run (success or failure).
To force a rebuild of a specific release: Actions → Build Upstream Release → Run workflow, set tag (e.g. v0.2.0) and force: true.
Required Forgejo secrets: REGISTRY_USER, REGISTRY_TOKEN, PUSHOVER_APP_TOKEN, PUSHOVER_USER_KEY. Optional: UPSTREAM_GITHUB_TOKEN (only needed as a fallback if the runner's IP hits GitHub's unauthenticated rate limit; a daily poll is well within the 60/hour limit).
Local build and test
git clone --depth 1 --branch v0.2.0 https://github.com/mukul975/cve-mcp-server.git /tmp/upstream-v0.2.0
mkdir -p /tmp/upstream-v0.2.0/docker && cp docker/serve.py /tmp/upstream-v0.2.0/docker/
docker build -f Dockerfile -t cve-mcp-server:local /tmp/upstream-v0.2.0
docker run --rm -p 8000:8000 cve-mcp-server:local
Prove the 421 fix — a non-localhost Host header must return 200, not 421:
curl -i -X POST http://127.0.0.1:8000/mcp \
-H "Host: cve-mcp.internal.example" \
-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":"curl","version":"0"}}}'
Rerun with -e ALLOWED_HOSTS="only.example:*" to confirm the same request now correctly returns 421, proving the toggle works both ways.