-
Notifications
You must be signed in to change notification settings - Fork 1
Deployment
Running the server. For pointing a client at one, see Installing; for a first run on your own machine, Quick start.
The image is the whole artifact. It bakes no skills: give it a config file and a writable cache directory and it serves whatever that config names. It ships the example config, so it runs with no config at all; what goes in your own is The config file.
Every flag has an environment fallback, because a container is configured with environment variables and a developer reaches for flags. These say how to run; the config file says what to serve, and the two never overlap.
| Env | Flag | Default | Notes |
|---|---|---|---|
CONFIG |
--config |
/etc/mcp-kb/config.yaml |
The config file listing the sources to serve |
CACHE_DIR |
--cache-dir |
/var/cache/mcp-kb |
Where a non-file:// source materialises, and where index.json lives |
TRANSPORT |
--transport |
http |
http or stdio
|
HOST |
--host |
0.0.0.0 |
Bind address, http only |
PORT |
--port |
8000 |
Port, http only |
LOG_LEVEL |
--log-level |
INFO |
The least severe line written, by every logger in the process. A fetch, plugin or library changing state is INFO, WARNING (stale, skipped) or ERROR (failed). Traffic is held back to DEBUG: a request line per MCP call from uvicorn, and a line per WebDAV request from httpx |
A source's credentials are the other half of the environment: each is an
{env: NAME} reference on the source, resolved from the process environment at
the moment of the call. A plugin never carries one. Nothing else in the process
reads the environment.
mcp-kb schema prints the config JSON Schema and exits, which is what
config.schema.json in the repository holds.
A bad config file is fatal at boot, on purpose: exit code 2 and the reason on stderr. So is a server that refuses its credentials while a fetch is first built — a 401 or a 403 from WebDAV, an authentication failure from git: exit code 3, naming the fetch and the status. That failure is the one a wait does not fix — a wrong app password, an account not yet let in — and a pod that exits is restarted where everyone can see it, until it is fixed.
A config that parses but names something that cannot be reached is not fatal — that fetch is reported failed and everything else is served — and nor is a refusal on a later refresh of a fetch already serving, which goes stale. See Operations.
docker run --name mcp-kb -d -p 127.0.0.1:8000:8000 \
--mount type=volume,source=mcp-kb-cache,target=/var/cache/mcp-kb \
--mount "type=bind,source=$PWD/config.yaml,target=/etc/mcp-kb/config.yaml,readonly" \
-e GITHUB_TOKEN \
kubed/mcp-kb:latestA cache volume and your config, and that is the deployment. Leave the config mount off and the image serves its built-in example.
Use --mount, not -v, for the config: -v creates a missing source as a
directory, and the container then fails with "not a directory". --mount
refuses a missing file up front. The config is read-only; the cache is not, and
wants to be a named volume rather than a bind mount — a named volume
inherits the image's ownership of /var/cache/mcp-kb and needs nothing further,
where a host directory arrives owned by whoever made it.
127.0.0.1 keeps the port on this machine. The server has no auth, so publish
it wider only behind something that has.
The container runs as uid 65534. If the cache is not writable by it the
server still serves — the catalogue is built in memory either way — but it logs
that it cannot write index.json and re-harvests everything on every restart.
-e GITHUB_TOKEN passes a variable through for an {env: GITHUB_TOKEN}
reference in the config. Never put a credential in a source URL; the server
refuses one that carries it.
There are no manifests in this repository — deployment is the installer's — but five details are worth copying rather than rediscovering.
The config, and prompts, come from ConfigMaps. Mount the config at
/etc/mcp-kb/config.yaml and, if you ship prompts or skills with the
deployment rather than from a remote, mount them somewhere and declare a plugin
at file:///that/path. A ConfigMap mount is entirely symlinks — every key
is key → ..data/key → ..<timestamp>/key — and that is fine: a file:// plugin
follows a link that lands inside its own root, and the path a client sees is the
one the config asked for, not the timestamped directory behind it.
plugins:
- name: grafana-prompts
source: file:///srv/prompts/grafana
prompts: ["*.md"]The cache is an emptyDir, and the pod needs fsGroup.
spec:
securityContext:
fsGroup: 65534
runAsNonRoot: true
containers:
- name: mcp-kb
volumeMounts:
- name: cache
mountPath: /var/cache/mcp-kb
volumes:
- name: cache
emptyDir: {}An emptyDir or a PVC mounts root-owned. The image runs as 65534, so without
fsGroup: 65534 the cache directory is unwritable, index.json is never
written, and every restart re-clones everything instead of reading last boot's
answer back in milliseconds.
Use a startupProbe, not a tight readinessProbe. The first boot with an
empty cache clones every git repository and copies every WebDAV folder before
the server binds its port. How long that takes is a property of what you have
named, not of this server, and a readiness probe sized for the steady state will
restart the pod in the middle of it.
startupProbe:
httpGet: { path: /health, port: 8000 }
periodSeconds: 5
failureThreshold: 60
readinessProbe:
httpGet: { path: /health, port: 8000 }
livenessProbe:
httpGet: { path: /health, port: 8000 }Do not tie liveness to what loaded. /health answers 200 whenever the
process is serving, including when a fetch failed to load — the failure is
reported inside fetches and on every plugin that reads it, and everything that
did load is still being served. That is the design: one unreachable remote must
not take a working catalogue down. A probe that parses the body and fails on a
failed fetch converts a partial outage into a restart loop, and a restart does
not fix an unreachable remote. Alert on the body; probe on the status code. The
one restart that is worth it — refused credentials on a first build — the
process already does by exiting.
Credentials are Secrets, referenced by name. The config file holds
{env: NEXTCLOUD_PASSWORD}, never a value, so it is safe to keep in a ConfigMap
and in git:
env:
- name: NEXTCLOUD_PASSWORD
valueFrom:
secretKeyRef: { name: mcp-kb, key: nextcloud-password }An env/secretKeyRef value is fixed when the pod starts, so rotating the
Secret needs a kubectl rollout restart.
CONFIG=./config.yaml CACHE_DIR=./.cache TRANSPORT=stdio mcp-kbOne process, one client, over pipes — the client launches the server. There is no
URL, so there are no query parameters and no headers: nothing is scoped and every
MCP feature is assumed present, which is right for the clients that launch a
server this way. /health, /reindex and /openapi.yaml are HTTP routes and do
not exist in this mode.
Home · Installing · Sources · Plugins · Operations
Configuration, Tools and Endpoints are generated by scripts/generate_wiki.py from the config models, a live server's tool list and the OpenAPI document — so they describe the server that shipped, not the one someone remembered. Prose for a generated page goes in wiki/notes/<page>.notes.md.
mcp-kb · MIT
Getting started
The config file
What clients get
Reference