Skip to content

Commit 0739908

Browse files
committed
fix(search): scope repository retrieval and add Coda MCP OAuth
1 parent f890e5f commit 0739908

35 files changed

Lines changed: 1137 additions & 70 deletions

‎apps/sim/app/api/organizations/[id]/connected-accounts/connect/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const POST = defineInternalJsonRoute({
2020
errorPolicy: createCredentialGroupInternalErrorPolicy('Failed to connect account'),
2121
mapInput: ({ params, body }) => ({
2222
organizationId: params.id,
23-
optionId: body.optionId,
23+
...body,
2424
}),
2525
useCase: startOrganizationAccountConnection,
2626
})

‎apps/sim/app/o/[organizationId]/integrations/live-member-integrations.tsx‎

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,74 @@ export function LiveMemberIntegrations({
117117
/>
118118
)
119119
})}
120-
{options.length === 0 && (
121-
<p className='px-4 text-small'>
122-
An organization admin needs to enable account providers in Credential Groups.
123-
</p>
124-
)}
120+
{(data.credentialGroup?.status === 'active' ? data.credentialGroup.mcpServers : [])
121+
.filter(
122+
(server) =>
123+
server.enabled &&
124+
server.managedConnectorId === 'coda' &&
125+
`Coda ${server.name}`.toLowerCase().includes(search.toLowerCase())
126+
)
127+
.map((server) => {
128+
const accounts = (data.viewerMcpAccounts ?? []).filter(
129+
(account) => account.mcpServerId === server.id
130+
)
131+
return (
132+
<SettingsResourceRow
133+
key={server.id}
134+
title='Coda'
135+
description={
136+
accounts.length
137+
? accounts
138+
.map(
139+
(account) =>
140+
`${account.displayName}${account.status === 'needs_reauth' ? ' (reconnect needed)' : ''}`
141+
)
142+
.join(', ')
143+
: 'Connect your Coda account to search page contents and table rows.'
144+
}
145+
trailing={
146+
<div className='flex items-center gap-2'>
147+
<DisconnectAccountMenu
148+
organizationId={organizationId}
149+
integrationName='Coda'
150+
accounts={accounts}
151+
/>
152+
{accounts.map((account) => (
153+
<Chip
154+
key={account.credentialId}
155+
disabled={reconnect.isPending}
156+
onClick={() =>
157+
reconnect.mutate(account.credentialId, { onSuccess: navigate, onError })
158+
}
159+
>
160+
Reconnect
161+
</Chip>
162+
))}
163+
<Chip
164+
variant='primary'
165+
disabled={connect.isPending}
166+
onClick={() =>
167+
connect.mutate(
168+
{ organizationId, mcpServerId: server.id },
169+
{ onSuccess: navigate, onError }
170+
)
171+
}
172+
>
173+
{accounts.length ? 'Add account' : 'Connect'}
174+
</Chip>
175+
</div>
176+
}
177+
/>
178+
)
179+
})}
180+
{options.length === 0 &&
181+
!data.credentialGroup?.mcpServers.some(
182+
(server) => server.managedConnectorId === 'coda'
183+
) && (
184+
<p className='px-4 text-small'>
185+
An organization admin needs to enable account providers in Credential Groups.
186+
</p>
187+
)}
125188
{data.canManage && (
126189
<div className='px-4'>
127190
<ChipLink href={`/o/${organizationId}/settings/connected-accounts`}>

‎apps/sim/hooks/queries/credential-groups.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export function useUpdateCredentialGroupMcpConnector() {
235235
}: {
236236
workspaceId: string
237237
groupId: string
238-
connectorId: 'fireflies' | 'granola' | 'databricks'
238+
connectorId: 'fireflies' | 'granola' | 'databricks' | 'coda'
239239
body: ContractBodyInput<typeof updateCredentialGroupMcpConnectorContract>
240240
}) =>
241241
requestJson(updateCredentialGroupMcpConnectorContract, {
@@ -257,7 +257,7 @@ export function useDeleteCredentialGroupMcpConnector() {
257257
}: {
258258
workspaceId: string
259259
groupId: string
260-
connectorId: 'fireflies' | 'granola' | 'databricks'
260+
connectorId: 'fireflies' | 'granola' | 'databricks' | 'coda'
261261
}) =>
262262
requestJson(deleteCredentialGroupMcpConnectorContract, {
263263
params: { id: workspaceId, groupId, connectorId },

‎apps/sim/hooks/queries/organization-accounts.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
'use client'
1+
import type { StartOrganizationAccountConnectionBody } from '@/lib/api/contracts/organization-accounts'
2+
;('use client')
23

34
import {
45
isServer,
@@ -195,10 +196,13 @@ export function useUpdateOrganizationAccounts() {
195196

196197
export function useConnectOrganizationAccount() {
197198
return useMutation({
198-
mutationFn: ({ organizationId, optionId }: { organizationId: string; optionId: string }) =>
199+
mutationFn: ({
200+
organizationId,
201+
...body
202+
}: { organizationId: string } & StartOrganizationAccountConnectionBody) =>
199203
requestJson(startOrganizationAccountConnectionContract, {
200204
params: { id: organizationId },
201-
body: { optionId },
205+
body,
202206
}),
203207
})
204208
}

‎apps/sim/lib/api/contracts/credential-groups.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ export type UpdateCredentialGroupBody = z.input<typeof updateCredentialGroupBody
369369
export const createCredentialGroupMcpConnectorBodySchema = z.discriminatedUnion('connectorId', [
370370
z.object({ connectorId: z.literal('fireflies') }).strict(),
371371
z.object({ connectorId: z.literal('granola') }).strict(),
372+
z.object({ connectorId: z.literal('coda') }).strict(),
372373
z
373374
.object({
374375
connectorId: z.literal('databricks'),

‎apps/sim/lib/api/contracts/organization-accounts.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe('organization MCP provider creation contract', () => {
4646
oauthClientId: 'client-1',
4747
}
4848

49-
it.each(['fireflies', 'granola'])(
49+
it.each(['fireflies', 'granola', 'coda'])(
5050
'allows adding %s without tenant configuration',
5151
(connectorId) => {
5252
expect(schema.parse({ connectorId })).toEqual({ connectorId })

‎apps/sim/lib/api/contracts/organization-accounts.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ export const getOrganizationAccountsContract = defineRouteContract({
4747
availableProviders: z.array(credentialGroupProviderSchema),
4848
canManage: z.boolean(),
4949
indexingAvailable: z.boolean(),
50+
viewerMcpAccounts: z
51+
.array(
52+
z.object({
53+
credentialId: z.string().min(1).max(128),
54+
displayName: z.string().max(512),
55+
mcpServerId: z.string().min(1).max(128),
56+
status: z.enum(['active', 'needs_reauth']),
57+
})
58+
)
59+
.max(ORGANIZATION_VIEWER_ACCOUNT_LIMIT)
60+
.optional(),
5061
viewerAccounts: z
5162
.array(
5263
z.object({
@@ -89,9 +100,13 @@ export type OrganizationAccountConnectionResponse = z.output<
89100
typeof organizationAccountConnectionResponseSchema
90101
>
91102

92-
export const startOrganizationAccountConnectionBodySchema = z
93-
.object({ optionId: z.string().min(1, 'Account option is required').max(128) })
94-
.strict()
103+
export const startOrganizationAccountConnectionBodySchema = z.union([
104+
z.object({ optionId: z.string().min(1).max(128) }).strict(),
105+
z.object({ mcpServerId: z.string().min(1).max(128) }).strict(),
106+
])
107+
export type StartOrganizationAccountConnectionBody = z.input<
108+
typeof startOrganizationAccountConnectionBodySchema
109+
>
95110

96111
export const startOrganizationAccountConnectionContract = defineRouteContract({
97112
method: 'POST',

‎apps/sim/lib/credential-groups/application/list-mcp-connections.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ describe('listCredentialGroupMcpConnections', () => {
186186
email: 'person@example.com',
187187
mcpServerId: 'mcp-server-1',
188188
connectorId: undefined,
189-
allowedConnectorIds: ['fireflies', 'granola', 'databricks'],
189+
allowedConnectorIds: ['fireflies', 'granola', 'databricks', 'coda'],
190190
})
191191
expect(result).toEqual({
192192
mcpConnections: [

‎apps/sim/lib/credential-groups/application/organization-access.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ describe('organization workspace sharing administration', () => {
184184
optionId: 'option-1',
185185
status: 'needs_reauth',
186186
}
187+
queueTableRows(schemaMock.credential, [])
187188
queueTableRows(schemaMock.credential, [account])
188189
const result = await getOrganizationAccountsSettings.execute({
189190
principal,
@@ -224,6 +225,36 @@ describe('organization workspace sharing administration', () => {
224225
})
225226
})
226227

228+
it('starts only an enabled MCP provider belonging to the canonical organization group', async () => {
229+
queueTableRows(schemaMock.member, [{ role: 'member' }])
230+
mocks.accountsGroup.mockResolvedValue({
231+
id: 'group-1',
232+
status: 'active',
233+
options: [],
234+
mcpServers: [{ id: 'coda-server', enabled: true }],
235+
})
236+
mocks.invite.mockResolvedValue({
237+
invitationLink: 'https://sim.test/credential-groups/enroll/fixture-token',
238+
})
239+
expect(
240+
await startOrganizationAccountConnection.execute({
241+
principal,
242+
input: { organizationId: 'org-1', mcpServerId: 'coda-server' },
243+
})
244+
).toMatchObject({
245+
authorizationUrl:
246+
'https://sim.test/api/credential-groups/enroll/fixture-token/mcp/coda-server',
247+
})
248+
queueTableRows(schemaMock.member, [{ role: 'member' }])
249+
await expect(
250+
startOrganizationAccountConnection.execute({
251+
principal,
252+
input: { organizationId: 'org-1', mcpServerId: 'another-server' },
253+
})
254+
).rejects.toThrow('no longer available')
255+
expect(mocks.invite).toHaveBeenCalledTimes(1)
256+
})
257+
227258
it('does not issue a direct authorization link when enrollment access was revoked', async () => {
228259
queueTableRows(schemaMock.member, [{ role: 'member' }])
229260
mocks.accountsGroup.mockResolvedValue({

‎apps/sim/lib/credential-groups/application/organization-accounts.ts‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { resolvePrincipalAuditAttribution } from '@sim/auth/principal'
33
import { credentialGroup as credentialGroupTable } from '@sim/db/schema'
44
import { eq } from 'drizzle-orm'
5+
import type { StartOrganizationAccountConnectionBody } from '@/lib/api/contracts/organization-accounts'
56
import type { OperationUseCase } from '@/lib/core/application/operation'
67
import {
78
authorizeOrganizationOperation,
@@ -14,7 +15,10 @@ import {
1415
import { OrchestrationError } from '@/lib/core/orchestration/types'
1516
import { validateUpdateCredentialGroupInput } from '@/lib/credential-groups/application/validation'
1617
import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
17-
import { createCredentialGroupOAuthStartUrl } from '@/lib/credential-groups/enrollment-links'
18+
import {
19+
createCredentialGroupMcpOAuthStartUrl,
20+
createCredentialGroupOAuthStartUrl,
21+
} from '@/lib/credential-groups/enrollment-links'
1822
import { CredentialGroupEnrollmentError } from '@/lib/credential-groups/enrollments'
1923
import { ManagedMcpConnectorError } from '@/lib/credential-groups/managed-mcp-service'
2024
import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
@@ -30,7 +34,10 @@ import type {
3034
CredentialGroupOptionInput,
3135
UpdateCredentialGroupInput,
3236
} from '@/lib/credential-groups/types'
33-
import { listViewerOrganizationAccounts } from '@/lib/credential-groups/viewer-accounts'
37+
import {
38+
listViewerOrganizationAccounts,
39+
listViewerOrganizationMcpAccounts,
40+
} from '@/lib/credential-groups/viewer-accounts'
3441
import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
3542

3643
export const organizationAccountOperations = {
@@ -149,6 +156,13 @@ export const getOrganizationAccountsSettings = defineOrganizationAccountsUseCase
149156
const credentialGroup = await getOrganizationAccountsGroup(context.organizationId)
150157
return {
151158
credentialGroup,
159+
viewerMcpAccounts: credentialGroup
160+
? await listViewerOrganizationMcpAccounts({
161+
organizationId: context.organizationId,
162+
userId: context.userId,
163+
matching: eq(credentialGroupTable.id, credentialGroup.id),
164+
})
165+
: [],
152166
viewerAccounts: credentialGroup
153167
? await listViewerOrganizationAccounts({
154168
organizationId: context.organizationId,
@@ -225,12 +239,25 @@ export const startOrganizationAccountConnection = defineOrganizationAccountsUseC
225239
input,
226240
context,
227241
}: {
228-
input: OrganizationAccountsInput & { optionId: string }
242+
input: OrganizationAccountsInput & StartOrganizationAccountConnectionBody
229243
context: OrganizationMembershipContext
230244
}) {
231245
const group = await getOrganizationAccountsGroup(context.organizationId)
232246
if (!group || group.status !== 'active')
233247
throw new OrchestrationError('not_found', 'Ask an organization admin to set up this source')
248+
if ('mcpServerId' in input) {
249+
if (!group.mcpServers.some((server) => server.id === input.mcpServerId && server.enabled))
250+
throw new OrchestrationError('not_found', 'This account provider is no longer available')
251+
const { invitationLink } = await createViewerCredentialGroupEnrollment({
252+
organizationId: context.organizationId,
253+
userId: context.userId,
254+
credentialGroupId: group.id,
255+
})
256+
return {
257+
invitationLink,
258+
authorizationUrl: createCredentialGroupMcpOAuthStartUrl(invitationLink, input.mcpServerId),
259+
}
260+
}
234261
if (
235262
!group.options.some((option) => option.id === input.optionId && option.status === 'active')
236263
) {

0 commit comments

Comments
 (0)