AI and data · RFC
0001: MCP server over decisions
Accepted and implemented: see MCP server for agents.
Summary#
Ship an optional Model Context Protocol (MCP) server, inkan mcp, that reads decisions.json and the
Markdown copies of a published site (or a local dist/) and exposes them as tools: list records with filters,
read one record, follow its lineage, search by text. Agents connected to the server fetch exactly the records
they need instead of a growing llms-full.txt.
Motivation#
The current files work in two ways. llms.txt is an index an agent follows link by link; llms-full.txt is
everything at once. Both are fine for a few dozen records. With several hundred records the full file no longer
fits a context window, and following links one by one costs a round trip per record with no way to ask
“which accepted decisions mention Kafka and are tagged messaging”.
decisions.json already holds the typed metadata and a markdown link per record. What is missing is a
protocol agents speak. MCP is the one coding assistants support today; a server over the existing JSON gives
agents filters and lineage without a new data format and without a database.
Proposal#
Command#
npx @tilaven/inkan mcp --site https://decisions.acme.devnpx inkan mcp --dist ./distThe server runs over stdio, the transport every MCP client supports for local servers. --site fetches
decisions.json and .md copies over HTTP; --dist reads them from a built folder, for use in CI or offline.
The index is loaded once at start and refreshed on demand with a refresh tool.
Tools#
| Tool | Arguments | Returns |
|---|---|---|
list_decisions | project?, type?, status?, tag?, author?, since?, limit? | Matching entries of decisions.json, without bodies |
get_decision | id | The entry plus the raw Markdown body of the record |
search_decisions | query, same filters as list_decisions | Entries whose title, description or body contains the query |
lineage | id | The chain of supersedes and supersededBy around a record, plus related |
list_projects | none | Projects with name, description and record counts |
refresh | none | Reloads the index from the site or folder |
Each tool returns the same objects as decisions.json, so a client that already understands the file
understands the tool output. get_decision fetches the markdown URL of the record, which is why the server
needs nothing beyond what the site already publishes.
Resources#
The server also exposes llms.txt, AGENTS.md and each record as MCP resources, so a client that prefers
resources to tools can attach a record to the conversation by URL.
Client configuration#
{ "mcpServers": { "decisions": { "command": "npx", "args": ["-y", "@tilaven/inkan", "mcp", "--site", "https://decisions.acme.dev"] } }}Scope#
- Read-only. Writing a record stays with
inkan newand a pull request;AGENTS.mdexplains that path. - No full-text ranking beyond substring matching on the loaded bodies. The
search.jsonthe build writes for external search providers (plain-textcontentper record) could be reused later. - Shipped inside the
@tilaven/inkanpackage behind themcpcommand so no second package is needed; the MCP SDK becomes a dependency only when the command runs, if that can be arranged with a lazy import.
Open questions#
- Should the server be a separate package (
@inkan/mcp) to keep the SDK out of the main dependency tree, or a lazily imported command as proposed? search_decisionsneeds record bodies. Fetch all.mdcopies at start (simple, one request per record) or lazily per query (slower first search)?- Private sites behind SSO: pass a bearer token or cookie through an option, or support only
--distfor those? - Does
decisions.jsonneed aversionorgeneratedAtfield so the server can tell a client how fresh the index is? That would be a small change to the JSON shape. - Is
lineageworth a tool, or is it enough thatget_decisionreturnssupersedesandsupersededByand the client follows them? - Should validation of broken
supersedes/relatedreferences land first, solineagenever returns dangling ids?