Skip to content

feat: optional scope path-prefix filter for query_documents (#146) - #156

Merged
shinpr merged 14 commits into
mainfrom
feature/query-documents-scope-filter
Jun 20, 2026
Merged

feat: optional scope path-prefix filter for query_documents (#146)#156
shinpr merged 14 commits into
mainfrom
feature/query-documents-scope-filter

Conversation

@shinpr

@shinpr shinpr commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an optional scope parameter to query_documents (MCP) and the query CLI subcommand that limits a search to documents under one or more absolute path prefixes. This lets a single shared database hold multiple corpora while letting a query focus on one when precision matters. Closes #146.

  • scope accepts one prefix or a list (results are unioned).
  • Matching is exact-or-descendant: a prefix matches a filePath equal to it or under it (/docs/api matches /docs/api/auth.md but not /docs/apiv2).
  • Applied as a .where() prefilter on the vector candidate prefetch; the FTS/keyword-boost branch inherits scope via its existing filePath IN (...).
  • Optional and backward compatible: omitting scope is byte-for-byte unchanged.

Design notes

  • Boundary-safe: each prefix is normalized (trailing separators stripped, posix/Windows roots handled), then built as filePath = P OR filePath LIKE 'D%' where D carries a separator boundary. Reuses the existing single-quote escaping and additionally escapes %, _, \ with ESCAPE '\'.
  • Absolute prefixes: stored filePaths are absolute, so a relative prefix matches nothing by design. The MCP tool description and shipped skill document how to obtain an absolute prefix (or omit scope).
  • Signature: VectorStore.search now takes an options object search(queryVector, { queryText?, limit?, scope? }).
  • FTS skip-on-empty: the keyword-boost branch is skipped when the scoped vector step returns zero hits (avoids a malformed filePath IN ()).
  • Input validated and trimmed at both the MCP (McpError) and CLI (process.exit) boundaries.

Surface

  • MCP: query_documents gains scope (string | string[]).
  • CLI: query gains a repeatable --scope <prefix> flag.
  • list_files / read_chunk_neighbors are intentionally out of scope for this PR.

Docs

  • Corrected and tightened all MCP tool descriptions to stand alone (return shapes documented; status description fixed to actual fields).
  • Documented scope in the shipped skills/mcp-local-rag (SKILL.md, CLI reference, result-refinement).

Testing

  • pnpm run check:all green (947 tests).
  • Real-LanceDB integration tests cover boundary exclusion, exact-file scope, trailing-separator normalization, root scopes, /- and \-style paths, multi-prefix union, injection escaping, FTS skip-on-empty, and scope-absent regression. Plus MCP/CLI validation and handler-threading tests.
  • Manually verified end-to-end via the CLI against a multi-corpus database.

🤖 Generated with Claude Code

shinpr and others added 14 commits June 21, 2026 05:41
…ject)

Change search(queryVector, queryText?, limit?) to
search(queryVector, options?: { queryText?; limit?; scope? }). Add a
private scope-predicate helper that, per prefix, normalizes trailing
separators (preserving posix root), derives the boundary separator from
the prefix (else node:path.sep), and builds an exact-or-descendant
predicate (`filePath = P OR filePath LIKE D% ESCAPE '\'`) with ' % _ \
escaped, OR'd across prefixes and applied via a single .where() on
vectorSearch only when scope is present. Scope-absent behavior is
unchanged. Add SearchOptions type and 10 real-LanceDB scope tests.

Consumer call sites (server handler, CLI) remain positional and migrate
in later tasks. Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The FTS/keyword-boost branch already inherits scope because its
`filePath IN (...)` set derives from the already-scoped vector hits.
Add a `results.length > 0` condition to the FTS guard so the branch is
skipped when the scoped vector step returns zero hits, avoiding a
malformed `filePath IN ()` predicate. Add an 8-test real-LanceDB FTS/
hybrid integration suite (in-scope-only, skip-on-empty via spy,
scope-absent regression, backslash, exact-file, root, multi-prefix,
injection). Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add scope?: string | string[] to QueryDocumentsInput and a normalizeScope
helper in parseQueryDocumentsInput, mirroring the limit defense-in-depth
pattern: accept a non-empty string or a non-empty array of non-empty
strings, reject all other shapes with McpError(InvalidParams), and
normalize to string[]. Add 14 unit tests. Handler wiring follows in a
later task. Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the handler call site to the options-object search() and pass
scope through (array-wrapped, key omitted when absent to preserve the
scope-absent path under exactOptionalPropertyTypes). Add a boundary test
asserting the scope string[] reaches search() unchanged when present,
the key is omitted when absent, and limit defaulting is preserved.
Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional scope property (oneOf string | string[]) to the
query_documents inputSchema with a description covering exact-or-descendant
path-prefix semantics. query remains the only required field. Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a repeatable --scope <prefix> flag that accumulates path prefixes
into a string[], reject empty values, and thread scope into the
options-object search() call (only when present). Document --scope in
HELP_TEXT. This migrates the last positional search() call site, so the
project type-checks clean again. Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add user-facing scope documentation to README (Searching section + CLI
example) alongside the peer limit parameter. Final QA for #146:
AC1-AC9 verified, full check:all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align the CLI --scope validation with the MCP boundary, which rejects
whitespace-only scope via trim(). A whitespace-only prefix is harmless
(fail-closed) but the asymmetry was inconsistent. Add a parseArgs test.
Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trim accepted scope values at the MCP and CLI boundaries so a
whitespace-padded prefix no longer silently matches nothing (the
whitespace-only case was already rejected). Correct the stale
buildScopePredicate comment: the FTS branch does not consume the
predicate, it inherits scope via its own filePath IN (...). Note the
posix backslash caveat in separator derivation. Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Trim the scope-feature comments that restated the code (buildScopePredicate
step narration, stripTrailingSeparators Windows paragraph, escapeLike,
SearchOptions/QueryDocumentsInput scope fields, normalizeScope, handler
array-wrap), keeping only the non-obvious rationale. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the query_documents/ingest_file/ingest_data/list_files/status return
shapes explicit, correct the status description to the actual getStatus
fields (was "database size"/"configuration information", neither returned),
note that scope must be an absolute prefix (relative matches nothing),
drop vague query-writing coaching and low-signal visualQuality detail,
and use positive "exactly one" phrasing. Necessary-and-sufficient, no
behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the scope parameter to the installable mcp-local-rag skill: --scope in
the CLI query reference, a Scope usage section and workflow/tool-table
mentions in SKILL.md, and an absolute-path check under result-refinement's
No Results. Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MCP tool descriptions must stand alone (the skill is supplementary and may
not be installed). Restore compactly: query construction guidance (preserve
specific terms / add context when vague), how to obtain an absolute scope
prefix (derive from an earlier result's filePath, or omit), and the
ingest_data format mapping (text/html/markdown). Align the shipped skill's
Scope section with the same relative-path pre-action. Part of #146.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shinpr shinpr self-assigned this Jun 20, 2026
@shinpr
shinpr merged commit 8f1bfff into main Jun 20, 2026
9 checks passed
@shinpr
shinpr deleted the feature/query-documents-scope-filter branch June 20, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: optional scope filter on query_documents to search one corpus in a shared database

1 participant