Skip to content

Commit a99e640

Browse files
committed
fix(search): bind meeting pagination and preserve read scopes
1 parent 986eaed commit a99e640

13 files changed

Lines changed: 406 additions & 51 deletions

File tree

‎apps/docs/content/docs/search/google-meet.mdx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Search recent conference transcripts and find generated meeting not
55

66
## Connect
77

8-
An administrator enables **Google Meet → Member accounts** under **Settings → Sources**. Each person connects Google Meet under **Integrations**. Sim uses the existing Google OAuth client and the connected member's Meet permissions. No service account is required.
8+
An administrator enables **Google Meet → Member accounts** under **Settings → Sources**. In **Integrations**, click **Connect** beside Google Meet, sign in to your Google account, and approve the requested access. Return to Sim and confirm your account appears on the row. Sim uses the existing Google OAuth client and your Meet permissions. No service account is required.
99

1010
For self-hosted deployments, enable the **Google Meet REST API** in the Google Cloud project used by Sim, configure `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`, and register:
1111

@@ -32,3 +32,13 @@ Reads retrieve complete finalized transcripts within the request, entry and byte
3232
Connect **Google Drive** to read saved meeting notes and transcripts, including older documents that remain in the organizer's Drive. Use Drive's native query syntax, such as `fullText contains 'rollback' and mimeType = 'application/vnd.google-apps.document'`, then read the result. Drive dates mean file modification time, not the meeting date. Normal document sharing and retention rules apply.
3333

3434
Use **Google Calendar** to search scheduled meetings and invitations. An event on the calendar does not establish that a Meet transcript or recording exists.
35+
36+
## Disconnect and troubleshoot
37+
38+
To disconnect, open **Integrations**, use the **…** menu beside Google Meet, choose **Disconnect** for your account, and confirm. Any workflows using that connection also lose access. This does not delete transcripts or notes from Google Drive.
39+
40+
- **Connect is unavailable or permission is denied:** ask your Sim administrator to enable the source and finish Google OAuth setup. Your Google Workspace administrator may need to approve the app.
41+
- **Reconnect needed:** use **Reconnect** beside Google Meet and authorize the account again.
42+
- **No matching transcript:** narrow to the meeting's dates or space, confirm transcription was enabled, and check whether the conference is within the API's 30-day window. Use Google Drive for saved or older documents and the bodies of smart notes.
43+
44+
For help, contact [help@sim.ai](mailto:help@sim.ai).

‎apps/docs/content/docs/search/index.mdx‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,27 @@ In Sources, open an integration to manage its connection and resource settings.
3232

3333
## Connector guides
3434

35-
These nine providers support live Search. Google Docs, Sheets, and Slides are accessed through Drive. Other [knowledge-base connectors](/knowledgebase/connectors) do not automatically become Search integrations.
35+
These providers support live Search. Google Docs, Sheets, and Slides are accessed through Drive. Other [knowledge-base connectors](/knowledgebase/connectors) do not automatically become Search integrations.
3636

3737
| Source | Search path | Modes |
3838
| --- | --- | --- |
3939
| [Coda](/search/coda) | Personal Coda MCP; legacy REST connections search document titles | Member or service |
4040
| [Confluence](/search/confluence) | Confluence Cloud CQL and content APIs | Member or service |
41-
| [GitHub](/search/github) | GitHub issue, code, and repository search | Member or GitHub App |
41+
| [Fireflies](/search/fireflies) | Meeting titles and spoken transcripts | Member only |
42+
| [GitHub](/search/github) | Repositories, code, issues and pull request discussions | Member or GitHub App |
4243
| [GitLab](/search/gitlab) | Configured self-managed project's search and read APIs | Service only; admin or CSV permissions |
43-
| [Gmail](/search/gmail) | Gmail message search and message reads | Member or service |
44+
| [Gmail](/search/gmail) | Message search and conversation reads | Member or service |
4445
| [Google Calendar](/search/google-calendar) | Calendar lists and event APIs | Member or service |
45-
| [Google Drive](/search/google-drive) | Drive search plus supported file reads/exports | Member or service |
46+
| [Google Drive](/search/google-drive) | File search, supported document reads and comments | Member or service |
47+
| [Google Meet](/search/google-meet) | Recent conference transcripts and generated-note links | Member only |
48+
| [Granola](/search/granola) | Semantic meeting search with source notes and transcript reads | Member only |
49+
| [HubSpot](/search/hubspot) | Contacts, companies, deals and tickets | Member only |
4650
| [Jira](/search/jira) | Jira Cloud JQL and issue APIs | Member only |
51+
| [Linear](/search/linear) | Issues and their comment discussions | Member only |
52+
| [Lucid](/search/lucid) | Lucidchart diagrams and Lucidspark boards | Member only |
53+
| [Notion](/search/notion) | Page and database content through Notion MCP | Member only |
4754
| [Slack](/search/slack) | Slack real-time search with the member's user token | Member only |
55+
| [Zoom](/search/zoom) | Past meetings with available transcripts, notes and summaries | Member only |
4856

4957
[Generic Secrets](/search/generic-secrets) is also available as a source, but does not add searchable documents. Organization mode makes its secrets available across the organization; Member mode lets each person manage their own secrets in Integrations. Build and Plan can use these secrets for requests; Search cannot mount them.
5058

‎apps/docs/content/docs/search/zoom.mdx‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ description: Search past meetings, transcripts, personal notes and AI summaries
55

66
## Connect
77

8-
An administrator enables **Zoom → Member accounts** under **Settings → Sources**. Each person connects Zoom under **Integrations**, using their own Zoom account. Their Zoom administrator may need to approve the app first. Current Zoom permissions determine which meetings and artifacts they can read.
8+
An administrator enables **Zoom → Member accounts** under **Settings → Sources**. Then each person connects their own account:
9+
10+
1. Open **Integrations** in Sim and click **Connect** beside Zoom.
11+
2. Sign in to Zoom, review the requested read permissions, and authorize the app. Your Zoom administrator may need to approve it first.
12+
3. Return to Sim and confirm your account appears on the Zoom row. Current Zoom permissions determine which meetings and artifacts you can read.
13+
14+
If connection fails, see [Troubleshooting](#troubleshooting).
915

1016
Sim uses the official [Zoom Meetings MCP server](https://developers.zoom.us/docs/mcp/zoom-meetings-mcp-server/). Search uses a separate General OAuth app registration from workflow actions. Zoom reauthorization can replace or narrow an existing user/app grant, so sharing the workflow client would risk disconnecting existing workflows. A Zoom workflow connection does not authorize Search.
1117

@@ -21,10 +27,29 @@ The OAuth exchange uses PKCE and `client_secret_basic`. Search requests only tho
2127

2228
## Search and read
2329

30+
On **Home**, ask a question such as “What did we decide about deployment rollback last week? Search Zoom and cite the transcript.” Follow a result’s source link to open it in Zoom.
31+
2432
Use short plain keywords such as `deployment rollback`. Zoom matches meeting topics, agendas and available meeting content. Results identify past meeting occurrences by UUID, rather than the recurring meeting number. Use `kind: meeting` when sending multiple native queries to one account.
2533

2634
`startDate` and `endDate` filter actual meeting start time. The end is exclusive. Continue with `nextCursor` using the same account, query and filters; Zoom's page token expires after 15 minutes. Each page verifies at most 10 candidates. Sorting the returned candidates does not establish the globally newest or oldest match.
2735

2836
Read a result for available timestamped transcripts, personal notes and separately labeled AI-generated summaries. Generated summaries and notes are not verbatim speech. Sim does not download recordings or transcribe audio. Recording, transcription and AI Companion settings, licenses, processing state and sharing permissions determine which artifacts exist.
2937

3038
Boolean/field operators, project selection, ownership filters and modification-date filters are unsupported. An absent artifact does not prove a meeting had no discussion. Oversized or malformed reads fail explicitly; provider failures do not appear as a successful search with no matches.
39+
40+
## Disconnect
41+
42+
In Sim **Integrations**, open the **…** menu beside Zoom, choose **Disconnect** for your account, and confirm. This stops that connection from being used in this organization. Any workflows using the same connection also lose access; other people's connections are unaffected.
43+
44+
To remove the authorization from Zoom too, open **Zoom App Marketplace → My Library**, find the Sim app you authorized for Search, open its **More** menu, and choose **Remove**, then confirm. See [Zoom's removal instructions](https://support.zoom.com/hc/en/article?id=zm_kb&sysparm_article=KB0062865); your Zoom administrator may control app removal.
45+
46+
Disconnecting does not delete meetings or recordings in Zoom or erase existing Sim conversations. See [Sim's Privacy Policy](https://www.sim.ai/privacy) for data handling and deletion requests.
47+
48+
## Troubleshooting
49+
50+
- **Connect is unavailable:** ask your Sim administrator to enable the source and finish the Zoom app configuration. Ask your Zoom administrator about app approval if authorization is blocked.
51+
- **Reconnect needed:** use **Reconnect** on the Zoom row in Integrations and authorize the same account again.
52+
- **Missing meeting or text:** confirm you can open it in Zoom, try a distinctive topic and date range, and check whether recording, transcription or AI summary processing has finished. Available content depends on the meeting's settings and your permissions.
53+
- **Expired cursor or changed content:** run the search again before continuing the result. Zoom page tokens expire after 15 minutes.
54+
55+
For help, contact [help@sim.ai](mailto:help@sim.ai).

‎apps/sim/lib/api/contracts/mothership-assistant-tools.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,15 @@ export const nativeSearchQuerySchema = z
6161
})
6262
.strict()
6363
.superRefine((input, context) => {
64-
if (input.kind && hasSearchKinds(input.provider)) {
65-
const kinds = PROVIDER_KIND_SCHEMAS[input.provider]
66-
if (!kinds.safeParse(input.kind).success)
64+
if (input.kind) {
65+
const kinds = hasSearchKinds(input.provider) ? PROVIDER_KIND_SCHEMAS[input.provider] : null
66+
if (!kinds?.safeParse(input.kind).success)
6767
context.addIssue({
6868
code: 'custom',
6969
path: ['kind'],
70-
message: `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`,
70+
message: kinds
71+
? `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`
72+
: `${input.provider} does not support kind selection.`,
7173
})
7274
}
7375
if ((input.provider === 'notion' || input.provider === 'lucid') && !input.query)

‎apps/sim/lib/credential-groups/__integration__/hubspot-mcp.integration.ts‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ import {
3030
import { createManagedMcpAuthProvider } from '@/lib/mcp/application/managed-auth-provider'
3131
import * as oauth from '@/lib/mcp/oauth/auth'
3232
import { createCoordinatedMcpOauthFetch } from '@/lib/mcp/oauth/coordinated-fetch'
33-
import { loadPreregisteredClient, McpOauthRedirectRequired } from '@/lib/mcp/oauth/provider'
33+
import {
34+
loadPreregisteredClient,
35+
McpOauthRedirectRequired,
36+
SimMcpOauthProvider,
37+
} from '@/lib/mcp/oauth/provider'
3438
import { getOrCreateOauthRow, saveClientInformation } from '@/lib/mcp/oauth/storage'
3539
import { mcpService } from '@/lib/mcp/service'
3640

@@ -248,6 +252,75 @@ describe.each(SHARED_CLIENTS)('$name shared member connector', (connector) => {
248252
Object.assign(env, { [connector.clientSecretKey]: 'rotated-secret' })
249253
await expect(runtime(id)).rejects.toThrow(/authorization/)
250254
})
255+
if (connector.id === 'zoom') {
256+
it.each(['initial consent', 'runtime scope challenge'] as const)(
257+
'restricts generic OAuth %s to registered read permissions',
258+
async (phase) => {
259+
const { mcpServer } = await create()
260+
const issuer = 'https://oauth.fixture.test'
261+
const loadProvider = async () =>
262+
new SimMcpOauthProvider({
263+
row: await getOrCreateOauthRow({ mcpServerId: mcpServer.id, organizationId: org }),
264+
preregistered: await loadPreregisteredClient(mcpServer.id),
265+
})
266+
const provider = await loadProvider()
267+
const fetchFn: typeof fetch = async (request) => {
268+
const url = new URL(
269+
typeof request === 'string' ? request : request instanceof URL ? request : request.url
270+
)
271+
if (url.href === connector.url)
272+
return new Response(null, {
273+
status: 403,
274+
headers: {
275+
'www-authenticate':
276+
'Bearer error="insufficient_scope", scope="meeting:write:meeting"',
277+
},
278+
})
279+
if (url.pathname.includes('oauth-protected-resource'))
280+
return Response.json({
281+
resource: connector.url,
282+
authorization_servers: [issuer],
283+
scopes_supported: [...connector.scope.split(' '), 'meeting:write:meeting'],
284+
})
285+
if (
286+
url.pathname.includes('oauth-authorization-server') ||
287+
url.pathname.includes('openid-configuration')
288+
)
289+
return Response.json({
290+
issuer,
291+
authorization_endpoint: `${issuer}/authorize`,
292+
token_endpoint: `${issuer}/token`,
293+
response_types_supported: ['code'],
294+
code_challenge_methods_supported: ['S256'],
295+
token_endpoint_auth_methods_supported: [connector.tokenAuthMethod],
296+
})
297+
throw new Error(`Unexpected OAuth fixture request: ${url.origin}${url.pathname}`)
298+
}
299+
try {
300+
if (phase === 'initial consent') {
301+
await oauth.mcpAuthGuarded(provider, { serverUrl: connector.url, fetchFn })
302+
} else {
303+
await provider.saveTokens({ access_token: 'fixture-access', token_type: 'Bearer' })
304+
const request = createCoordinatedMcpOauthFetch(
305+
{ credentialId: mcpServer.id, loadProvider, initialProvider: provider },
306+
{ serverUrl: connector.url, fetch: fetchFn }
307+
)
308+
await request(connector.url, { method: 'POST' })
309+
}
310+
throw new Error('Expected authorization to require consent')
311+
} catch (error) {
312+
if (!(error instanceof McpOauthRedirectRequired)) throw error
313+
const authorization = new URL(error.authorizationUrl)
314+
expect(authorization.searchParams.get('scope')).toBe(connector.scope)
315+
expect(authorization.searchParams.get('code_challenge_method')).toBe('S256')
316+
const storedProvider = await loadProvider()
317+
expect(
318+
Buffer.from(sha256Hex(await storedProvider.codeVerifier()), 'hex').toString('base64url')
319+
).toBe(authorization.searchParams.get('code_challenge'))
320+
}
321+
}
322+
)
323+
}
251324
it('binds the public OAuth round trip to the shared client and rejects rotation before exchange', async () => {
252325
const { mcpServer } = await create()
253326
const token = generateId()

‎apps/sim/lib/mcp/oauth/auth.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
22
import { ManagedMcpOauthProvider } from '@/lib/mcp/oauth/managed-provider'
3+
import { SimMcpOauthProvider } from '@/lib/mcp/oauth/provider'
34
import { createSsrfGuardedMcpFetch } from '@/lib/mcp/pinned-fetch'
45

56
type McpAuthOptions = Parameters<typeof auth>[1]
@@ -18,7 +19,8 @@ export function mcpAuthGuarded(
1819
): ReturnType<typeof auth> {
1920
return auth(provider, {
2021
...options,
21-
...(provider instanceof ManagedMcpOauthProvider && provider.authorizationScope
22+
...((provider instanceof ManagedMcpOauthProvider || provider instanceof SimMcpOauthProvider) &&
23+
provider.authorizationScope
2224
? { scope: provider.authorizationScope }
2325
: {}),
2426
fetchFn: options.fetchFn ?? createSsrfGuardedMcpFetch({ serverUrl: String(options.serverUrl) }),

‎apps/sim/lib/mcp/oauth/provider.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export class SimMcpOauthProvider implements OAuthClientProvider {
6464
this.preregistered = preregistered
6565
}
6666

67+
/** Deployment registrations may restrict consent even when discovery advertises more tools. */
68+
get authorizationScope(): string | undefined {
69+
return this.preregistered?.scope
70+
}
71+
6772
get redirectUrl(): string {
6873
return `${getBaseUrl().replace(/\/$/, '')}/api/mcp/oauth/callback`
6974
}

‎apps/sim/lib/mothership/generated/sim-assistant-tools.generated.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ export const nativeSearchQuerySchema = z
8181
})
8282
.strict()
8383
.superRefine((input, context) => {
84-
if (input.kind && hasSearchKinds(input.provider)) {
85-
const kinds = PROVIDER_KIND_SCHEMAS[input.provider]
86-
if (!kinds.safeParse(input.kind).success)
84+
if (input.kind) {
85+
const kinds = hasSearchKinds(input.provider) ? PROVIDER_KIND_SCHEMAS[input.provider] : null
86+
if (!kinds?.safeParse(input.kind).success)
8787
context.addIssue({
8888
code: 'custom',
8989
path: ['kind'],
90-
message: `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`,
90+
message: kinds
91+
? `${input.provider} kind must be one of: ${kinds.options.join(', ')}.`
92+
: `${input.provider} does not support kind selection.`,
9193
})
9294
}
9395
if ((input.provider === 'notion' || input.provider === 'lucid') && !input.query)

0 commit comments

Comments
 (0)