Skip to content

Serve the docs to an agent

An agent that answers from your docs, or edits them, reads passages. manni graph serve hands it each passage with its proof. The proof says whether its pins hold, when it last changed, and how its evals went. The server speaks the Model Context Protocol (MCP) on stdio, so any MCP client can start it.

This is Devin’s page. The outcome: an agent asks one question, and it gets every passage that answers it, with the proof to quote it or flag it.

You need two things in the repository the agent works in.

A chunk artifact. One command builds the graph and writes it, the search index and the chunks into one directory:

Terminal window
manni graph publish

The chunks are chunks.json and one row file per language. Publish the graph covers each step.

The MCP SDK. It is an optional dependency of manni, so npm installs it with manni. An install that omits optional packages leaves it out, so add it beside manni:

Terminal window
npm install @modelcontextprotocol/sdk

Add the server to the client’s MCP config. For Claude Code, that is .mcp.json at the repository root:

{
"mcpServers": {
"manni-docs": {
"command": "npx",
"args": ["manni", "graph", "serve"]
}
}
}

The client starts the server in the repository, and the server reads manni.config.yaml from there. It prints one line on stderr, then waits for calls:

Terminal window
$ manni graph serve
manni: serving lookup, retrieve, traverse and impact over stdio from graph/chunks.json.

The server opens the artifact once, at start. Restart it after a rebuild, so it serves the new text. It exits 0 when the client closes its stdin.

With --publish, the server keeps the artifact current itself. It answers the client first, then publishes when the artifact is missing or older than the checkout. Before each later call, it publishes again if any file git status lists has changed.

{
"mcpServers": {
"manni-docs": {
"command": "npx",
"args": ["manni", "graph", "serve", "--publish"]
}
}
}

A call made during a publish waits for it, so an agent never reads a passage the last edit changed. The CLI reference lists each reason it publishes.

In Claude Code, the manni plugin does this for you

Section titled “In Claude Code, the manni plugin does this for you”

The manni plugin registers this server as plugin:manni:graph and runs it with --publish. A repository with collections: needs no .mcp.json entry and no publish step. The plugin publishes into its own data directory, outside your working tree, and never beside graph.out. A local site preview serves the graph beside graph.out, so it needs manni graph publish --collection site first.

lookup is the tool to start with. It takes the question and nothing else:

{ "name": "lookup", "arguments": { "question": "How does a citation pin a claim to source lines?" } }

The result holds three sections:

  • matches: every ranked passage, with its lexical, vector and fused scores.
  • traversal: every passage reached by following links outward from the matches, with each edge followed.
  • impact: every passage that depends on the matches, with each edge.

On this repository, that call returns three matches from the cite pages. One passage is reached outward, and 21 depend on the matches. Each passage with text carries its provenance record. Abridged, the first match reads:

{
"rank": 1,
"page": "docs/src/content/docs/cite/get-started/index.mdx",
"title": "Pin a claim",
"provenance": { "pins": { "total": 13, "current": 13, "error": 0 } }
}

An agent can quote a passage whose pins are all current. It can flag one with an error pin as unverified. Before it changes behavior a page describes, it reads impact to learn which pages to update.

The result arrives as structuredContent, and again as one text block holding the same JSON. The lookup result lists every key.

The answer is compacted to fit an agent’s context and capped at graph.serve.maxChars, 40000 characters by default. Past the cap, the lowest-priority passages come back as references with no text. What an answer carries gives the rules, and graph lookup -f json prints the full result.

Three more tools give the agent control. Each runs the same core as its CLI twin, so a call finds what the command would.

ToolUse it toCLI twin
retrieveChoose the language, scope, depth and budget, or filter on metadata with where.graph retrieve
traverseWalk the graph from one passage.graph traverse --context
impactList the passages that depend on one passage.graph traverse --impact --context

The tools lists every argument.

--withhold leaves passages out of every call. drifted drops a passage with a pin at error severity. failing drops one whose current eval verdict failed.

"args": ["manni", "graph", "serve", "--withhold", "drifted,failing"]

No tool takes a withhold argument, so an agent cannot lower the floor you set. A passage left out is named in the result’s trace.withheld. When every match is left out, the result is a refusal with the reason withheld.

A refusal comes back with isError: false, and its refusal says why. A mistake the CLI reports as a usage error is a refusal here too, with the CLI’s own words in detail. An unknown node is unknown-node, for instance. The agent reads the reason and asks again.

Only an argument the tool’s schema does not name, or a value of the wrong type, returns isError: true.

Each of these exits 2 before anything is served, with one line on stderr.

MessageFix
manni: graph serve needs the optional @modelcontextprotocol/sdk package; run npm install @modelcontextprotocol/sdk.Install the SDK.
manni: graph/chunks.json does not exist; run manni graph publish.Build the artifact, as above.
manni: graph/chunks.json was built from another graph; run manni graph publish.Export the chunks again from the graph you built.