-
Notifications
You must be signed in to change notification settings - Fork 3.7k
fix(mcp): stabilize authenticated tool discovery #5754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
aa654cd
ce6d0f6
89dce25
10baba5
80da6da
6ff6634
0a12ca9
5e975cd
c1764a1
d4bdbe8
d7a4922
444f512
cdc3f46
5940a20
8899fdf
b9fe771
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ import { mcpServerIdParamsSchema } from '@/lib/api/contracts/mcp' | |
| import { validationErrorResponse } from '@/lib/api/server' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { withMcpAuth } from '@/lib/mcp/middleware' | ||
| import { mcpService } from '@/lib/mcp/service' | ||
| import { type McpServerDiscoveryState, mcpService } from '@/lib/mcp/service' | ||
| import type { McpTool, McpToolSchema } from '@/lib/mcp/types' | ||
| import { | ||
| categorizeError, | ||
|
|
@@ -188,16 +188,18 @@ export const POST = withRouteHandler( | |
|
|
||
| let syncResult: SyncResult = { updatedCount: 0, updatedWorkflowIds: [] } | ||
| let discoveredTools: McpTool[] = [] | ||
| let discoveryState: McpServerDiscoveryState | null = null | ||
| let discoveryError: string | null = null | ||
| const discoveryStartedAt = new Date() | ||
|
|
||
| try { | ||
| discoveredTools = await mcpService.discoverServerTools( | ||
| const discovery = await mcpService.discoverServerToolsWithMetadata( | ||
| userId, | ||
| serverId, | ||
| workspaceId, | ||
| true | ||
| ) | ||
| discoveredTools = discovery.tools | ||
| discoveryState = discovery.state | ||
| logger.info( | ||
| `[${requestId}] Discovered ${discoveredTools.length} tools from server ${serverId}` | ||
| ) | ||
|
|
@@ -208,14 +210,37 @@ export const POST = withRouteHandler( | |
| }) | ||
| } | ||
|
|
||
| if (discoveryError === null) { | ||
| const [refreshedServer] = await db | ||
| .select({ | ||
| name: mcpServers.name, | ||
| url: mcpServers.url, | ||
| connectionStatus: mcpServers.connectionStatus, | ||
| lastConnected: mcpServers.lastConnected, | ||
| lastToolsRefresh: mcpServers.lastToolsRefresh, | ||
| lastError: mcpServers.lastError, | ||
| toolCount: mcpServers.toolCount, | ||
| }) | ||
| .from(mcpServers) | ||
| .where( | ||
| and( | ||
| eq(mcpServers.id, serverId), | ||
| eq(mcpServers.workspaceId, workspaceId), | ||
| isNull(mcpServers.deletedAt) | ||
| ) | ||
| ) | ||
| .limit(1) | ||
|
|
||
| if (discoveryError === null && discoveryState !== 'superseded') { | ||
| try { | ||
| syncResult = await syncToolSchemasToWorkflows( | ||
| workspaceId, | ||
| serverId, | ||
| discoveredTools, | ||
| requestId, | ||
| { url: server.url ?? undefined, name: server.name ?? undefined } | ||
| { | ||
| url: refreshedServer?.url ?? undefined, | ||
| name: refreshedServer?.name ?? undefined, | ||
| } | ||
| ) | ||
| } catch (error) { | ||
| // Discovery already persisted status and cached tools; a workflow-sync | ||
|
|
@@ -227,47 +252,36 @@ export const POST = withRouteHandler( | |
| } | ||
| } | ||
|
|
||
| const now = new Date() | ||
|
|
||
| const [refreshedServer] = await db | ||
| .update(mcpServers) | ||
| .set({ | ||
| lastToolsRefresh: now, | ||
| updatedAt: now, | ||
| }) | ||
| .where( | ||
| and( | ||
| eq(mcpServers.id, serverId), | ||
| eq(mcpServers.workspaceId, workspaceId), | ||
| isNull(mcpServers.deletedAt) | ||
| ) | ||
| ) | ||
| .returning({ | ||
| connectionStatus: mcpServers.connectionStatus, | ||
| lastConnected: mcpServers.lastConnected, | ||
| lastError: mcpServers.lastError, | ||
| toolCount: mcpServers.toolCount, | ||
| }) | ||
|
|
||
| let connectionStatus = refreshedServer?.connectionStatus ?? 'error' | ||
| let lastError = refreshedServer ? refreshedServer.lastError : discoveryError | ||
| const toolCount = refreshedServer?.toolCount ?? discoveredTools.length | ||
| let toolCount = refreshedServer?.toolCount ?? discoveredTools.length | ||
|
|
||
| if (discoveryState === 'superseded') { | ||
| connectionStatus = 'disconnected' | ||
| lastError = 'Tool discovery was superseded by a newer refresh. Please retry.' | ||
| toolCount = 0 | ||
| } else if ( | ||
| discoveryError === null && | ||
| (discoveryState === 'unavailable' || discoveryState === 'winner-cache') | ||
| ) { | ||
| connectionStatus = 'connected' | ||
| lastError = null | ||
| toolCount = discoveredTools.length | ||
| } | ||
|
j15z marked this conversation as resolved.
|
||
|
|
||
| if (discoveryError !== null && connectionStatus === 'connected') { | ||
| const newerSuccessWonRace = | ||
| refreshedServer?.lastConnected != null && | ||
| refreshedServer.lastConnected > discoveryStartedAt | ||
| refreshedServer?.lastToolsRefresh != null && | ||
| (server.lastToolsRefresh == null || | ||
| refreshedServer.lastToolsRefresh > server.lastToolsRefresh) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Invalidation hides refresh failuresMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit b9fe771. Configure here.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is valid: Two bounded options:
My lean is option 1 for this PR: it closes the reported race without a migration, while retaining the mutation token as ordering rather than overloading it as success provenance. |
||
|
|
||
| if (!newerSuccessWonRace) { | ||
| connectionStatus = 'disconnected' | ||
| lastError = discoveryError | ||
| toolCount = 0 | ||
|
j15z marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| if (connectionStatus === 'connected') { | ||
| await mcpService.clearCache(workspaceId) | ||
| } | ||
|
|
||
| return createMcpSuccessResponse({ | ||
| status: connectionStatus, | ||
| toolCount, | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.