GrooveSeek

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

View the Project on GitHub alphabet-h/grooveseek

3. .kb-mcpignore bounds indexing, not access, and uses ignore only as a matcher

Context and Problem Statement

Until v0.21.0 the only way to keep something out of the index was exclude_dirs, a list of whole directory basenames. There was no way to say “not drafts/*.md”, “not *.tmp.md”, or “not archive/2024/**”. The obvious shape for that is a gitignore-syntax file in the knowledge base, which is what Cursor, ripgrep and most developer tools offer.

Two questions had to be answered before writing any of it, and neither has a default that is merely a matter of taste.

First, what does the file bound? kb-mcp already had a deliberate, test-pinned answer for exclude_dirs: it means “not indexed”, not “not readable”. A file under an excluded directory never appears in search, but get_document still returns it to a caller that knows its path (validate_get_document_path takes no exclusion argument, and document_in_excluded_dir_is_still_readable pins that). A new ignore file could keep that contract or break it, and the industry is split: Cursor ships two files for exactly this distinction — .cursorindexingignore for the index, .cursorignore for access.

Second, how much of the ignore crate to take. It offers a whole directory walker (WalkBuilder) as well as a matcher (Gitignore). kb-mcp already walks with walkdir, in three separate places that have drifted apart from each other twice.

Decision Drivers

Considered Options

Scope of the file

  1. Index only, matching the existing exclude_dirs contract.
  2. Index and access: also refuse get_document / get_best_practice.
  3. Two files, one for each, as Cursor does.

Implementation

  1. ignore::WalkBuilder, replacing walkdir.
  2. ignore::gitignore::Gitignore as a matcher only, keeping walkdir.
  3. Gitignore semantics written onto the globset dependency already present.

Decision Outcome

Index only (option 1), with ignore used as a matcher only (option 2).

The scope decision follows from what the file can actually guarantee. Whoever can write into the knowledge base can also delete .kb-mcpignore; a rule that lives inside the tree cannot be the thing that guards the tree. Refusing get_document for ignored paths would look like an access control while resting on a file any writer can remove — the shape BU-20 was corrected for. One contract for both exclusion mechanisms is also simply easier to state: nothing excluded is ever indexed, and nothing indexed is the boundary on reading. Anything that must not be readable belongs outside kb_path, which is what the README has always said.

Option 3 was rejected as concept count without a matching benefit: it doubles the file, the documentation and the tests to express a distinction whose stronger half we just declined to offer.

The implementation decision is what measurement produced. WalkBuilder brings defaults that change behaviour invisibly for an existing knowledge base: hidden() is true by default, and on Windows “hidden” means dot-prefixed or carrying FILE_ATTRIBUTE_HIDDEN, so a note the user hid in Explorer would silently leave the index. add_ignore resolves against the process’s current directory rather than the walk root, which for an installed service is arbitrary. require_git, parents, git_ignore and git_global are all on by default, making behaviour depend on whether the knowledge base happens to be a git repository. And filter_entry accepts one predicate with no documented ordering against the ignore check, while kb-mcp already filters there for the hardcoded denylist, Office lock files, symlinks and hard links.

Taking the matcher alone leaves the existing walk untouched and, more importantly, allows one function to answer the exclusion question for all three surfaces — which is the failure mode this project has already paid for twice.

Option 3 (hand-rolled on globset) was rejected on the evidence that independent gitignore implementations diverge from each other in exactly the edge cases nobody tests, and on a prior internal finding: when review keeps landing edge cases on a hand-written matcher, that is the signal to delegate to a library.

Consequences