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
53 changes: 52 additions & 1 deletion apps/sim/lib/sim-search/live/fireflies-mcp.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createLogger } from '@sim/logger'
import { toArray, toRecord } from '@sim/utils/object'
import { nativeText } from '@/lib/sim-search/live/dates'
import { NativeSearchError, string } from '@/lib/sim-search/live/http'
Expand All @@ -6,7 +7,49 @@ import { boundedMeetingContent } from '@/lib/sim-search/live/meeting-content'
import type { NativeDocument, NativePage, NativeSearchInput } from '@/lib/sim-search/live/types'

const MAX_MEETING_CONTENT = 200_000
const MAX_DIAGNOSTIC_TEXT_LENGTH = 65_536
const UTC_DAY_MS = 86_400_000
const logger = createLogger('FirefliesMcpSearch', { logLevel: 'WARN' })

/** Only fixed envelope keys and value kinds may enter diagnostics, never provider keys or values. */
function responseShape(value: unknown) {
const record = toRecord(value)
const kind = (entry: unknown) =>
entry === null ? 'null' : Array.isArray(entry) ? 'array' : typeof entry
return {
type: kind(value),
fields: Object.fromEntries(
[
'transcripts',
'data',
'result',
'results',
'items',
'output',
'text',
'error',
'errors',
].flatMap((key) => (Object.hasOwn(record, key) ? [[key, kind(record[key])]] : []))
),
}
}

function responseTextShape(value: unknown) {
if (typeof value !== 'string') return undefined
if (value.length > MAX_DIAGNOSTIC_TEXT_LENGTH)
return { length: value.length, inspection: 'skipped_size_limit' }
const text = value.trim()
const fenced = text.match(/^```(?:json)?\r?\n([\s\S]*)\r?\n```$/i)?.[1]
let jsonShape: ReturnType<typeof responseShape> | undefined
try {
jsonShape = responseShape(JSON.parse(fenced ?? text))
} catch {}
return {
length: value.length,
format: fenced !== undefined ? 'code_fence' : jsonShape ? 'json' : 'text',
...(jsonShape ? { jsonShape } : {}),
}
}

function requireMeetingId(id: string): string {
if (!/^[\w-]{1,200}$/.test(id))
Expand Down Expand Up @@ -139,11 +182,19 @@ export async function searchFirefliesMcp(
const rows = Array.isArray(result)
? result
: (data.transcripts ?? toRecord(data.data).transcripts)
if (!Array.isArray(rows))
if (!Array.isArray(rows)) {
logger.warn('Unsupported Fireflies transcript list response shape', {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
response: responseShape(result),
data: responseShape(data.data),
result: responseShape(data.result),
output: responseShape(data.output),
text: responseTextShape(typeof result === 'string' ? result : data.text),
})
throw new NativeSearchError(
'unavailable',
'Fireflies returned an unsupported transcript list format.'
)
}
const documents = rows.slice(0, limit).flatMap((value) => {
const document = meetingDocument(toRecord(value))
return document ? [document] : []
Expand Down
55 changes: 55 additions & 0 deletions apps/sim/lib/sim-search/live/meeting-mcp.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getAllMockLoggers } from '@sim/testing/mocks/logger.mock'
import { describe, expect, it } from 'vitest'
import { readFirefliesMcp, searchFirefliesMcp } from '@/lib/sim-search/live/fireflies-mcp'
import { readGranolaMcp, searchGranolaMcp } from '@/lib/sim-search/live/granola-mcp'
Expand Down Expand Up @@ -157,6 +158,60 @@ describe('meeting MCP provider wire contracts', () => {
await expect(searchFirefliesMcp(client, input)).rejects.toThrow('unsupported')
})

it.each(['text', 'json', 'code_fence', 'oversized'] as const)(
'diagnoses unsupported Fireflies %s envelopes without logging private values or arbitrary keys',
async (format) => {
const privateMarker = 'PRIVATE_MEETING_OR_CREDENTIAL'
const json = JSON.stringify({ items: [{ title: privateMarker }], [privateMarker]: 'secret' })
const text =
format === 'oversized'
? JSON.stringify({ items: [privateMarker.repeat(3000)] })
: format === 'code_fence'
? ['```json', json, '```'].join('\n')
: format === 'json'
? json
: privateMarker.repeat(1000)
const client: ManagedSearchMcpClient = {
async call() {
return {
text,
data: { error: privateMarker },
result: { items: [{ title: privateMarker }] },
...Object.fromEntries(
Array.from({ length: 1000 }, (_, index) => [
`${privateMarker}_${index}`,
privateMarker,
])
),
}
},
}
await expect(searchFirefliesMcp(client, { ...input, query: privateMarker })).rejects.toThrow(
'unsupported'
)
const warnings = getAllMockLoggers().flatMap((logger) => logger.warn.mock.calls)
expect(warnings).toHaveLength(1)
expect(warnings[0]?.[1]).toMatchObject({
response: { type: 'object', fields: { text: 'string', data: 'object', result: 'object' } },
data: { type: 'object', fields: { error: 'string' } },
result: { type: 'object', fields: { items: 'array' } },
text:
format === 'oversized'
? { length: text.length, inspection: 'skipped_size_limit' }
: {
length: text.length,
format,
...(format === 'text'
? {}
: { jsonShape: { type: 'object', fields: { items: 'array' } } }),
},
})
const serialized = JSON.stringify(warnings)
expect(serialized).not.toContain(privateMarker)
expect(serialized.length).toBeLessThan(2000)
}
)

it('rejects invalid continuation offsets before Fireflies receives a query', async () => {
const client = {
async call() {
Expand Down
Loading