Previously buildInstructions emitted one line per collection with name,
doc count, and description, which can run to ~1.5 KB for a dozen
collections and is injected into every session's system prompt at
MCP initialize. Most agents never query qmd in a given session, so the
catalogue lines are a recurring token cost for static info that the
existing 'status' tool already exposes on demand.
Now emit a single comma-joined names line plus a hint that 'status'
returns the rest. listContexts() lookup is dropped since the per-
collection descriptions are no longer rendered.
Refs #647
The query tool description tells agents to compute fromLine = line - 20
for context around a hit. For hits in lines 1 through 20 that yields a
negative fromLine, which propagated unchanged through:
MCP get handler -> store.getDocumentBody -> Array.prototype.slice
A negative slice start offsets from the end of the array rather than
clamping to the beginning, so a top-of-file hit on a long document
returned an empty string and on a short document returned content from
the wrong region (e.g. lines 11-30 of a 30-line file in response to a
request for the head of the document). The lineNumbers branch was the
same shape: addLineNumbers(text, -19) emitted "-19:", "-18:" prefixes.
Same buggy slice lived in the CLI getDocument path independently.
Fix in three layers, plus the docstring:
- src/mcp/server.ts: clamp parsedFromLine to >= 1 after parsing input
args and the :line suffix, before it reaches getDocumentBody and
addLineNumbers. Also tighten the query tool's recommendation to
`fromLine = max(1, line - 20)` so following the docstring literally
produces a valid value.
- src/cli/qmd.ts: same clamp on the CLI getDocument fromLine after
the colon-suffix parse.
- src/store.ts: defensive Math.max(0, ...) on the slice start in
getDocumentBody so SDK callers and any future entry points are
protected without relying on every caller remembering to clamp.
- test/store.test.ts: regression test on getDocumentBody with
fromLine = -19 returns the head of the document, not the tail.
- test/cli.test.ts: regression test on `qmd get --from -19` matches
the no-flag baseline (head of document).
The MCP `query` tool, HTTP `/query` endpoint, and CLI `qmd query`
all returned chunk-local line numbers in their snippet output, so
the line could not be passed back to `qmd_get` as `fromLine`
without an out-of-band lookup. Pass the full document body plus
`bestChunkPos` to `extractSnippet` instead of the chunk text alone
so it can compute absolute line offsets while still scoping the
keyword scan to the reranker-chosen chunk window (preserves #149).
Also restores documented behavior of `qmd query --full`, which was
emitting the best chunk (~3.6KB max) instead of the full document.
extractSnippet now also falls back to a full-body scan when given a
chunkPos but the chunk window contains no positive matches. The
upstream chunk selector leaves bestIdx=0 as its initialization
default whenever scoring fails to find a winner (e.g. queryTerms
filtered to empty by the length>2 guard, or semantic-only matches
with no lex overlap), so an unconditional chunk-scoped scan would
land on chunk 0 instead of where the actual match lives.
- src/mcp/server.ts: SearchResultItem gains `line: number`; both MCP
and HTTP `/query` handlers populate it
- src/cli/qmd.ts: OutputRow.body now sources from r.body
- src/store.ts: extractSnippet falls back to full-body scan when
chunk-scoped pass finds no positive match
- test/mcp.test.ts: new fixture asserts absolute line 301 for a
marker placed past the first chunk boundary
- test/store.test.ts: regression test for the bestScore<=0 fallback