d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
268 lines
8.5 KiB
TypeScript
268 lines
8.5 KiB
TypeScript
import { db } from '@sim/db'
|
|
import { mcpServers, workflow, workflowBlocks } from '@sim/db/schema'
|
|
import { createLogger } from '@sim/logger'
|
|
import { toError } from '@sim/utils/errors'
|
|
import { and, eq, inArray, isNull } from 'drizzle-orm'
|
|
import type { NextRequest } from 'next/server'
|
|
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 { McpServerStatusConfig, McpTool, McpToolSchema } from '@/lib/mcp/types'
|
|
import {
|
|
createMcpErrorResponse,
|
|
createMcpSuccessResponse,
|
|
MCP_TOOL_CORE_PARAMS,
|
|
} from '@/lib/mcp/utils'
|
|
|
|
const logger = createLogger('McpServerRefreshAPI')
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
/** Schema stored in workflow blocks includes description from the tool. */
|
|
type StoredToolSchema = McpToolSchema & { description?: string }
|
|
|
|
interface StoredTool {
|
|
type: string
|
|
title: string
|
|
toolId: string
|
|
params: {
|
|
serverId: string
|
|
serverUrl?: string
|
|
toolName: string
|
|
serverName?: string
|
|
}
|
|
schema?: StoredToolSchema
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface SyncResult {
|
|
updatedCount: number
|
|
updatedWorkflowIds: string[]
|
|
}
|
|
|
|
interface ServerMetadata {
|
|
url?: string
|
|
name?: string
|
|
}
|
|
|
|
/**
|
|
* Syncs tool schemas and server metadata from discovered MCP tools to all
|
|
* workflow blocks using those tools. Updates stored serverUrl/serverName
|
|
* when the server's details have changed, preventing stale badges after
|
|
* a server URL edit.
|
|
*/
|
|
async function syncToolSchemasToWorkflows(
|
|
workspaceId: string,
|
|
serverId: string,
|
|
tools: McpTool[],
|
|
requestId: string,
|
|
serverMeta?: ServerMetadata
|
|
): Promise<SyncResult> {
|
|
const toolsByName = new Map(tools.map((t) => [t.name, t]))
|
|
|
|
const workspaceWorkflows = await db
|
|
.select({ id: workflow.id })
|
|
.from(workflow)
|
|
.where(eq(workflow.workspaceId, workspaceId))
|
|
|
|
const workflowIds = workspaceWorkflows.map((w) => w.id)
|
|
if (workflowIds.length === 0) return { updatedCount: 0, updatedWorkflowIds: [] }
|
|
|
|
const agentBlocks = await db
|
|
.select({
|
|
id: workflowBlocks.id,
|
|
workflowId: workflowBlocks.workflowId,
|
|
subBlocks: workflowBlocks.subBlocks,
|
|
})
|
|
.from(workflowBlocks)
|
|
.where(and(eq(workflowBlocks.type, 'agent'), inArray(workflowBlocks.workflowId, workflowIds)))
|
|
|
|
const updatedWorkflowIds = new Set<string>()
|
|
|
|
for (const block of agentBlocks) {
|
|
const subBlocks = block.subBlocks as Record<string, unknown> | null
|
|
if (!subBlocks) continue
|
|
|
|
const toolsSubBlock = subBlocks.tools as { value?: StoredTool[] } | undefined
|
|
if (!toolsSubBlock?.value || !Array.isArray(toolsSubBlock.value)) continue
|
|
|
|
let hasUpdates = false
|
|
const updatedTools = toolsSubBlock.value.map((tool) => {
|
|
if (tool.type !== 'mcp' || tool.params?.serverId !== serverId) {
|
|
return tool
|
|
}
|
|
|
|
const freshTool = toolsByName.get(tool.params.toolName)
|
|
if (!freshTool) return tool
|
|
|
|
const newSchema: StoredToolSchema = {
|
|
...freshTool.inputSchema,
|
|
description: freshTool.description,
|
|
}
|
|
|
|
const schemasMatch = JSON.stringify(tool.schema) === JSON.stringify(newSchema)
|
|
|
|
const urlChanged = serverMeta?.url != null && tool.params.serverUrl !== serverMeta.url
|
|
const nameChanged = serverMeta?.name != null && tool.params.serverName !== serverMeta.name
|
|
|
|
if (!schemasMatch || urlChanged || nameChanged) {
|
|
hasUpdates = true
|
|
|
|
const validParamKeys = new Set(Object.keys(newSchema.properties || {}))
|
|
|
|
const cleanedParams: Record<string, unknown> = {}
|
|
for (const [key, value] of Object.entries(tool.params || {})) {
|
|
if (MCP_TOOL_CORE_PARAMS.has(key) || validParamKeys.has(key)) {
|
|
cleanedParams[key] = value
|
|
}
|
|
}
|
|
|
|
if (urlChanged) cleanedParams.serverUrl = serverMeta.url
|
|
if (nameChanged) cleanedParams.serverName = serverMeta.name
|
|
|
|
return { ...tool, schema: newSchema, params: cleanedParams }
|
|
}
|
|
|
|
return tool
|
|
})
|
|
|
|
if (hasUpdates) {
|
|
const updatedSubBlocks = {
|
|
...subBlocks,
|
|
tools: { ...toolsSubBlock, value: updatedTools },
|
|
}
|
|
|
|
await db
|
|
.update(workflowBlocks)
|
|
.set({ subBlocks: updatedSubBlocks, updatedAt: new Date() })
|
|
.where(eq(workflowBlocks.id, block.id))
|
|
|
|
updatedWorkflowIds.add(block.workflowId)
|
|
}
|
|
}
|
|
|
|
if (updatedWorkflowIds.size > 0) {
|
|
logger.info(
|
|
`[${requestId}] Synced tool schemas to ${updatedWorkflowIds.size} workflow(s) for server ${serverId}`
|
|
)
|
|
}
|
|
|
|
return {
|
|
updatedCount: updatedWorkflowIds.size,
|
|
updatedWorkflowIds: Array.from(updatedWorkflowIds),
|
|
}
|
|
}
|
|
|
|
export const POST = withRouteHandler(
|
|
withMcpAuth<{ id: string }>('read')(
|
|
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
|
try {
|
|
const paramsValidation = mcpServerIdParamsSchema.safeParse(await params)
|
|
if (!paramsValidation.success) return validationErrorResponse(paramsValidation.error)
|
|
const { id: serverId } = paramsValidation.data
|
|
logger.info(`[${requestId}] Refreshing MCP server: ${serverId}`)
|
|
|
|
const [server] = await db
|
|
.select()
|
|
.from(mcpServers)
|
|
.where(
|
|
and(
|
|
eq(mcpServers.id, serverId),
|
|
eq(mcpServers.workspaceId, workspaceId),
|
|
isNull(mcpServers.deletedAt)
|
|
)
|
|
)
|
|
.limit(1)
|
|
|
|
if (!server) {
|
|
return createMcpErrorResponse(
|
|
new Error('Server not found or access denied'),
|
|
'Server not found',
|
|
404
|
|
)
|
|
}
|
|
|
|
let connectionStatus: 'connected' | 'disconnected' | 'error' = 'error'
|
|
let toolCount = 0
|
|
let lastError: string | null = null
|
|
let syncResult: SyncResult = { updatedCount: 0, updatedWorkflowIds: [] }
|
|
let discoveredTools: McpTool[] = []
|
|
|
|
const currentStatusConfig: McpServerStatusConfig =
|
|
(server.statusConfig as McpServerStatusConfig | null) ?? {
|
|
consecutiveFailures: 0,
|
|
lastSuccessfulDiscovery: null,
|
|
}
|
|
|
|
try {
|
|
discoveredTools = await mcpService.discoverServerTools(
|
|
userId,
|
|
serverId,
|
|
workspaceId,
|
|
true
|
|
)
|
|
connectionStatus = 'connected'
|
|
toolCount = discoveredTools.length
|
|
logger.info(`[${requestId}] Discovered ${toolCount} tools from server ${serverId}`)
|
|
|
|
syncResult = await syncToolSchemasToWorkflows(
|
|
workspaceId,
|
|
serverId,
|
|
discoveredTools,
|
|
requestId,
|
|
{ url: server.url ?? undefined, name: server.name ?? undefined }
|
|
)
|
|
} catch (error) {
|
|
connectionStatus = 'error'
|
|
lastError =
|
|
error instanceof Error
|
|
? error.message.split('\n')[0].slice(0, 200)
|
|
: 'Connection failed'
|
|
logger.warn(`[${requestId}] Failed to connect to server ${serverId}:`, error)
|
|
}
|
|
|
|
const now = new Date()
|
|
const newStatusConfig =
|
|
connectionStatus === 'connected'
|
|
? { consecutiveFailures: 0, lastSuccessfulDiscovery: now.toISOString() }
|
|
: {
|
|
consecutiveFailures: currentStatusConfig.consecutiveFailures + 1,
|
|
lastSuccessfulDiscovery: currentStatusConfig.lastSuccessfulDiscovery,
|
|
}
|
|
|
|
const [refreshedServer] = await db
|
|
.update(mcpServers)
|
|
.set({
|
|
lastToolsRefresh: now,
|
|
connectionStatus,
|
|
lastError,
|
|
lastConnected: connectionStatus === 'connected' ? now : server.lastConnected,
|
|
toolCount,
|
|
statusConfig: newStatusConfig,
|
|
updatedAt: now,
|
|
})
|
|
.where(eq(mcpServers.id, serverId))
|
|
.returning()
|
|
|
|
if (connectionStatus === 'connected') {
|
|
await mcpService.clearCache(workspaceId)
|
|
}
|
|
|
|
return createMcpSuccessResponse({
|
|
status: connectionStatus,
|
|
toolCount,
|
|
lastConnected: refreshedServer?.lastConnected?.toISOString() || null,
|
|
error: lastError,
|
|
workflowsUpdated: syncResult.updatedCount,
|
|
updatedWorkflowIds: syncResult.updatedWorkflowIds,
|
|
})
|
|
} catch (error) {
|
|
logger.error(`[${requestId}] Error refreshing MCP server:`, error)
|
|
return createMcpErrorResponse(toError(error), 'Failed to refresh MCP server', 500)
|
|
}
|
|
}
|
|
)
|
|
)
|