Skip to content

Hosted HTTP deployment

A hosted edi-mcp lets many users connect with claude mcp add --transport http edi https://edi-mcp.example.org/mcp. The HTTP transport is stateless Streamable HTTP1 at /mcp: no session header, no server-initiated stream, so any instance can answer any request and instances can sit behind a plain load balancer.

edi-mcp --transport http --bind-address 127.0.0.1 --bind-port 8080

POST /mcp takes JSON-RPC requests; GET and DELETE on /mcp return 405 with Allow: POST. /mcp/ is served directly rather than redirected. Every response carries an x-request-id header (the caller's own value when it is a short safe token, otherwise a generated one) that also appears in the JSON logs.

Configuration essentials

EDI_MCP_SERVER__TRANSPORT=http
EDI_MCP_SERVER__BIND_ADDRESS=127.0.0.1          # the proxy connects locally
EDI_MCP_SERVER__BIND_PORT=8080
EDI_MCP_SERVER__PUBLIC_BASE_URL=https://edi-mcp.example.org

public_base_url does two things:

  • its host and origin join the Host and Origin allow-lists (server.allowed_hosts, server.allowed_origins) used for DNS-rebinding protection. Add any other names the service answers on to those lists; server.dns_rebinding_protection forces the check on or off (unset leaves the SDK default, which is on for loopback binds or when allow-lists are set);
  • when it starts with https://, it opens the TLS gate for per-caller keys (below).

Other HTTP settings: server.json_response answers POST /mcp with application/json instead of a single SSE frame, and server.max_request_body_size (default 1 MiB) caps request bodies. All settings are listed on the Configuration page.

EDI API keys over HTTP

Each caller brings their own EDI API key in the X-EDI-Key header (the name is server.request_key_header). The header is honoured only when public_base_url is https://; otherwise the middleware strips it before any handler sees it and logs one warning. server.allow_insecure_header_key overrides the gate for local development only.

A call with a key is served by PASTA; a call without one is served anonymously from DataONE, as on stdio (see Backends). The operator's own key (EDI_API_KEY in the server's environment) is not lent to HTTP callers unless server.share_config_key_over_http is true; enable that only on a private deployment where every caller may act as the operator. The MCP-level Authorization header is never forwarded to EDI. See Security for the full key-handling rules.

Behind a reverse proxy

Terminate TLS at the proxy (the key is a bearer secret), forward Host unchanged, and pass X-EDI-Key through. uvicorn runs with proxy headers enabled2. Caddy3:

edi-mcp.example.org {
    reverse_proxy 127.0.0.1:8080
}

nginx:

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_buffering off;          # single SSE frame per response
}

systemd

[Unit]
Description=edi-mcp (Streamable HTTP)
After=network-online.target

[Service]
User=edi
EnvironmentFile=/etc/edi-mcp.env
ExecStart=/opt/edi-mcp/.venv/bin/edi-mcp --transport http
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict

[Install]
WantedBy=multi-user.target

Keep /etc/edi-mcp.env readable by the service user only if it holds an operator key.

Container image

A Dockerfile (Python slim base, non-root user, health check on /healthz) is planned for the v0.1.0 release and is not in the repository yet. Until then, install the package into a virtual environment as above.

Health and readiness

  • GET /healthz is liveness only: always 200 with status, version, protocolVersion and transport; it contacts nothing upstream.
  • GET /readyz returns 503 with status: warming while startup work runs, then 200 with status: ready, degraded and keyConfigured (whether an operator key is set; the key itself is never shown).

Not available over HTTP

Downloads exist only on stdio, confined to downloads.directory: the download tools (planned for build step 6) are never listed over HTTP. Each instance keeps its own in-memory response cache (cache.* settings), so instances do not share state.