Skip to content

MCP HTTP transport

The Python vmaf-mcp server can run in HTTP server mode with --transport http. This mode exposes a REST API for Kubernetes liveness/readiness probes, Prometheus scraping and direct curl-based scoring. The Go vmafx-mcp server has its own HTTP mode with a different surface; the table below shows the difference.

Added in: VMAFX Phase 3A (ADR-0701) Default transport: stdio (unchanged for IDE/MCP-client compatibility)

Python versus Go

Python vmaf-mcp Go vmafx-mcp
Enable HTTP --transport http (flag), listen port from --port or VMAFX_PORT (default 8080) VMAFX_MCP_TRANSPORT=http, listen address from VMAFX_MCP_HTTP_ADDR (default :3000); no CLI flags
Protocol served REST routes /healthz, /readyz, /metrics, /v1/score MCP streamable-HTTP protocol at the listen address; no REST routes
Authentication, bind host VMAFX_MCP_HTTP_TOKEN, VMAFX_MCP_HTTP_NO_AUTH, VMAFX_MCP_HTTP_BIND the same three variables
TLS VMAFX_MCP_HTTP_TLS_CERT, VMAFX_MCP_HTTP_TLS_KEY not read by the Go server
Body cap 4 MiB 4 MiB

The rest of this page documents the Python REST mode, except the Security section, which applies to both. For the Go server see the Go section of the MCP overview.


Quick start

  1. Install with the HTTP extras.

    pip install 'vmaf-mcp[http]'
    
  2. Start the server on port 8080 with bearer authentication.

    VMAFX_MCP_HTTP_TOKEN='replace-with-a-secret' \
      vmaf-mcp --transport http --port 8080
    

Note

--port can be replaced by the environment variable VMAFX_PORT=8080; the flag wins when both are set.

Python embedding

The supported command-line entry point installs the canonical scoring runtime automatically. Applications that embed the transport directly may instead pass an object implementing vmaf_mcp.http_scoring.HttpScoringRuntime:

from vmaf_mcp.http_transport import run_http_server

run_http_server(port=8080, runtime=my_scoring_runtime)

The injected runtime owns path validation, request construction, score execution, and strict JSON serialization. It implements these four methods:

Method Contract
vmaf_binary() -> pathlib.Path Return the executable path used by /readyz; the transport checks that it exists and is a file.
build_request(**fields) -> object Validate the nine documented score fields and return the request object consumed by run_score. Raise TypeError, ValueError, or FileNotFoundError for invalid client input; the transport maps those exceptions to HTTP 400 without exposing their text.
async run_score(request) -> dict[str, object] Execute one request and return the JSON-compatible score payload. Other exceptions are logged and mapped to HTTP 500.
dumps_strict(data) -> str Return RFC 8259 JSON for the response payload, rejecting or normalising non-finite numbers rather than emitting bare NaN or Infinity.

How run_http_server treats the runtime (ADR-1304):

  • It resolves the object before allocating an event loop or binding a socket, and keeps it bound to that server's aiohttp application for the server lifetime.
  • It does not install the object into process-global state, so several embedded servers may use different runtimes at once; each runtime stays responsible for concurrency among the requests its own server handles.
  • If direct startup has neither an injected runtime nor the canonical server runtime, it raises instead of exposing a healthy-but-unready HTTP process.
  • Importing vmaf_mcp.server does not replace a runtime an embedding application installed first.

Endpoint reference

GET /healthz — Liveness probe

Returns 200 OK while the process is alive after the request passes the shared authentication middleware. Suitable for an authenticated Kubernetes livenessProbe.

Response

{ "status": "healthy" }

GET /readyz — Readiness probe

Returns 200 OK once the configured vmaf binary is reachable on the filesystem. Returns 503 Service Unavailable if the binary is absent. Suitable for Kubernetes readinessProbe.

The check is a lightweight stat call — no subprocess is spawned.

Response (ready)

{ "status": "ready", "vmaf_binary": "/usr/local/bin/vmaf" }

Response (not ready)

{ "status": "not_ready", "reason": "vmaf binary not found at /usr/local/bin/vmaf" }

GET /metrics — Prometheus metrics

Returns metrics in Prometheus exposition format. Suitable for prometheusRule scraping.

Exposed metrics

Metric Type Description
vmaf_scoring_requests_total{endpoint, status} Counter Total scoring requests, labelled by endpoint and HTTP status
vmaf_scoring_errors_total Counter Total scoring requests that resulted in a 500-level error
vmaf_scoring_duration_seconds Histogram Scoring request latencies (buckets: 0.1s … 300s)

POST /v1/score — Score a YUV pair

Submits a VMAF scoring request for a raw YUV pair. This is a thin REST wrapper over the vmaf_score MCP tool.

Request body (JSON)

Field Type Required Description
reference string yes Absolute path to the reference YUV file
distorted string yes Absolute path to the distorted YUV file
width integer yes Frame width in pixels
height integer yes Frame height in pixels
pixfmt string yes Pixel format: "420", "422", or "444"
bitdepth integer yes Bit depth: 8 | 10 | 12 | 16
model string no Model specifier (default: "version=vmaf_v1.0.16_3d0h", the library default model)
backend string no Backend: "cpu", "cuda", "sycl", "hip", "metal", or "auto" (default: "auto")
precision string no Output precision: "legacy" (%.6f, the C-CLI default per ADR-0119) or "max" (lossless %.17g). Default: "legacy"

Example request

curl -X POST http://localhost:8080/v1/score \
  -H 'Authorization: Bearer replace-with-a-secret' \
  -H 'Content-Type: application/json' \
  -d '{
    "reference": "/data/ref.yuv",
    "distorted": "/data/dis.yuv",
    "width": 1920,
    "height": 1080,
    "pixfmt": "420",
    "bitdepth": 8
  }'

Response (200 OK)

The vmaf JSON payload plus a request_id field:

{
  "vmaf": 85.432,
  "frames": [ ... ],
  "pooled_metrics": { ... },
  "request_id": "a3f9c21b"
}

Error responses

Status Condition
400 Missing required fields, invalid JSON body (including non-object JSON values such as null, arrays, or integers), or path outside allowlisted roots
401 Missing or invalid Authorization: Bearer token (when auth is enabled)
413 Request body exceeds 4 MiB (enforced by both Content-Length pre-flight and client_max_size for chunked bodies)
500 Scoring subprocess failed

The HTTP body takes only the nine fields above; subsample and the other extended vmaf_score arguments are available over stdio only.


Environment variable reference

These variables apply to the Python server. CLI flags take precedence over environment variables; environment variables take precedence over compiled-in defaults.

Variable Default Description
VMAFX_PORT 8080 HTTP listen port (overridden by --port)
VMAFX_LOG_LEVEL INFO Python log level: DEBUG, INFO, WARNING, ERROR
VMAFX_VMAF_BINARY (auto-detected) Explicit path to the vmaf binary; falls through to VMAF_BIN
VMAFX_MODEL_DIR (none) Additional model search root; appended to VMAF_MCP_ALLOW

Security (ADR-0967)

These variables harden the HTTP transport and are honoured identically by both the Python (vmaf-mcp) and the Go (vmafx-mcp) servers, so a single deployment config secures either implementation. Every variable the Go server reads is in its environment table.

Variable Default Description
VMAFX_MCP_HTTP_TOKEN (none) Bearer token. When set (and NO_AUTH is unset), every request must carry Authorization: Bearer <token>, matched in constant time.
VMAFX_MCP_HTTP_NO_AUTH (unset) Set to 1 to disable authentication entirely (explicit operator opt-out).
VMAFX_MCP_HTTP_BIND 127.0.0.1 Bind host. Loopback-only by default; set to 0.0.0.0 to listen on all interfaces.
VMAFX_MCP_HTTP_TLS_CERT, VMAFX_MCP_HTTP_TLS_KEY (none) Certificate and key paths for serving HTTPS (Python server only).

When neither VMAFX_MCP_HTTP_TOKEN nor VMAFX_MCP_HTTP_NO_AUTH=1 is set, the server rejects every request with 401 — a missing token means auth was not configured, and refusing is safer than silently accepting. The request body is capped at 4 MiB (413 on overflow). For the Go server, VMAFX_MCP_HTTP_BIND only substitutes the host when the configured VMAFX_MCP_HTTP_ADDR (e.g. :3000) carries no explicit host; an address that already pins a host wins.


Structured JSON logging

HTTP mode replaces the root logger's handlers with a single-line JSON formatter. Each log line is a JSON object with the following fields:

Field Example Description
timestamp "2026-05-28T12:34:56.789Z" ISO-8601 with millisecond precision
level "INFO" Python log level
message "POST /v1/score done in 420ms" Human-readable message
request_id "a3f9c21b" 8-character hex request identifier (or "-" for server-level events)
logger "vmafx.http" Logger name
module "http_transport" Source module
lineno 303 Source line number

SIGTERM behaviour and graceful shutdown

On receiving SIGTERM or SIGINT, the server:

  1. Logs "SIGTERM received — initiating graceful shutdown".
  2. Stops the asyncio event loop.
  3. Calls AppRunner.cleanup() to drain in-flight requests and close the TCP listener.

The cleanup runs within the event loop's finally block; there is no separate hard timeout enforced at the transport layer beyond the asyncio task cancellation semantics. Kubernetes pods should set terminationGracePeriodSeconds to at least 30 seconds to allow long-running scoring requests to complete.


Optional dependencies

HTTP mode requires the [http] extra, which is not installed by default:

pip install 'vmaf-mcp[http]'
# or install the base package and HTTP dependencies explicitly:
pip install vmaf-mcp 'aiohttp>=3.14.3' 'prometheus-client>=0.26.0'

aiohttp>=3.14.3 is a security floor: 3.14.3 is the first release that fixes CVE-2026-69244. The floor does not narrow the MCP server's supported Python range because both packages require Python 3.10 or newer. The transport registers only its dynamic API, health, readiness, and metrics routes; it does not configure aiohttp static resources or enable follow_symlinks.

If aiohttp or prometheus-client is absent and --transport http is requested, the server raises an ImportError with an installation hint. The published vX.Y.Z-server image installs [eval,http], so its default HTTP entrypoint includes both dependencies. Its image configuration explicitly sets VMAFX_MCP_HTTP_BIND=0.0.0.0; standalone Python installs retain the safer 127.0.0.1 default.


Kubernetes deployment

For a full Kubernetes deployment, see:

The Helm chart configures liveness and readiness probes against /healthz and /readyz (livenessProbe / readinessProbe in deploy/helm/vmafx/values.yaml) and ships a ServiceMonitor for Prometheus scraping. The chart does not set VMAFX_PORT.


See also

  • ADR-0701 — design decisions for this transport.
  • MCP tools reference — full list of MCP JSON-RPC tools available over the default stdio transport.
  • MCP backends — backend selection for scoring.