Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions apps/sim/app/api/mcp/servers/[id]/refresh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import type { McpServerStatusConfig } from '@/lib/mcp/types'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'

const logger = createLogger('McpServerRefreshAPI')
Expand Down Expand Up @@ -50,6 +51,12 @@ export const POST = withMcpAuth<{ id: string }>('read')(
let toolCount = 0
let lastError: string | null = null

const currentStatusConfig: McpServerStatusConfig =
(server.statusConfig as McpServerStatusConfig | null) ?? {
consecutiveFailures: 0,
lastSuccessfulDiscovery: null,
}

try {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
connectionStatus = 'connected'
Expand All @@ -63,20 +70,40 @@ export const POST = withMcpAuth<{ id: string }>('read')(
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: new Date(),
lastToolsRefresh: now,
connectionStatus,
lastError,
lastConnected: connectionStatus === 'connected' ? new Date() : server.lastConnected,
lastConnected: connectionStatus === 'connected' ? now : server.lastConnected,
toolCount,
updatedAt: new Date(),
statusConfig: newStatusConfig,
updatedAt: now,
})
.where(eq(mcpServers.id, serverId))
.returning()

logger.info(`[${requestId}] Successfully refreshed MCP server: ${serverId}`)
if (connectionStatus === 'connected') {
logger.info(
`[${requestId}] Successfully refreshed MCP server: ${serverId} (${toolCount} tools)`
)
await mcpService.clearCache(workspaceId)
} else {
logger.warn(
`[${requestId}] Refresh completed for MCP server ${serverId} but connection failed: ${lastError}`
)
}

return createMcpSuccessResponse({
status: connectionStatus,
toolCount,
Expand Down
21 changes: 19 additions & 2 deletions apps/sim/app/api/mcp/servers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
// Remove workspaceId from body to prevent it from being updated
const { workspaceId: _, ...updateData } = body

// Get the current server to check if URL is changing
const [currentServer] = await db
.select({ url: mcpServers.url })
.from(mcpServers)
.where(
and(
eq(mcpServers.id, serverId),
eq(mcpServers.workspaceId, workspaceId),
isNull(mcpServers.deletedAt)
)
)
.limit(1)

const [updatedServer] = await db
.update(mcpServers)
.set({
Expand All @@ -71,8 +84,12 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
)
}

// Clear MCP service cache after update
mcpService.clearCache(workspaceId)
// Only clear cache if URL changed (requires re-discovery)
const urlChanged = body.url && currentServer?.url !== body.url
if (urlChanged) {
await mcpService.clearCache(workspaceId)
logger.info(`[${requestId}] Cleared cache due to URL change`)
}

logger.info(`[${requestId}] Successfully updated MCP server: ${serverId}`)
return createMcpSuccessResponse({ server: updatedServer })
Expand Down
10 changes: 7 additions & 3 deletions apps/sim/app/api/mcp/servers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,14 @@ export const POST = withMcpAuth('write')(
timeout: body.timeout || 30000,
retries: body.retries || 3,
enabled: body.enabled !== false,
connectionStatus: 'connected',
lastConnected: new Date(),
updatedAt: new Date(),
deletedAt: null,
})
.where(eq(mcpServers.id, serverId))

mcpService.clearCache(workspaceId)
await mcpService.clearCache(workspaceId)

logger.info(
`[${requestId}] Successfully updated MCP server: ${body.name} (ID: ${serverId})`
Expand All @@ -145,12 +147,14 @@ export const POST = withMcpAuth('write')(
timeout: body.timeout || 30000,
retries: body.retries || 3,
enabled: body.enabled !== false,
connectionStatus: 'connected',
lastConnected: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
})
.returning()

mcpService.clearCache(workspaceId)
await mcpService.clearCache(workspaceId)

logger.info(
`[${requestId}] Successfully registered MCP server: ${body.name} (ID: ${serverId})`
Expand Down Expand Up @@ -212,7 +216,7 @@ export const DELETE = withMcpAuth('admin')(
)
}

mcpService.clearCache(workspaceId)
await mcpService.clearCache(workspaceId)

logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
Expand Down
103 changes: 103 additions & 0 deletions apps/sim/app/api/mcp/tools/stored/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { db } from '@sim/db'
import { workflow, workflowBlocks } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { withMcpAuth } from '@/lib/mcp/middleware'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'

const logger = createLogger('McpStoredToolsAPI')

export const dynamic = 'force-dynamic'

interface StoredMcpTool {
workflowId: string
workflowName: string
serverId: string
serverUrl?: string
toolName: string
schema?: Record<string, unknown>
}

/**
* GET - Get all stored MCP tools from workflows in the workspace
*
* Scans all workflows in the workspace and extracts MCP tools that have been
* added to agent blocks. Returns the stored state of each tool for comparison
* against current server state.
*/
export const GET = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
logger.info(`[${requestId}] Fetching stored MCP tools for workspace ${workspaceId}`)

// Get all workflows in workspace
const workflows = await db
.select({
id: workflow.id,
name: workflow.name,
})
.from(workflow)
.where(eq(workflow.workspaceId, workspaceId))

const workflowMap = new Map(workflows.map((w) => [w.id, w.name]))
const workflowIds = workflows.map((w) => w.id)

if (workflowIds.length === 0) {
return createMcpSuccessResponse({ tools: [] })
}

// Get all agent blocks from these workflows
const agentBlocks = await db
.select({
workflowId: workflowBlocks.workflowId,
subBlocks: workflowBlocks.subBlocks,
})
.from(workflowBlocks)
.where(eq(workflowBlocks.type, 'agent'))

const storedTools: StoredMcpTool[] = []

for (const block of agentBlocks) {
if (!workflowMap.has(block.workflowId)) continue

const subBlocks = block.subBlocks as Record<string, unknown> | null
if (!subBlocks) continue

const toolsSubBlock = subBlocks.tools as Record<string, unknown> | undefined
const toolsValue = toolsSubBlock?.value

if (!toolsValue || !Array.isArray(toolsValue)) continue

for (const tool of toolsValue) {
if (tool.type !== 'mcp') continue

const params = tool.params as Record<string, unknown> | undefined
if (!params?.serverId || !params?.toolName) continue

storedTools.push({
workflowId: block.workflowId,
workflowName: workflowMap.get(block.workflowId) || 'Untitled',
serverId: params.serverId as string,
serverUrl: params.serverUrl as string | undefined,
toolName: params.toolName as string,
schema: tool.schema as Record<string, unknown> | undefined,
})
}
}

logger.info(
`[${requestId}] Found ${storedTools.length} stored MCP tools across ${workflows.length} workflows`
)

return createMcpSuccessResponse({ tools: storedTools })
} catch (error) {
logger.error(`[${requestId}] Error fetching stored MCP tools:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to fetch stored MCP tools'),
'Failed to fetch stored MCP tools',
500
)
}
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ interface McpTool {
inputSchema?: any
}

interface McpServer {
id: string
url?: string
}

interface StoredTool {
type: 'mcp'
title: string
toolId: string
params: {
serverId: string
serverUrl?: string
toolName: string
serverName: string
}
Expand All @@ -34,6 +40,7 @@ interface StoredTool {

interface McpToolsListProps {
mcpTools: McpTool[]
mcpServers?: McpServer[]
searchQuery: string
customFilter: (name: string, query: string) => number
onToolSelect: (tool: StoredTool) => void
Expand All @@ -45,6 +52,7 @@ interface McpToolsListProps {
*/
export function McpToolsList({
mcpTools,
mcpServers = [],
searchQuery,
customFilter,
onToolSelect,
Expand All @@ -59,44 +67,48 @@ export function McpToolsList({
return (
<>
<PopoverSection>MCP Tools</PopoverSection>
{filteredTools.map((mcpTool) => (
<ToolCommand.Item
key={mcpTool.id}
value={mcpTool.name}
onSelect={() => {
if (disabled) return
{filteredTools.map((mcpTool) => {
const server = mcpServers.find((s) => s.id === mcpTool.serverId)
return (
<ToolCommand.Item
key={mcpTool.id}
value={mcpTool.name}
onSelect={() => {
if (disabled) return

const newTool: StoredTool = {
type: 'mcp',
title: mcpTool.name,
toolId: mcpTool.id,
params: {
serverId: mcpTool.serverId,
toolName: mcpTool.name,
serverName: mcpTool.serverName,
},
isExpanded: true,
usageControl: 'auto',
schema: {
...mcpTool.inputSchema,
description: mcpTool.description,
},
}
const newTool: StoredTool = {
type: 'mcp',
title: mcpTool.name,
toolId: mcpTool.id,
params: {
serverId: mcpTool.serverId,
serverUrl: server?.url,
toolName: mcpTool.name,
serverName: mcpTool.serverName,
},
isExpanded: true,
usageControl: 'auto',
schema: {
...mcpTool.inputSchema,
description: mcpTool.description,
},
}

onToolSelect(newTool)
}}
>
<div
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
style={{ background: mcpTool.bgColor }}
onToolSelect(newTool)
}}
>
<IconComponent icon={mcpTool.icon} className='h-[11px] w-[11px] text-white' />
</div>
<span className='truncate' title={`${mcpTool.name} (${mcpTool.serverName})`}>
{mcpTool.name}
</span>
</ToolCommand.Item>
))}
<div
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
style={{ background: mcpTool.bgColor }}
>
<IconComponent icon={mcpTool.icon} className='h-[11px] w-[11px] text-white' />
</div>
<span className='truncate' title={`${mcpTool.name} (${mcpTool.serverName})`}>
{mcpTool.name}
</span>
</ToolCommand.Item>
)
})}
</>
)
}
Loading
Loading