Skip to content

Commit 9ff3c55

Browse files
committed
improvement(search): return a longer live read window when an MCP client asks for more chunks
1 parent 12ca9d6 commit 9ff3c55

4 files changed

Lines changed: 70 additions & 3 deletions

File tree

‎apps/sim/lib/knowledge/mcp/server.protocol.test.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,15 @@ describe('Search MCP protocol', () => {
9393
expect(JSON.parse(searchContent.text)).toMatchObject({
9494
results: [{ documentId, queryIndexes: [0] }],
9595
})
96-
const read = await client.callTool({ name: 'read_document', arguments: { documentId } })
96+
const read = await client.callTool({
97+
name: 'read_document',
98+
arguments: { documentId, limit: 8 },
99+
})
97100
expect(read.isError).not.toBe(true)
101+
expect(mocks.liveRead.mock.lastCall?.[0].input).toMatchObject({
102+
limit: 8,
103+
scaleWindowWithLimit: true,
104+
})
98105
expect(read.content).toEqual([
99106
{ type: 'text', text: expect.stringContaining('Read evidence') },
100107
])

‎apps/sim/lib/knowledge/mcp/server.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export function createKnowledgeMcpServer(context: KnowledgeMcpContext): McpServe
298298
...options,
299299
documentId: id,
300300
organizationId,
301+
scaleWindowWithLimit: true,
301302
resultSecretRegistry: registry,
302303
signal,
303304
},

‎apps/sim/lib/sim-search/live/application.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,51 @@ describe('authorized live retrieval', () => {
11891189
})
11901190
).rejects.toBe('user_stop:test')
11911191
})
1192+
describe('read window', () => {
1193+
const content = 'x'.repeat(30_000)
1194+
async function readWindow(extra: Record<string, unknown>) {
1195+
const [result] = (await searchLiveKnowledge.execute({ principal, input })).results
1196+
if (!result) throw new Error('Expected the search fixture to return a result')
1197+
mocks.read.mockResolvedValue({ ...document, content })
1198+
const read = await readLiveDocument.execute({
1199+
principal,
1200+
input: {
1201+
workspaceId: 'workspace',
1202+
documentId: result.documentId,
1203+
limit: 3,
1204+
resultSecretRegistry: new ResolvedSecretTraceRegistry([]),
1205+
...extra,
1206+
},
1207+
})
1208+
const [chunk] = read.chunks
1209+
if (!chunk) throw new Error('Expected the read to return a window')
1210+
return { length: chunk.content.length, next: read.next }
1211+
}
1212+
1213+
it('returns a proportionally longer window when a scaling caller asks for more chunks', async () => {
1214+
const { length, next } = await readWindow({ limit: 8, scaleWindowWithLimit: true })
1215+
expect(length).toBe(21_333)
1216+
expect(next).toEqual({ startChunkIndex: 0, startOffset: 21_333 })
1217+
})
1218+
1219+
it('continues a longer window from where the previous one ended', async () => {
1220+
const { length, next } = await readWindow({
1221+
limit: 8,
1222+
scaleWindowWithLimit: true,
1223+
startOffset: 21_333,
1224+
})
1225+
expect(length).toBe(30_000 - 21_333)
1226+
expect(next).toBeNull()
1227+
})
1228+
1229+
it.each([
1230+
['at the default limit', { limit: 3, scaleWindowWithLimit: true }],
1231+
['below the default limit', { limit: 1, scaleWindowWithLimit: true }],
1232+
['for a caller that does not scale', { limit: 8 }],
1233+
])('keeps the standard window %s', async (_, extra) => {
1234+
expect((await readWindow(extra)).length).toBe(8_000)
1235+
})
1236+
})
11921237
it('refuses a chunk index other than zero before reading the document from its provider', async () => {
11931238
const [result] = (await searchLiveKnowledge.execute({ principal, input })).results
11941239
if (!result) throw new Error('Expected the search fixture to return a result')

‎apps/sim/lib/sim-search/live/application.ts‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,20 @@ export const LIVE_NOT_STARTED_MESSAGE =
287287
export const LIVE_OVER_ACCOUNT_LIMIT_MESSAGE = `Not searched: one search covers at most ${MAX_ACCOUNTS} accounts. Search this source or account alone.`
288288
/** Previews center on the query's longest matching term, like indexed passages. */
289289
const PREVIEW_CHARACTERS = 1800
290-
/** Characters one read returns, the same page budget indexed document reads use. */
290+
/** Characters one read returns at the default limit, the page budget indexed document reads use. */
291291
const READ_WINDOW_CHARACTERS = 8000
292+
/** The read limit {@link READ_WINDOW_CHARACTERS} is sized for, which is the default limit. */
293+
const READ_WINDOW_LIMIT = 3
294+
295+
/**
296+
* Characters one read returns. A live document is one chunk, so a caller that scales the window
297+
* gets a longer one when it asks for more chunks than the default; at or below the default, and
298+
* for every other caller, the window stays the standard page.
299+
*/
300+
function readWindowCharacters(input: Pick<LiveReadInput, 'limit' | 'scaleWindowWithLimit'>) {
301+
if (!input.scaleWindowWithLimit || input.limit <= READ_WINDOW_LIMIT) return READ_WINDOW_CHARACTERS
302+
return Math.floor((READ_WINDOW_CHARACTERS * input.limit) / READ_WINDOW_LIMIT)
303+
}
292304

293305
/**
294306
* Verifies candidates in rank order. A revoked grant fails the whole account; any other
@@ -980,6 +992,8 @@ export type LiveReadInput = ResourceOwner & {
980992
documentId: string
981993
filters?: WorkspaceSearchFilters
982994
limit: number
995+
/** Lengthens the window for a limit above the default, as {@link readWindowCharacters} sizes it. */
996+
scaleWindowWithLimit?: boolean
983997
startChunkIndex?: number
984998
startOffset?: number
985999
resultSecretRegistry: ResolvedSecretTraceRegistry
@@ -1125,7 +1139,7 @@ export const readLiveDocument = defineAuthorizedKnowledgeUseCase({
11251139
'validation',
11261140
'This document changed. Read again from the beginning'
11271141
)
1128-
const end = Math.min(content.length, start + READ_WINDOW_CHARACTERS)
1142+
const end = Math.min(content.length, start + readWindowCharacters(input))
11291143
return {
11301144
documentId: input.documentId,
11311145
knowledgeBaseId: '',

0 commit comments

Comments
 (0)