GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

4. Resource reads are bounded by the index, not by the filesystem

Context and Problem Statement

kb-mcp gained the MCP resources capability in v0.22.0. A client can now ask for kb://doc/<path> and receive a document’s text.

That raises a question the project has already answered once, for a different mechanism, and answered deliberately: what bounds a read?

ADR-0003, accepted days earlier, decided that .kb-mcpignore bounds indexing and not accessget_document returns any file under kb_path whose extension is registered, whether or not the index contains it, and document_in_excluded_dir_is_still_readable pins that. The stated reason: a rule that lives inside the tree cannot be the thing that guards the tree, because whoever can write into the knowledge base can delete it.

resources/read could inherit that contract unchanged, or it could be narrower. The question is not academic: a resource is something the server offered, so a client asking for one is doing something different from a caller who already knows a path and asks get_document for it.

Decision Drivers

Considered Options

  1. The same contract as get_document — any file under kb_path with a registered extension, whether indexed or not.
  2. Index membership first, then exactly get_document’s guards.
  3. get_document’s guards plus a live .kb-mcpignore check on every read.

Decision Outcome

Option 2: a document is served as a resource only if it is in the index, and then only through the same guards get_document applies.

Option 3 is rejected outright. It rebuilds the boundary ADR-0003 declined, on reasoning that has not changed in the days since, and it would make the answer to “can I read this?” depend on a file any writer of the knowledge base can delete.

Option 2 is narrower than option 1, so it cannot widen what is reachable. Its justification is also materially different from the one ADR-0003 rejected: it does not trust a file inside the knowledge base to police the knowledge base. It trusts kb-mcp’s own database — state the server built and owns.

The distinction that makes it right rather than merely safe is what a resource is. get_document answers a caller who already knows the path; the contract there is “anything under kb_path is readable, so keep secrets outside it”. resources/read answers a caller holding a URI this server handed out. Serving a URI that was never on offer is not the same operation, and bounding it by the offer is the natural contract rather than a restriction bolted on.

It also makes resources/list honest — but only if the listing is built from what a read will actually accept, which is not the raw index. Narrowing [parsers].enabled without reindexing deliberately keeps the rows for the dropped extensions, and the extension check inside the shared guard then refuses them. A listing built on index membership alone would therefore hand out kb://doc/… links that the very next call rejects. So both the listing and the read go through one query, servable_document_paths() — the indexed paths minus those the active registry cannot open. “The listing offers what a read accepts” is a property of a single list or of neither.

Consequences