-
Notifications
You must be signed in to change notification settings - Fork 1
Sources
A source is a backend, named once: where bytes come from and how to log in. It is the only place a credential is written, however many plugins read through it. What is served out of one is Plugins, grouped into Libraries; every field is in Configuration.
sources:
- name: github
url: git+https://github.com
- name: nextcloud
url: webdav+http://cloud.example.com/remote.php/dav/files
auth:
username: {env: NEXTCLOUD_USER}
password: {env: NEXTCLOUD_PASSWORD}
cache: live
refresh: 5mA plugin's source — and a marketplace library's — is an address whose
scheme is a source's name:
nextcloud://mcp-kb/ai the ai folder of the mcp-kb account, over WebDAV
github://grafana/skills?ref=… that repository at that commit
file:///srv/prompts/grafana a directory on this machine
The source's url is the base and the address's path completes it, so
github://grafana/skills is https://github.com/grafana/skills and
nextcloud://mcp-kb/ai is that folder under
…/remote.php/dav/files. file:// is built in and needs no source entry: a
local directory carries no credential and nothing to configure.
Nothing about a backend reaches what is served. A cache path, a clone directory, a WebDAV URL and a source's name are config; a URI, a listing row, a prompt name and a served body are not.
Whatever the backend, what a source names becomes a directory on this
machine before anything reads it, and that materialised tree is a fetch. Its
identity is the address without its subdirectory — the path and the ?ref= —
so two plugins in one repository at one commit are one clone, one export and one
entry in /health.
That is what makes declaring your own plugin beside somebody's marketplace free: same URL, same ref, same fetch.
- name: github
url: git+https://github.com| URL | Is |
|---|---|
git+https://host |
any git remote over HTTPS |
git+http://host |
the same, unencrypted |
git+file:///path |
repositories on this machine |
The repository is cloned bare and shallow into the cache — no working copy,
nothing writable, and git+file:// clones whole because libgit2's local
transport refuses a shallow fetch. The ref on the address is resolved to a
commit and that commit's tree is exported to its own directory, so a refresh
adds a tree rather than overwriting the one being served.
A symlink in a git repository is exported as a regular file holding the link's
target as its text, because an export has no way to recreate links. The same
tree behind file:// resolves the link instead. Neither escapes its root.
?ref= on the address is a branch, a tag or a 40-character commit sha. Left
off, the address tracks the remote's default branch.
-
A pinned sha never touches the network to answer "has it moved?" A commit
cannot move, so there is nothing to ask. The example config pins all four of
its libraries and gives its source no
refresh:for exactly that reason. - A branch or a tag costs one ref listing per check. That is the cheapest question git has — no fetch, no objects — and only a commit that turned out to have moved downloads anything.
Pin a sha when you want an image that serves the same bytes in a year. Track a
branch when you want the tip, and give the source a refresh: so something goes
and looks.
- name: github
url: git+https://github.com
auth:
username: x-access-token
password: {env: GITHUB_TOKEN}GitHub wants the literal x-access-token as the username and a token as the
password. The username also takes an {env:} reference, for the common case
where a service account's name is issued in the same secret as its password and
writing it out twice is how the two drift apart.
The credential is resolved at the moment of the call and nowhere else. It is
the only way in: a URL carrying its own user:token@ is refused outright,
because git writes a remote URL into the clone's config verbatim and that URL is
quoted back in what /health reports.
- name: nextcloud
url: webdav+http://cloud.example.com/remote.php/dav/files
auth:
username: {env: NEXTCLOUD_USER}
password: {env: NEXTCLOUD_PASSWORD}The URL plus a plugin's path is the folder — for Nextcloud, what you see in
the web UI under remote.php/dav/files/<user>/. auth is required: WebDAV has
no useful anonymous mode, so an omitted credential is a typo rather than a
choice, and it is refused at load. Use a Nextcloud app password, never the
account's own.
A failure says which: the folder /remote.php/dav/files/mcp-kb/ai does not exist, HTTP 401: the credentials were refused, or HTTP 403: the account may not read this folder. A refusal while a fetch is first built stops the server —
see Deployment.
The folder is priced with one recursive PROPFIND and the digest of its ETags names the copy, so a folder that has not changed resolves to the export already on disk and is not downloaded again.
plugins:
- name: house-prompts
source: file:///srv/prompts/grafana
prompts: ["*.md"]Served in place; nothing is copied, and no source entry is needed. The path must
be absolute: file:///path, three slashes. file://relative/path fills the
host slot instead and is refused.
A symlink is followed when it lands inside the root and ignored when it escapes
— which is what makes a Kubernetes ConfigMap mount, all symlinks, servable. The
path a client sees is the one the config asked for, not the ..<timestamp>/
directory a mount resolves to.
A source over a local base path — url: file:///srv/skills, under a name of its
own — is allowed and occasionally worth it: it gives several plugins under one
directory a shared base and a refresh:. The name file itself is refused,
because that scheme is built in and nothing could ever address such a source.
snapshot is the default and is how every mirrored backend behaves: the tree is
copied when it is built and read from disk thereafter, so a request touches no
network at all.
cache: live keeps that copy and revalidates a file as it is read — one
PROPFIND for its ETag, and a download only if the ETag moved. A file edited in
Nextcloud is served on the next read, with no refresh and no restart. It needs a
WebDAV source; on git or a local directory it would be a dial that does nothing,
and it is refused at load.
What live mode does not do:
- It cannot see a new file. One that did not exist when the tree was harvested has no URI, so nothing ever asks to read it — and the same goes for a renamed or deleted one.
-
It does not refresh a description. Listing rows come from the harvest, so
an edited
description:appears when the fetch is rebuilt.
Both of those are what refresh: is for, and POST /reindex forces one now.
Live mode is for the bodies of the files that are already there.
Revalidation is only as sharp as the server's ETag. Nextcloud derives one from
the content; a server that derives it from mtime and size can miss an edit that
changed neither. cache: snapshot plus a refresh: interval is the honest
setting there.
refresh: 30s # or 5m, or 1hThe interval on which the fetches under this source are examined. A source
with no refresh: is read once at boot and never revisited on its own;
POST /reindex is the manual lever.
Examining is cheap and rebuilding is not, so they are separate: the check takes
a fingerprint — the exported commit for git, the ETag digest for WebDAV, a file
count, byte total and newest mtime for file:// — and only a fingerprint that
moved triggers a harvest. A fetch checked every 30s that has not changed in a
month does no work at all beyond the check.
A file:// fingerprint counts the catalogue and manifest files as well —
marketplace.json and plugin.json, wherever the conventions put them — so a
marketplace mounted from a volume is re-read on the same interval when you edit
it, even though those files are never themselves served.
The shortest refresh: among all sources is also what the server advertises as
its listing cache TTL. See Operations.
A fetch that fails does not fail the server. It is reported in /health under
its own key, in one of two states, and the difference matters:
-
stale— it materialised successfully at some point and a later refresh failed. The last good tree is still being served,builtandfingerprintstill describe it, anderrorsays what went wrong. A momentarily unreachable git remote must not empty a catalogue that was complete a minute ago. -
failed— nothing was ever materialised. There is no tree; only the error, on the fetch and on every plugin that reads it.
A stale fetch clears itself the next time a rebuild succeeds. Neither state changes the HTTP status code, which is 200 either way — see Operations for why a liveness probe must not read the body.
Home · Config file · Plugins · Configuration · 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