Skip to content

Commit c58697d

Browse files
authored
fix(search): reuse managed MCP sessions within operations (#8567)
* fix(search): reuse managed MCP sessions within operations * fix(search): close acceptance sessions and verify concurrency
1 parent 94b879d commit c58697d

10 files changed

Lines changed: 798 additions & 183 deletions

File tree

‎apps/sim/lib/mcp/service.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,18 @@ class McpService {
492492
return client
493493
}
494494

495+
/** An operation owns this unpooled client and must disconnect it in its finalizer. */
496+
async openManagedMcpSession(
497+
serverId: string,
498+
scope: ResourceScope,
499+
auth: McpOauthCredentials,
500+
signal: AbortSignal
501+
): Promise<Pick<McpClient, 'listTools' | 'callTool' | 'disconnect'>> {
502+
const config = await this.getServerConfig(serverId, scope)
503+
if (!config) throw new Error('Managed MCP server is unavailable')
504+
return this.createManagedOauthClient(config, auth, signal)
505+
}
506+
495507
async discoverManagedMcpTools(
496508
serverId: string,
497509
scope: string | ResourceScope,

‎apps/sim/lib/sim-search/live/account-session.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ResourceOwner } from '@/lib/core/resource-scope'
33
import { resourceScopeFromOwner } from '@/lib/core/resource-scope'
44
import type { PinnedConnectionPool } from '@/lib/core/security/input-validation.server'
55
import type { ResolvedLiveAccount } from '@/lib/sim-search/live/accounts'
6-
import { createCodaMcpClient, readCodaMcp, searchCodaMcp } from '@/lib/sim-search/live/coda-mcp'
6+
import { readCodaMcp, searchCodaMcp } from '@/lib/sim-search/live/coda-mcp'
77
import { readFirefliesMcp, searchFirefliesMcp } from '@/lib/sim-search/live/fireflies-mcp'
88
import { createAdminGitLabSession } from '@/lib/sim-search/live/gitlab-admin'
99
import { readGranolaMcp, searchGranolaMcp } from '@/lib/sim-search/live/granola-mcp'
@@ -39,6 +39,8 @@ export interface LiveAccountSession {
3939
policy: LiveSearchPolicy
4040
/** Service verification covered a bounded subset of the source's configured users. */
4141
servicePartial: boolean
42+
/** Releases the operation-owned provider transport after all reads and checks settle. */
43+
close(): Promise<void>
4244
search(input: NativeSearchInput): Promise<NativePage>
4345
/** True only when the document is inside the source boundary and the member may read it. */
4446
verify(document: Reference): Promise<boolean>
@@ -104,10 +106,9 @@ export async function openLiveAccountSession(
104106
'unavailable',
105107
'This provider does not support managed MCP Search.'
106108
)
107-
return provider === 'coda'
108-
? createCodaMcpClient(owner, userId, account.id, signal, input.searches)
109-
: createManagedSearchMcpClient(owner, userId, account.id, provider, signal, input.searches)
109+
return createManagedSearchMcpClient(owner, userId, account.id, provider, signal, input.searches)
110110
}
111+
const memberPolicy = livePolicyFor(input.policies, provider)
111112
const mcp = await openMcp()
112113
const searchMcp = (search: NativeSearchInput) => {
113114
if (!mcp) throw new NativeSearchError('unavailable', 'Managed MCP connection unavailable.')
@@ -176,9 +177,15 @@ export async function openLiveAccountSession(
176177
verify: (document: Reference) => verifyPolicy(document, document.accessMetadata),
177178
}
178179
}
179-
const boundary = await sourceBoundary(livePolicyFor(input.policies, provider))
180+
const boundary = await sourceBoundary(memberPolicy).catch(async (error: unknown) => {
181+
await mcp?.close()
182+
throw error
183+
})
180184

181185
return {
186+
async close() {
187+
await mcp?.close()
188+
},
182189
policy: boundary.policy,
183190
servicePartial: boundary.partial,
184191
async search(search) {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,9 @@ vi.mock('@/lib/sim-search/live/policy-store', () => ({
4444
livePolicyFor: vi.fn(() => defaultLiveSearchPolicy()),
4545
}))
4646
vi.mock('@/lib/sim-search/live/managed-mcp', () => ({
47-
createManagedSearchMcpClient: async () => ({ call: mocks.mcpCall }),
47+
createManagedSearchMcpClient: async () => ({ call: mocks.mcpCall, close: async () => {} }),
4848
}))
4949
vi.mock('@/lib/sim-search/live/coda-mcp', () => ({
50-
createCodaMcpClient: vi.fn(),
5150
searchCodaMcp: vi.fn(),
5251
readCodaMcp: vi.fn(),
5352
}))

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -607,12 +607,13 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
607607
results: [],
608608
}
609609
}
610+
let session: LiveAccountSession | undefined
610611
try {
611612
signal.throwIfAborted()
612613
const resolved = await measureSearchStage('live.resolve', () =>
613614
resolveListedLiveAccount(input, userId, account)
614615
)
615-
const session = await measureSearchStage('live.session', () =>
616+
session = await measureSearchStage('live.session', () =>
616617
openLiveAccountSession({
617618
owner: input,
618619
userId,
@@ -623,9 +624,10 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
623624
searches: natives.length,
624625
})
625626
)
627+
const currentSession = session
626628
return await Promise.all(
627629
natives.map((target) =>
628-
searchQuery(account, resolved, session, target.native, statusFor(target)).catch(
630+
searchQuery(account, resolved, currentSession, target.native, statusFor(target)).catch(
629631
(error) => failed(error, target)
630632
)
631633
)
@@ -634,6 +636,7 @@ export const searchLiveKnowledge = defineAuthorizedKnowledgeUseCase({
634636
return natives.map((target) => failed(error, target))
635637
} finally {
636638
settled.abort()
639+
await session?.close()
637640
}
638641
}
639642
let searched: SearchedQuery[]
@@ -760,8 +763,9 @@ export const readLiveDocument = defineAuthorizedKnowledgeUseCase({
760763
: AbortSignal.timeout(15_000)
761764
const pool = createPinnedConnectionPool()
762765
let document: NativeDocument
766+
let session: LiveAccountSession | undefined
763767
try {
764-
const session = await openLiveAccountSession({
768+
session = await openLiveAccountSession({
765769
owner: input,
766770
userId,
767771
resolved,
@@ -774,7 +778,10 @@ export const readLiveDocument = defineAuthorizedKnowledgeUseCase({
774778
'not_found',
775779
'Document is outside your organization’s search scope'
776780
)
777-
document = await measureSearchStage('live.read', () => session.read(reference, input.filters))
781+
const currentSession = session
782+
document = await measureSearchStage('live.read', () =>
783+
currentSession.read(reference, input.filters)
784+
)
778785
/** Readers degrade section failures to warnings, so the signal decides cancellation. */
779786
signal.throwIfAborted()
780787
const current = await session.verifyCurrent(document)
@@ -786,7 +793,11 @@ export const readLiveDocument = defineAuthorizedKnowledgeUseCase({
786793
'Document is outside your organization’s search scope'
787794
)
788795
} finally {
789-
pool.destroy()
796+
try {
797+
await session?.close()
798+
} finally {
799+
pool.destroy()
800+
}
790801
}
791802
if (!matchesLiveFilters(document, input.documentId, reference.provider, input.filters))
792803
throw new OrchestrationError('not_found', 'Document is outside the selected search filters')

‎apps/sim/lib/sim-search/live/coda-mcp.ts‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,14 @@
11
import { createLogger } from '@sim/logger'
2-
import type { ResourceOwner } from '@/lib/core/resource-scope'
32
import { parseCodaResourceUri } from '@/lib/sim-search/live/coda-uri'
43
import { hasDateBounds, nativeText } from '@/lib/sim-search/live/dates'
54
import { array, NativeSearchError, object, string } from '@/lib/sim-search/live/http'
6-
import {
7-
createManagedSearchMcpClient,
8-
type ManagedSearchMcpClient,
9-
} from '@/lib/sim-search/live/managed-mcp'
5+
import type { ManagedSearchMcpClient } from '@/lib/sim-search/live/managed-mcp'
106
import type { NativeDocument, NativePage, NativeSearchInput } from '@/lib/sim-search/live/types'
117

128
const logger = createLogger('CodaMcpSearch')
139

1410
export interface CodaMcpClient extends ManagedSearchMcpClient {}
1511

16-
export function createCodaMcpClient(
17-
owner: ResourceOwner,
18-
userId: string,
19-
credentialId: string,
20-
signal: AbortSignal,
21-
searches = 1
22-
): Promise<CodaMcpClient> {
23-
return createManagedSearchMcpClient(owner, userId, credentialId, 'coda', signal, searches)
24-
}
25-
2612
function requireCodaUri(uri: string): string {
2713
const parsed = parseCodaResourceUri(uri)
2814
if (!parsed)

0 commit comments

Comments
 (0)