Skip to main content
VentStream ships a read-only MCP server: ventstream mcp. It lets AI agents — Claude Desktop, IDE assistants, support bots — query the documents your pipelines materialize in the sinks, without holding any source-database credentials. The server is part of the open-source engine binary. It needs nothing but your pipeline config and read-only sink credentials, and it never touches the source database: agents read the sink documents the pipeline keeps in sync within milliseconds.

The config

ventstream mcp takes the same pipeline config your engine already runs — usually the exact file, unchanged. From it the server learns three things: where the sink is (and its credentials), what the targets are, and what the documents look like (from the joins spec). A typical config:
The joins spec is where targets get their names and the documents get their shape:
With this config the server exposes one target, orders, whose documents are orders with an embedded customer object and items array — and it serves that shape description to agents automatically. A complete runnable version of this setup — schema, seed data, configs, token flow, Claude wiring — lives in examples/mcp.

Run it

Serve several pipelines from one endpoint by repeating --config. Target names must be unique across the loaded configs:
The env vars referenced by the config (VS_MEILI_ENDPOINT, …) must be set for the MCP server too — give it read-only sink credentials rather than the writer’s.

Running as a role

The subcommand above is the interactive way. For supervised deployments — systemd, Kubernetes, or Fleet-managed — the engine can run the MCP server as a role, dispatched from the config itself with no CLI arguments:
VS_ENGINE_CONFIG=… ventstream (no arguments) boots the server, serves /readyz on the health listener, and shuts down cleanly on SIGTERM. An mcp-role config has no source: — it only ever reads the sink. Fleet-managed: apply the same YAML with ventstreamctl configurations apply --engine-config … — the control plane validates it and delivers it to the deployment like any pipeline config. The managed-agent chart supports workload.roles: mcp (under kind: realtime) with service.mcpPort exposing the listener, and ventstreamctl mcp keys generate mints tokens, printing the token once and the token_hash entry for your keys secret. Query traffic always goes straight from agents to the MCP deployment — never through the control plane.

Transports

stdio (default) — the MCP client spawns the process. A Claude Desktop entry looks like:
HTTP (--listen) — a shared endpoint for remote agents. Run one instance per environment next to your sinks; agents connect over the network and no sink credentials leave that deployment:
Clients POST MCP JSON-RPC to /mcp with Authorization: Bearer <token>; /healthz serves load-balancer checks. The server is stateless — no session ids — so replicas behind a plain load balancer need no session affinity. Security is enforced, not optional:
  • Binding a non-loopback address requires auth (--auth-token-ref or --keys-ref) — the server refuses to start without it.
  • Browser-origin requests are rejected unless allow-listed with --allow-origin (DNS-rebinding protection).
  • Request bodies cap at 1 MiB; tool dispatch times out at 30 s.
  • Run it behind TLS termination (your load balancer or reverse proxy).

Tokens and scoping

Mint a token with the built-in generator:
The simplest setup is one full-access token via --auth-token-ref env:VS_MCP_TOKEN. For per-agent control, use a keys file — each key is a named token restricted to the targets it may query:
A scoped key’s world shrinks to its targets: list_targets and resources list only them, and querying anything else returns the same error as a target that does not exist — a scoped agent cannot discover what it is not allowed to see. Every request is logged with its key name for attribution. Revoke a key by removing it from the file and restarting (or rolling the deployment).

Tools

pk accepts a scalar or an array for composite keys. Values are normalized exactly like the writer normalizes them, so 100 and "100" find the same document.
returns the full joined document — parent row plus embedded relations — straight from the sink. Each target is also exposed as an MCP resource at vs://targets/{name} describing its document shape (primary table, key columns, embedded relations and their cardinality), derived from your joins spec. Agents read it to learn what the documents look like before querying.

How keys resolve

Doc ids and sink keys are deterministic, and the server links the same encoder code the sinks use when writing — Redis keyspace, Meilisearch primary keys, OpenSearch ids. A read key is byte-identical to the written key by construction. Target enumeration follows your sink routing:
  • fixed — the configured target or index.
  • views (Redis) — one target per view.
  • by_projection_target — one target per join target.index.
  • by_output_relation — targets are not derivable from config; pass them explicitly with --target <name> (repeated).

Guardrails

  • Use read-only sink credentials in the config you give the server — it only ever issues reads, but the credential should enforce it too.
  • --allow-target <name> (repeated) restricts the served targets; anything else behaves as unknown.
  • --max-results <n> caps search/scan result sizes (default 50, hard max 500).

Notes

  • A degraded pipeline does not stop reads — agents see whatever the sink currently holds. Watch pipeline health to know how fresh that is.
  • Server-initiated SSE streams are not offered: every tool is a short single-shot read, answered as plain JSON.