Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions apps/sim/lib/sim-search/live/lucid-mcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it } from 'vitest'
import { readLucidMcp } from '@/lib/sim-search/live/lucid-mcp'
import type { ManagedSearchMcpClient } from '@/lib/sim-search/live/managed-mcp'

const ID = '0b6a3f0e-5a52-4c55-9d8c-6d2f2a1b9c11'
const COUNTS = [3, 0, 2, 1]
const TITLE = 'Fixture diagram'
const metadataRow = {
documentId: ID,
viewUrl: `https://lucid.app/lucidchart/${ID}/view`,
product: 'lucidchart',
title: TITLE,
version: 7,
pageCount: COUNTS.length,
lastModified: '2026-10-01T12:00:00Z',
}

function manifest(extra: Record<string, unknown> = {}) {
return {
document_id: ID,
edit_url: `https://lucid.app/lucidchart/${ID}/edit`,
title: TITLE,
metadata: { page_count: COUNTS.length, page_region_counts: COUNTS, ...extra },
}
}

function regionResponse(pageIndex: number, region: number) {
const pageId = `page-${pageIndex}`
return {
...manifest({ page_index: pageIndex + 1 }),
page_index: pageIndex + 1,
page_id: pageId,
text: JSON.stringify({
pages: [
{
pageIndex,
pageId,
pageTitle: `Page ${pageIndex + 1}`,
totalChunks: COUNTS[pageIndex],
requestedChunks: COUNTS[pageIndex]
? [{ chunkIndex: region, data: { label: `p${pageIndex}r${region}` } }]
: [],
},
],
}),
}
}

/** Region fetches settle in reverse request order, after a number of ticks, to expose ordering. */
function lucidClient(options: { corruptFirstRegion?: boolean } = {}) {
let inFlight = 0
let maxInFlight = 0
let regionCalls = 0
const client: ManagedSearchMcpClient = {
async call(name, args) {
if (name === 'lucid_get_document_metadata') return metadataRow
if (args.metadata_only) return manifest()
const pageIndex = Number(args.page_index) - 1
const region = Array.isArray(args.region_index) ? Number(args.region_index[0]) - 1 : 0
const call = regionCalls++
inFlight++
maxInFlight = Math.max(maxInFlight, inFlight)
for (let tick = 0; tick < 10 - call; tick++) await Promise.resolve()
inFlight--
const response = regionResponse(pageIndex, region)
return options.corruptFirstRegion && call === 0 ? { ...response, page_id: 'other' } : response
},
}
return { client, maxInFlight: () => maxInFlight, regionCalls: () => regionCalls }
}

const REFERENCE = { id: ID, kind: 'lucidchart', revision: '7' }

describe('readLucidMcp', () => {
it('fetches at most four regions at once and assembles them in page and region order', async () => {
const { client, maxInFlight, regionCalls } = lucidClient()
const document = await readLucidMcp(client, REFERENCE)
expect(regionCalls()).toBe(7)
expect(maxInFlight()).toBe(4)
const pages = JSON.parse(document.content ?? '').pages as {
pageId: string
requestedChunks: { data: { label: string } }[]
}[]
expect(pages.map((page) => page.pageId)).toEqual(['page-0', 'page-1', 'page-2', 'page-3'])
expect(pages.map((page) => page.requestedChunks.map((chunk) => chunk.data.label))).toEqual([
['p0r0', 'p0r1', 'p0r2'],
[],
['p2r0', 'p2r1'],
['p3r0'],
])
})

it('stops scheduling region fetches once a region fails validation', async () => {
const { client, regionCalls } = lucidClient({ corruptFirstRegion: true })
await expect(readLucidMcp(client, REFERENCE)).rejects.toThrow(/mismatched page regions/)
expect(regionCalls()).toBe(4)
})
})
51 changes: 34 additions & 17 deletions apps/sim/lib/sim-search/live/lucid-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,44 +322,59 @@ function manifest(value: unknown, id: string, kind: string | undefined) {
return { row, counts: counts as number[] }
}

/**
* Region fetches are independent provider reads; running them together keeps a full read inside
* the live read deadline. Validation walks them in page and region order, and a fetch is only
* scheduled once the region this many places before it has been validated, so a rejected or
* oversized document stops further reads and holds at most this many responses.
*/
const REGION_FETCH_CONCURRENCY = 4

/** Complete, bounded provider pages preserve graph data; no returned URL is fetched. */
export async function readLucidMcp(
client: ManagedSearchMcpClient,
reference: Pick<NativeDocument, 'id' | 'kind' | 'revision'>
): Promise<NativeDocument> {
const before = await metadata(client, reference.id)
const [before, initialRaw] = await Promise.all([
metadata(client, reference.id),
client.call('fetch', { id: reference.id, metadata_only: true }),
])
if (!before) invalid('Lucid document metadata is incomplete or no longer readable.')
if (
(reference.kind && reference.kind !== before.kind) ||
(reference.revision && reference.revision !== before.revision)
)
invalid('Lucid document changed since this result was issued. Search again before reading.')
const initial = manifest(
await client.call('fetch', { id: before.id, metadata_only: true }),
before.id,
before.kind
)
const initial = manifest(initialRaw, before.id, before.kind)
if (
initial.counts.length !== before.accessMetadata?.pageCount ||
initial.row.title !== before.title
)
invalid('Lucid document coverage changed before reading. Search again.')
const pages: Record<string, unknown>[] = []
const output = () => JSON.stringify({ document_id: before.id, title: before.title, pages })
for (let pageIndex = 0; pageIndex < initial.counts.length; pageIndex++) {
const count = initial.counts[pageIndex]!
const requests = initial.counts.flatMap((count, pageIndex) =>
Array.from({ length: Math.max(1, count) }, (_, region) => ({
id: before.id,
page_index: pageIndex + 1,
...(count ? { region_index: [region + 1] } : {}),
}))
)
const responses: Promise<unknown>[] = []
const schedule = (index: number) => {
if (index >= requests.length) return
const response = client.call('fetch', requests[index])
// An invalid earlier region abandons in-flight fetches; their rejections must not go unhandled.
response.catch(() => {})
responses[index] = response
}
for (let index = 0; index < REGION_FETCH_CONCURRENCY; index++) schedule(index)
let next = 0
for (const [pageIndex, count] of initial.counts.entries()) {
let assembled: Record<string, unknown> | undefined
const chunks: Record<string, unknown>[] = []
for (let region = 0; region < Math.max(1, count); region++) {
const fetched = manifest(
await client.call('fetch', {
id: before.id,
page_index: pageIndex + 1,
...(count ? { region_index: [region + 1] } : {}),
}),
before.id,
before.kind
)
const fetched = manifest(await responses[next], before.id, before.kind)
if (
fetched.counts.some((value, index) => value !== initial.counts[index]) ||
fetched.counts.length !== initial.counts.length ||
Expand Down Expand Up @@ -404,6 +419,8 @@ export async function readLucidMcp(
chunks.push(...returnedChunks)
if (Buffer.byteLength(output(), 'utf8') > MAX_CONTENT_BYTES)
invalid('Lucid document exceeds the 512 KiB complete-read limit. Open the source document.')
schedule(next + REGION_FETCH_CONCURRENCY)
next++
}
}
const after = await metadata(client, before.id)
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-explicit-any.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -1650,7 +1650,7 @@
"apps/sim/lib/sim-search/live/google-meet.ts": 1,
"apps/sim/lib/sim-search/live/granola-mcp.ts": 2,
"apps/sim/lib/sim-search/live/hubspot-mcp.test.ts": 1,
"apps/sim/lib/sim-search/live/lucid-mcp.ts": 2,
"apps/sim/lib/sim-search/live/lucid-mcp.ts": 1,
"apps/sim/lib/sim-search/live/pages.ts": 1,
"apps/sim/lib/sim-search/live/policy-schema.ts": 1,
"apps/sim/lib/sim-search/live/policy.ts": 2,
Expand Down
Loading