MCP clients¶
This page is for anyone connecting groundkit to an AI assistant — Claude Desktop or Claude Code — so the assistant can search your documents itself and cite exactly where an answer came from, instead of answering from memory alone. MCP (Model Context Protocol) is the open standard that lets an assistant call an external tool like groundkit this way; you don't need to understand the protocol itself to use this page, just to follow the configuration below. By the end, your assistant will be able to run groundkit searches directly.
groundkit ships a real MCP server, not a demo of one. grk serve-mcp speaks
stdio — it exchanges messages over your computer's own standard
input/output pipes rather than a network connection, which is how most
desktop AI clients expect a local tool to work — and grk serve mounts the
same four operations over streamable HTTP (those same operations,
reachable as an ordinary network connection) alongside a REST mirror
(the same four operations again, as plain HTTP endpoints for callers that
would rather not speak MCP at all). SPEC.md §9's Phase 4 done criterion is
that grk ingest ./docs && grk serve-mcp connects from Claude Desktop or
Claude Code using config someone else could follow without having seen this
repo. This page is that config.
Prerequisites¶
(see Installation for installing from a clone instead, e.g. for development).
No extra is needed for the server. FastAPI, its ASGI server (the underlying
process that actually speaks HTTP), and the MCP SDK are base runtime
dependencies, not an opt-in like dense or rerank —
ADR-0015
draws the line as "an extra may gate an option on a command, never the
command itself," and serve-mcp is the command SPEC.md §9 names as the
phase's deliverable. A plain base install is enough for a client to launch
the server.
A collection (a named, self-contained index — the unit grk ingest and
grk search both operate on) has to exist before you point a client at it —
the server never creates one:
That writes a collection named default under .groundkit/ (the same
default every request below uses when it omits a collection name). See the
Quickstart if you want dense or hybrid
retrieval available to the client too — the same "vectors have to be there
from the first ingest" rule applies here as anywhere else in groundkit.
Claude Desktop¶
Claude Desktop reads server definitions from claude_desktop_config.json:
Add a groundkit entry to mcpServers:
{
"mcpServers": {
"groundkit": {
"command": "/absolute/path/to/groundkit/.venv/bin/grk",
"args": [
"serve-mcp",
"--index-dir", "/absolute/path/to/.groundkit",
"--base-dir", "/absolute/path/to/docs"
]
}
}
}
command must be a path the client can actually execute
A bare "command": "grk" only works if grk is on the PATH that the
client process inherits — which is not the same PATH as your shell. If
groundkit lives in a virtual environment, as it does for anyone who
installed it with uv sync from a checkout, grk is on no PATH at all and
the client fails at launch. The error surfaces as the server disappearing
or "failed to start", which reads like a groundkit bug and is not one.
Use the absolute path to the entry point:
- venv (Linux/macOS):
<repo>/.venv/bin/grk - venv (Windows):
<repo>\.venv\Scripts\grk.exe uv tool install groundkitor a system-widepip install: baregrkis fine, since those put it on a PATH the client inherits.
Check with which grk / Get-Command grk. If that prints nothing, you need
the absolute path.
Use absolute paths for the arguments too. Claude Desktop launches the server with a working
directory of its own choosing, not the directory you ingested from — a
relative --index-dir .groundkit resolves against whatever that unpredictable
working directory turns out to be, not against your project. --base-dir is
required on both serve and serve-mcp: it is the containment root every
citation (a pointer to the exact character range in a source file a result
came from) is checked against before a chunk (the passage of a document that
range sits inside — documents are split into chunks so a result can point at
something small enough to check precisely) is returned, so an absolute path
there is not a style preference, it is the value the server refuses to start
without.
Restart Claude Desktop after editing the file for it to pick up the new server.
Claude Code¶
The equivalent registration from the command line:
claude mcp add groundkit --scope project -- \
/absolute/path/to/groundkit/.venv/bin/grk serve-mcp \
--index-dir /absolute/path/to/.groundkit \
--base-dir /absolute/path/to/docs
The same command rule from the Claude Desktop section applies: the executable
after -- must be a path the client can run, not a name it would have to
resolve on a PATH it does not share.
--scope project writes the entry to a .mcp.json at the project root
instead of your personal, machine-local config, so a team can commit it
alongside the corpus it points at. The file claude mcp add produces is the
same shape as the Claude Desktop block above:
{
"mcpServers": {
"groundkit": {
"command": "/absolute/path/to/groundkit/.venv/bin/grk",
"args": [
"serve-mcp",
"--index-dir", "/absolute/path/to/.groundkit",
"--base-dir", "/absolute/path/to/docs"
]
}
}
}
The same absolute-path reasoning from the Claude Desktop section applies here
— .mcp.json is checked out at whatever path a teammate clones the repo to,
but the server process is still launched with an unpredictable working
directory, so the paths inside the config need to be absolute on whichever
machine runs it.
The four tools¶
This is what your assistant can actually do once it's connected. SPEC.md §1.2 names exactly four operations, and the server exposes exactly those four — nothing more, nothing fewer:
| Tool | What it does | Returns |
|---|---|---|
search |
Runs a query against one collection — BM25, dense, or hybrid — optionally reranked. | A SearchResponse: the same shape grk search --json prints, with citation-bearing results. |
fetch_chunk |
Fetches one chunk by id and re-verifies its citation by re-reading the source file, not the indexed copy. | A chunk, its citation, and a verification verdict (verified, drifted, or unresolvable) — content is included only when verified. |
list_collections |
Lists the collections present in the server's index directory. | A list of collection names. |
index_status |
Reports one collection's document and chunk counts, its embedding identity, and how often its retriever has been rebuilt. | Counts, the embedding manifest when the collection is dense, the schema version, and the staleness cache's rebuild counters (ADR-0026) — no document content, no source paths. |
Ingest is deliberately not among them. SPEC.md §1.2 is the authority
cited so this reads as a decision rather than an omission: every operation
here is read-only, and adding a write means adding an authentication story
that Phase 4 has not built (see Security posture, below). grk ingest stays
a local, operator-typed command; nothing reachable over a transport can build
or modify a collection.
Streamable HTTP¶
grk serve runs the FastAPI app and mounts the MCP streamable-HTTP transport
on it at /mcp, alongside the same four operations as REST routes under
/v1/ for callers that would rather speak plain HTTP than MCP. Point a
streamable-HTTP-capable client at http://<host>:<port>/mcp, using whatever
--host and --port you passed to grk serve (loopback by default — see
below).
Reach it by 127.0.0.1, localhost or [::1] and nothing else is required.
If you point a custom hostname at the service — a hosts entry, a local DNS
alias, a tunnel that rewrites the Host header — the request is refused with
400 (REST) or 421 (/mcp) unless that name resolves to loopback, because the
service validates the Host header on both transports
(ADR-0024). A
deliberately published bind (--allow-remote-access) accepts any Host. Note
the matcher is case-sensitive and does not strip a trailing root dot, so
LOCALHOST and localhost. are refused where localhost is accepted.
Both transports — stdio from grk serve-mcp and streamable HTTP from
grk serve — dispatch through the same operation registry
(ADR-0014
decision 5), so a client talking to either one is exercising the same four
operations against the same running collections. What differs is framing,
error rendering, and — only for the HTTP path — the socket it binds.
Security posture¶
Phase 4 ships no authentication of any kind
Every operation above is read-only, and that emptiness is what lets the
shared-secret header SPEC.md §7 asks for be skipped rather than built:
there is nothing mutating to guard. Two controls stand in place of a
credential, and neither is one. The bind decides who can open a
connection: by default 127.0.0.1, and it refuses anything else unless
you pass --allow-remote-access — a flag that does exactly what it says,
publishing your document content and absolute filesystem paths to anyone
who can reach the port. Host validation decides which requests are
answered once a connection exists, which is what stops a web page using
your own browser to read a loopback port it could never route to
(ADR-0024).
A published bind switches the second off deliberately, since it protects
nobody who could already connect directly.
search responses carry source text; index_status and
list_collections carry collection topology. None of it is behind a
credential.
There is no ingest and no delete over either transport, MCP or REST. Everything reachable from a client is a read.
Read ADR-0014
for the full reasoning, and Security and
Known limitations for the rest of the posture. If you
need remote access, put a real reverse proxy with authentication in front of
the loopback bind rather than passing --allow-remote-access directly to the
internet — this server was not designed to be the thing facing untrusted
callers.
Troubleshooting¶
--base-dir is required and the server will not start without it, on
either serve or serve-mcp. It is the containment root
fetch_chunk's citation-verification step checks every resolved path
against. A service that could not verify a citation would be shipping the
opposite of groundkit's product claim, so there is no default to fall back
to.
--index-dir must already exist, and the server never creates it. Point
it at a directory you have already run grk ingest against. If the client
reports the server exited immediately, check that the directory exists and
is spelled the same way it was ingested — a fresh empty directory is not the
same as a missing one, but a nonexistent one at startup is refused rather
than silently created for you.
Asking for a collection that does not exist returns not-found, not a new,
empty collection. list_collections is the way to check what is actually
there before calling search, fetch_chunk, or index_status on a name you
are not certain about.
If the client reports it cannot launch grk at all, the process is
started with a different PATH than your interactive shell has — see the
command warning in the Claude Desktop section above. This is the single most
common setup failure, and it is indistinguishable from a server crash in most
clients' error reporting.
Test the command line before involving a client. Every launch failure above
is reproducible from a terminal, where the error is legible instead of being
reported as "server failed to start". Run the exact command and args from
your config by hand and send it one frame:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
| /absolute/path/to/.venv/bin/grk serve-mcp --index-dir /abs/.groundkit --base-dir /abs/docs
A JSON object on stdout naming groundkit means the server is fine and the
problem is in the client config. Anything else — a traceback, a "not found", or
silence — is the real error the client was hiding. Note that stdout carries
JSON-RPC frames and nothing else: logs go to stderr, so any non-JSON line on
stdout is itself a bug worth reporting.
Next¶
Deployment covers running grk serve as a long-lived,
network-reachable service instead of a client-launched local process — and
the authentication gap this page's Security posture section names is the
first thing to read before doing that. Retrieval modes
covers which mode — bm25, dense, or hybrid — is right for what your
assistant will be searching.