Docker packaging and Forgejo CI for mukul975/cve-mcp-server — an MCP server exposing CVE/vulnerability-intelligence tools (NVD, EPSS, CISA KEV, OSV and more) over streamable HTTP. A daily-polling workflow detects new upstream GitHub releases and builds, Trivy-scans and pushes a matching image automatically.
  • Python 63.2%
  • Dockerfile 36.8%
Find a file
Paul Git aaaacbe7a1 Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server
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.
2026-07-30 21:41:29 +08:00
.forgejo/workflows Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00
docker Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00
.env.example Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00
.gitignore Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00
AGENTS.md Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00
docker-compose.yml Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00
Dockerfile Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00
README.md Add Dockerised HTTP MCP packaging and Forgejo auto-build for cve-mcp-server 2026-07-30 21:41:29 +08:00

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 Dockerfile against 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.