Skip to content
Beta. VectorStep is in beta.Let us know if something breaks.

Adding MCP tool servers

Agents act through tools, and tools come from MCP servers that the Gateway runs for them — Grafana, Atlassian, a web-fetch tool, a database client, anything that speaks MCP over stdio. This page is the how-to for adding one on any platform.

You declare each server under mcp_servers: in the Gateway’s config.yaml. At startup the Gateway spawns each one as a subprocess, does the MCP handshake, and caches its tools. An agent then lists the server by name in tools: in its agent.yaml. The full config reference is in Gateway configuration.

mcp_servers:
time:
command: uvx
args: ["mcp-server-time"]
grafana:
command: npx
args: ["-y", "@grafana/mcp-grafana"]
env:
GRAFANA_URL: ${GRAFANA_URL}
GRAFANA_TOKEN: ${GRAFANA_TOKEN}

Adding or changing a server needs a Gateway restart. POST /reload re-reads agents and skills, but it never re-reads config.yaml and never starts new server subprocesses.

Which servers can run here — and the one rule to remember

Section titled “Which servers can run here — and the one rule to remember”

The Gateway runs MCP servers as subprocesses inside its own container. So:

The Gateway can run any MCP server that speaks stdio and can be launched as a command inside the Gateway container. Whatever that command needs — a runtime, a package, a system library, a binary — has to be in the container. If it isn’t in our image, build your own image from ours and add it.

That’s the whole model. Our image is a base, not a limit: the stock image covers the common case (npx and uvx servers), and for anything else you FROM it and install what your server needs (how, with examples).

Two kinds of server can’t run this way, whatever you add to the image:

  • Remote servers reached over HTTP/SSE. The config has no URL option — only command, args and env — and the Gateway speaks stdio to what it spawns. Use a server that can be launched locally, or put a stdio bridge in the image that launches as a command.
  • Servers you can only run as a container (docker run -i …). There’s no Docker inside the Gateway container. Install the server’s own runtime and files into your image instead, or run it where it can be reached as above.

The published vectorstep-gateway image is built to run npx and uvx servers without extra setup:

Included Version (0.1.12)
Node.js, npm, npx Node 22
uv, uvx 0.12
Python 3.11
curl yes

Not included: git, docker, Go, Java, Deno, Bun. A server that needs one of these — for example uvx --from git+https://…, a server launched with docker run, or a Go or Java binary — won’t run in the stock image. Build your own image that adds it (see Pre-installing servers).

On native Linux (no container) the Gateway binary bundles neither Node nor Python, so install Node.js and/or uv on the host yourself — but only if you configure npx or uvx servers. A server that fails to spawn is logged and skipped; every other server and agent keeps working.

Platform Config file
Docker Compose ~/.vectorstep/config/gateway.yaml
Native Linux /etc/vectorstep/vectorstep-gateway/config.yaml
Kubernetes the config.yaml key of the vectorstep-gateway-config ConfigMap (kubectl -n vectorstep edit configmap vectorstep-gateway-config)

Add the server under mcp_servers:. (The shipped Kubernetes config starts with mcp_servers: {} — replace that line.)

A server often needs a token or URL. Never put a secret in config.yaml; reference an environment variable and supply the value through your platform’s secret mechanism:

Platform Where the secret goes
Docker Compose ~/.vectorstep/.env — it’s forwarded into the Gateway container
Native Linux /etc/vectorstep/vectorstep-gateway/env
Kubernetes the vectorstep-gateway-secrets Secret, which the Deployment already loads as environment variables: kubectl -n vectorstep patch secret vectorstep-gateway-secrets -p '{"stringData":{"GRAFANA_TOKEN":"…"}}'

Then reference it in the server’s own env: block, as in the example above. This is the only way a secret reaches the server. Each spawned server gets a deliberately minimal environment — just PATH, HOME, LANG and whatever its own env: block lists, with ${VAR} filled in from the Gateway’s environment. It does not inherit the Gateway’s other variables, so one server can’t see your LLM provider keys, the Gateway’s tokens, or another server’s credentials. (We checked: with GRAFANA_TOKEN, ANTHROPIC_API_KEY and an unrelated secret all set on the Gateway, a server declaring only GRAFANA_TOKEN received HOME, LANG, PATH and that one value.)

The corollary: if a server needs a variable and you only set it on the Gateway, the server won’t see it. Declare it in that server’s env:.

Platform Command
Docker Compose docker compose restart gateway
Native Linux sudo systemctl restart vectorstep-gateway
Kubernetes kubectl -n vectorstep rollout restart deployment/vectorstep-gateway

Each of these needs a Gateway token (the invoke or admin token works for the read routes). On Kubernetes, port-forward first (kubectl -n vectorstep port-forward svc/vectorstep-gateway 18780:18780).

Terminal window
curl -s -H "Authorization: Bearer $TOKEN" localhost:18780/mcp/servers
# {"time":{"running":true,"pid":7,"restart_count":0}, ...}
curl -s -H "Authorization: Bearer $TOKEN" localhost:18780/mcp/tools
# {"time":[{"name":"get_current_time", ...}, ...]}
  • GET /mcp/servers shows each server’s running state.
  • GET /mcp/tools lists the tools each server exposed — an empty list for a server means it failed to start.
  • GET /health reports "status": "degraded" while any configured server is down.
  • The vectorstep_gateway_mcp_servers_running metric is 1 per running server (see Operations).
  • With the Gateway MCP, the list_mcp_servers and list_mcp_tools tools show the same thing.
  • Failures are in the Gateway log: Failed to start MCP server '…': ….

List the server’s name (the key under mcp_servers:, not the package) in the agent’s tools::

agent.yaml
name: sre-triage
model: anthropic/claude-sonnet-4-6
max_tokens: 8192
tools:
- grafana

The agent is validated against your configuration, so a typo is caught up front rather than at run time — tools: [nope] is rejected with “tools reference unconfigured MCP server ‘nope’”. To restrict which of a server’s tools an agent may actually call, see Tool policy.

npx -y <package> downloads the package the first time it runs. The Gateway gives a server 10 seconds to complete its startup handshake, and that limit isn’t configurable. If the download takes longer than that, the server fails with:

ERROR Failed to start MCP server 'memory': MCP request 'initialize' timed out after 10.0s

and /health shows degraded. Whether this happens depends on your network: on our test cluster, with empty caches, an npx server started within about four seconds of the first attempt, while on a slower connection the same server timed out. A server whose first start fails is not retried until the Gateway restarts (one that crashes after starting is restarted automatically — watch restart_count in /mcp/servers). The download did complete and is cached, so restarting the Gateway once fixes it: in our test the same server started within two seconds on the second start. uvx servers are usually quicker, but the same can happen on a slow network.

Where that cache lives matters:

  • Kubernetes: the shipped Deployment sets HOME to the Gateway’s PVC (/data/home), so the cache survives restarts and a restart is all it takes.
  • Docker Compose: the Gateway container’s HOME isn’t on the persistent /data volume, so the cache lives in the container itself and is lost whenever the container is recreated (for example, upgrading the image) — the next start is cold again.

The way to avoid this entirely, and the one we recommend for anything beyond experimenting, is to install the servers into the image.

This is also how you run a server that needs anything the stock image lacks. Build a small image on top of the Gateway’s and install your servers and their prerequisites into it — it’s the same FROM + RUN you’d write for any container. Nothing is downloaded at start-up, so servers come up immediately — no cold-start timeout, no dependence on npm or PyPI being reachable (which also makes this the answer for air-gapped or egress-restricted clusters), and the versions are pinned and reviewable.

FROM ghcr.io/bantex01/vectorstep-gateway:0.1.12
USER root
# npm packages install globally; uv tools go to /opt/uv-tools with their
# launchers on PATH. Pin exact versions.
RUN npm install -g @modelcontextprotocol/[email protected] \
&& UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin \
uv tool install mcp-server-time==2026.8.18
# Anything else the server needs (git, a Go binary, …) goes here too.
USER vectorstep

Then refer to the servers by their installed command names — no npx, no uvx:

mcp_servers:
time:
command: mcp-server-time
memory:
command: mcp-server-memory

Build and push it to a registry your cluster can pull from, and point the Gateway at your image: set image: on the Gateway Deployment (add an imagePullSecrets entry for a private registry), or image: on the gateway service in your compose file. Keep your image’s base tag in step with the service — the service and Gateway must run matching versions (see Image tags and versions).

We tested this image on an empty volume with a read-only root filesystem and no network at all: both servers initialised within the same second.

Always switch to root to install, and back to vectorstep (the non-root user the Gateway runs as) at the end.

Your server needs… Add to your Dockerfile
An OS package (git, a client library, a CLI the server shells out to) — the base is Debian RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
An npm package RUN npm install -g <package>@<version> — then command: is the package’s bin name
A Python package/CLI RUN UV_TOOL_DIR=/opt/uv-tools UV_TOOL_BIN_DIR=/usr/local/bin uv tool install <package>==<version>
A prebuilt binary (Go, Rust, …) COPY --chmod=755 my-mcp-server /usr/local/bin/ — build it for the image’s architecture (amd64 or arm64; the published image is both)
A different runtime (Java, Ruby, …) install it with apt-get, or copy it in, then command: points at the server’s launcher

We tested the OS-package, npm and Python recipes: the git image ran as the vectorstep user under a read-only root filesystem, and the npm and Python ones are the example above. Pin versions so the image is reproducible, rebuild it when you upgrade the Gateway, and keep it on the same version as the service.

The shipped Gateway Deployment runs with readOnlyRootFilesystem: true, a non-root user and dropped capabilities, and is set up so npx/uvx servers work under those restrictions: HOME and the package caches point at the PVC. We ran an npx and a uvx server on a cluster with these exact settings: with empty caches both started on the first attempt, the caches landed on the PVC (about 100 MB for these two) and survived a restart so the next start was instant, and a secret supplied through the Gateway’s Secret reached only the server that declared it. Two things to plan for:

  • Egress. Without a pre-built image, the first start needs to reach the package registries (registry.npmjs.org for npx; pypi.org and files.pythonhosted.org for uvx). If you apply egress NetworkPolicies, allow those or use a pre-installed image.
  • Changes are a restart, not a hot reload. Edit the ConfigMap, then kubectl rollout restart. (See Changing config on Kubernetes for how agent and pipeline changes — which do hot-reload — are delivered.)

Edit ~/.vectorstep/config/gateway.yaml and ~/.vectorstep/.env, then docker compose restart gateway. A plain docker compose up -d does not restart a container whose compose definition hasn’t changed, so use restart. For a pre-built image, replace the gateway service’s image: in your compose file.

Install Node.js and/or uv on the host for any npx/uvx server (see Linux install), edit /etc/vectorstep/vectorstep-gateway/config.yaml and the env file beside it, then sudo systemctl restart vectorstep-gateway. Check journalctl -u vectorstep-gateway for spawn errors.

Symptom Likely cause
Failed to start MCP server '…': … 'initialize' timed out after 10.0s A cold npx/uvx download exceeded the 10-second start limit. Restart once (cache is warm), or pre-install
/health shows degraded At least one configured server isn’t running — check /mcp/servers and the log
/mcp/tools lists a server with no tools That server failed to start
Server starts but its calls fail with auth errors It didn’t get its secret — declare the variable in the server’s own env: block; the Gateway’s other variables aren’t passed through
tools reference unconfigured MCP server '…' when saving an agent The name in tools: isn’t a key under mcp_servers:
command not found / spawn error for npx or uvx Native Linux: Node.js / uv isn’t installed on the host. Container: the server needs a runtime the image lacks (git, docker, Go, Java…) — build a derived image
Config change had no effect MCP servers are read at startup — restart the Gateway; POST /reload doesn’t re-read config.yaml
Server worked, then stopped working after an image upgrade (Docker Compose) The package cache was in the old container; the new one starts cold — restart once, or pre-install