diff --git a/.changeset/grep-match-page-numbers.md b/.changeset/grep-match-page-numbers.md
new file mode 100644
index 0000000..01aded8
--- /dev/null
+++ b/.changeset/grep-match-page-numbers.md
@@ -0,0 +1,6 @@
+---
+"@ontos-ai/knowhere-sdk": patch
+"@ontos-ai/knowhere-mcp": patch
+---
+
+Copy source-chunk `pageNumbers` onto `knowledge.grepChunks` matches so clients can label citations without a follow-up `readChunks` call.
diff --git a/README.md b/README.md
index 3266cc8..b30a13a 100644
--- a/README.md
+++ b/README.md
@@ -423,6 +423,7 @@ const grep = await client.knowledge.grepChunks({
pattern: 'warranty',
maxResults: 10,
});
+// grep.matches include pageNumbers when the source chunk has them.
const serverSearch = await client.knowledge.search({
query: 'battery warranty',
diff --git a/packages/mcp/src/__tests__/mcp.test.ts b/packages/mcp/src/__tests__/mcp.test.ts
index 4e64b8a..f2eb519 100644
--- a/packages/mcp/src/__tests__/mcp.test.ts
+++ b/packages/mcp/src/__tests__/mcp.test.ts
@@ -648,6 +648,7 @@ describe('knowhere MCP wrapper', () => {
startOffset: 5,
endOffset: 12,
snippet: '2026 revenue grew',
+ pageNumbers: [4],
},
],
scannedChunks: 20,
@@ -672,7 +673,7 @@ describe('knowhere MCP wrapper', () => {
-
+
2026 revenue grew
diff --git a/packages/mcp/src/tool-result-formatter.ts b/packages/mcp/src/tool-result-formatter.ts
index f05f542..32ed231 100644
--- a/packages/mcp/src/tool-result-formatter.ts
+++ b/packages/mcp/src/tool-result-formatter.ts
@@ -423,6 +423,7 @@ function appendGrepMatch(
chunkPath: fields.chunkPath,
filePath: fields.filePath,
storageLocation: fields.storageLocation,
+ pageNumbers: fields.pageNumbers?.join(','),
startOffset: readNumber(match, 'startOffset'),
endOffset: readNumber(match, 'endOffset'),
})}>`,
diff --git a/src/knowledge/__tests__/knowledge.test.ts b/src/knowledge/__tests__/knowledge.test.ts
index 7edf1f6..586f9db 100644
--- a/src/knowledge/__tests__/knowledge.test.ts
+++ b/src/knowledge/__tests__/knowledge.test.ts
@@ -780,6 +780,68 @@ describe('Knowledge', () => {
});
});
+ it('should copy page numbers onto grep matches from the source chunk', async () => {
+ const knowledge = await createKnowledgeWithCachedResult(createPageParseResult());
+
+ const grep = await knowledge.grepChunks({
+ localDocumentId: 'local-report',
+ pattern: 'Page one',
+ maxResults: 1,
+ });
+
+ expect(grep.matches[0]).toMatchObject({
+ chunkId: 'page-1',
+ chunkType: 'page',
+ pageNumbers: [1],
+ });
+ });
+
+ it('should copy page numbers onto remote grep matches from listed chunks', async () => {
+ const cacheDirectory = await createTempDirectory();
+ const { client } = createClient(createPageParseResult());
+ const knowledge = new Knowledge(client, { cacheDirectory });
+
+ const grep = await knowledge.grepChunks({
+ documentId: 'doc_remote',
+ pattern: 'Page one',
+ maxResults: 1,
+ });
+
+ expect(grep.matches[0]).toMatchObject({
+ chunkId: 'page-1',
+ chunkType: 'page',
+ pageNumbers: [1],
+ });
+ });
+
+ it('should copy page numbers onto grep matches from parsed storage', async () => {
+ const cacheDirectory = await createTempDirectory();
+ const parseResult = createPageParseResult();
+ const storage = createInMemoryParsedStorage();
+ storage.seedResult({
+ documentId: 'doc_remote',
+ revisionKey: 'jres_remote',
+ result: parseResult,
+ });
+ const { client, documentsListChunks } = createClient(parseResult);
+ const knowledge = new Knowledge(client, { cacheDirectory }).withParsedStorage({
+ storage,
+ });
+
+ const grep = await knowledge.grepChunks({
+ documentId: 'doc_remote',
+ pattern: 'Page one',
+ maxResults: 1,
+ });
+
+ expect(grep.matches[0]).toMatchObject({
+ chunkId: 'page-1',
+ chunkType: 'page',
+ pageNumbers: [1],
+ });
+ expect(documentsListChunks).toHaveBeenCalled();
+ });
+
it('should omit useAgentic when unset so API map-nav default applies', async () => {
const cacheDirectory = await createTempDirectory();
const { client, retrievalQuery } = createClient(createParseResult());
diff --git a/src/knowledge/knowledge.ts b/src/knowledge/knowledge.ts
index a7c9003..198580c 100644
--- a/src/knowledge/knowledge.ts
+++ b/src/knowledge/knowledge.ts
@@ -456,17 +456,7 @@ export class Knowledge {
scannedChunks += 1;
const chunkMatches = matcher(chunk.content);
for (const match of chunkMatches) {
- matches.push({
- position: chunk.position,
- chunkId: chunk.chunkId,
- chunkType: chunk.chunkType,
- sectionPath: chunk.sectionPath,
- sourceChunkPath: chunk.sourceChunkPath,
- filePath: chunk.filePath,
- startOffset: match.startOffset,
- endOffset: match.endOffset,
- snippet: buildSnippet(chunk.content, match.startOffset, match.endOffset, contextChars),
- });
+ matches.push(toGrepMatch(chunk, match, contextChars));
if (matches.length >= maxResults) {
return { document, matches, scannedChunks, truncated: true };
}
@@ -753,11 +743,12 @@ export class Knowledge {
) {
continue;
}
- if (!matchesGrepScope(toIndexedRemoteChunk(chunk), params)) {
+ const indexed = toIndexedRemoteChunk(chunk);
+ if (!matchesGrepScope(indexed, params)) {
continue;
}
scannedChunks += 1;
- const chunkMatches = matcher(chunk.content ?? '');
+ const chunkMatches = matcher(indexed.content);
for (const [matchIndex, match] of chunkMatches.entries()) {
if (
response.pagination.page === startPage &&
@@ -767,22 +758,7 @@ export class Knowledge {
) {
continue;
}
- matches.push({
- position: toChunkPosition(chunk),
- chunkId: chunk.chunkId,
- chunkType: chunk.chunkType,
- sectionPath: chunk.sectionPath ?? '',
- sourceChunkPath: chunk.sourceChunkPath ?? chunk.sectionPath ?? '',
- filePath: chunk.filePath ?? undefined,
- startOffset: match.startOffset,
- endOffset: match.endOffset,
- snippet: buildSnippet(
- chunk.content ?? '',
- match.startOffset,
- match.endOffset,
- contextChars,
- ),
- });
+ matches.push(toGrepMatch(indexed, match, contextChars));
if (matches.length >= maxResults) {
const continuationCursor = createGrepContinuationCursor({
documentId,
@@ -985,17 +961,7 @@ export class Knowledge {
for (const chunk of scopedChunks) {
scannedChunks += 1;
for (const match of matcher(chunk.content)) {
- matches.push({
- position: chunk.position,
- chunkId: chunk.chunkId,
- chunkType: chunk.chunkType,
- sectionPath: chunk.sectionPath,
- sourceChunkPath: chunk.sourceChunkPath,
- filePath: chunk.filePath,
- startOffset: match.startOffset,
- endOffset: match.endOffset,
- snippet: buildSnippet(chunk.content, match.startOffset, match.endOffset, contextChars),
- });
+ matches.push(toGrepMatch(chunk, match, contextChars));
if (matches.length >= maxResults) {
return {
document: createStoredKnowledgeDocument({
@@ -2325,6 +2291,27 @@ function toReadChunk(chunk: IndexedKnowledgeChunk): KnowledgeReadChunk {
};
}
+function toGrepMatch(
+ chunk: IndexedKnowledgeChunk,
+ match: KnowledgeGrepMatchOffset,
+ contextChars: number,
+): KnowledgeGrepMatch {
+ return {
+ position: chunk.position,
+ chunkId: chunk.chunkId,
+ chunkType: chunk.chunkType,
+ sectionPath: chunk.sectionPath,
+ sourceChunkPath: chunk.sourceChunkPath,
+ filePath: chunk.filePath,
+ startOffset: match.startOffset,
+ endOffset: match.endOffset,
+ snippet: buildSnippet(chunk.content, match.startOffset, match.endOffset, contextChars),
+ ...(chunk.pageNumbers && chunk.pageNumbers.length > 0
+ ? { pageNumbers: [...chunk.pageNumbers] }
+ : {}),
+ };
+}
+
function matchesGrepScope(chunk: IndexedKnowledgeChunk, params: KnowledgeGrepParams): boolean {
if (params.chunkType && chunk.chunkType !== params.chunkType) {
return false;
diff --git a/src/knowledge/types.ts b/src/knowledge/types.ts
index 64e49ca..e889a81 100644
--- a/src/knowledge/types.ts
+++ b/src/knowledge/types.ts
@@ -206,6 +206,8 @@ export interface KnowledgeGrepMatch {
startOffset: number;
endOffset: number;
snippet: string;
+ /** Copied from the source chunk when the indexed chunk has page numbers. */
+ pageNumbers?: number[];
}
export interface KnowledgeGrepResponse {