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:
pipeline.yaml — a typical pipeline config
pipeline.yaml — a typical pipeline config
joins.yaml — targets and document shapes
joins.yaml — targets and document shapes
The joins spec is where targets get their names and the documents get
their shape:
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
--config. Target
names must be unique across the loaded configs:
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:--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:
/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-refor--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:--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:
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.
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 jointarget.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>capssearch/scanresult 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.