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
73 changes: 31 additions & 42 deletions apps/sim/app/api/mcp/tools/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const logger = createLogger('McpToolExecutionAPI')

export const dynamic = 'force-dynamic'

// Type definitions for improved type safety
interface SchemaProperty {
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
description?: string
Expand All @@ -31,9 +30,6 @@ interface ToolExecutionResult {
error?: string
}

/**
* Type guard to safely check if a schema property has a type field
*/
function hasType(prop: unknown): prop is SchemaProperty {
return typeof prop === 'object' && prop !== null && 'type' in prop
}
Expand All @@ -57,7 +53,8 @@ export const POST = withMcpAuth('read')(
userId: userId,
})

const { serverId, toolName, arguments: args } = body
const { serverId, toolName, arguments: rawArgs } = body
const args = rawArgs || {}

const serverIdValidation = validateStringParam(serverId, 'serverId')
if (!serverIdValidation.isValid) {
Expand All @@ -75,22 +72,31 @@ export const POST = withMcpAuth('read')(
`[${requestId}] Executing tool ${toolName} on server ${serverId} for user ${userId} in workspace ${workspaceId}`
)

let tool = null
let tool: McpTool | null = null
try {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
tool = tools.find((t) => t.name === toolName)

if (!tool) {
return createMcpErrorResponse(
new Error(
`Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}`
),
'Tool not found',
404
)
if (body.toolSchema) {
tool = {
name: toolName,
inputSchema: body.toolSchema,
serverId: serverId,
serverName: 'provided-schema',
} as McpTool
logger.debug(`[${requestId}] Using provided schema for ${toolName}, skipping discovery`)
} else {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
tool = tools.find((t) => t.name === toolName) ?? null

if (!tool) {
return createMcpErrorResponse(
new Error(
`Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}`
),
'Tool not found',
404
)
}
}

// Cast arguments to their expected types based on tool schema
if (tool.inputSchema?.properties) {
for (const [paramName, paramSchema] of Object.entries(tool.inputSchema.properties)) {
const schema = paramSchema as any
Expand All @@ -100,7 +106,6 @@ export const POST = withMcpAuth('read')(
continue
}

// Cast numbers
if (
(schema.type === 'number' || schema.type === 'integer') &&
typeof value === 'string'
Expand All @@ -110,42 +115,33 @@ export const POST = withMcpAuth('read')(
if (!Number.isNaN(numValue)) {
args[paramName] = numValue
}
}
// Cast booleans
else if (schema.type === 'boolean' && typeof value === 'string') {
} else if (schema.type === 'boolean' && typeof value === 'string') {
if (value.toLowerCase() === 'true') {
args[paramName] = true
} else if (value.toLowerCase() === 'false') {
args[paramName] = false
}
}
// Cast arrays
else if (schema.type === 'array' && typeof value === 'string') {
} else if (schema.type === 'array' && typeof value === 'string') {
const stringValue = value.trim()
if (stringValue) {
try {
// Try to parse as JSON first (handles ["item1", "item2"])
const parsed = JSON.parse(stringValue)
if (Array.isArray(parsed)) {
args[paramName] = parsed
} else {
// JSON parsed but not an array, wrap in array
args[paramName] = [parsed]
}
} catch (error) {
// JSON parsing failed - treat as comma-separated if contains commas, otherwise single item
} catch {
if (stringValue.includes(',')) {
args[paramName] = stringValue
.split(',')
.map((item) => item.trim())
.filter((item) => item)
} else {
// Single item - wrap in array since schema expects array
args[paramName] = [stringValue]
}
}
} else {
// Empty string becomes empty array
args[paramName] = []
}
}
Expand All @@ -172,7 +168,7 @@ export const POST = withMcpAuth('read')(

const toolCall: McpToolCall = {
name: toolName,
arguments: args || {},
arguments: args,
}

const result = await Promise.race([
Expand All @@ -197,7 +193,6 @@ export const POST = withMcpAuth('read')(
}
logger.info(`[${requestId}] Successfully executed tool ${toolName} on server ${serverId}`)

// Track MCP tool execution
try {
const { trackPlatformEvent } = await import('@/lib/core/telemetry')
trackPlatformEvent('platform.mcp.tool_executed', {
Expand All @@ -206,8 +201,8 @@ export const POST = withMcpAuth('read')(
'mcp.execution_status': 'success',
'workspace.id': workspaceId,
})
} catch (_e) {
// Silently fail
} catch {
// Telemetry failure is non-critical
}

return createMcpSuccessResponse(transformedResult)
Expand All @@ -220,12 +215,9 @@ export const POST = withMcpAuth('read')(
}
)

/**
* Validate tool arguments against schema
*/
function validateToolArguments(tool: McpTool, args: Record<string, unknown>): string | null {
if (!tool.inputSchema) {
return null // No schema to validate against
return null
}

const schema = tool.inputSchema
Expand Down Expand Up @@ -270,9 +262,6 @@ function validateToolArguments(tool: McpTool, args: Record<string, unknown>): st
return null
}

/**
* Transform MCP tool result to platform format
*/
function transformToolResult(result: McpToolResult): ToolExecutionResult {
if (result.isError) {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type React from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Loader2, PlusIcon, WrenchIcon, XIcon } from 'lucide-react'
import { useParams } from 'next/navigation'
Expand Down Expand Up @@ -845,6 +845,52 @@ export function ToolInput({
? (value as unknown as StoredTool[])
: []

const hasBackfilledRef = useRef(false)
useEffect(() => {
if (
isPreview ||
mcpLoading ||
mcpTools.length === 0 ||
selectedTools.length === 0 ||
hasBackfilledRef.current
) {
return
}

const mcpToolsNeedingSchema = selectedTools.filter(
(tool) => tool.type === 'mcp' && !tool.schema && tool.params?.toolName
)

if (mcpToolsNeedingSchema.length === 0) {
return
}

const updatedTools = selectedTools.map((tool) => {
if (tool.type !== 'mcp' || tool.schema || !tool.params?.toolName) {
return tool
}

const mcpTool = mcpTools.find(
(mt) => mt.name === tool.params?.toolName && mt.serverId === tool.params?.serverId
)

if (mcpTool?.inputSchema) {
logger.info(`Backfilling schema for MCP tool: ${tool.params.toolName}`)
return { ...tool, schema: mcpTool.inputSchema }
}

return tool
})

const hasChanges = updatedTools.some((tool, i) => tool.schema && !selectedTools[i].schema)

if (hasChanges) {
hasBackfilledRef.current = true
logger.info(`Backfilled schemas for ${mcpToolsNeedingSchema.length} MCP tool(s)`)
setStoreValue(updatedTools)
}
}, [mcpTools, mcpLoading, selectedTools, isPreview, setStoreValue])

/**
* Checks if a tool is already selected in the current workflow
* @param toolId - The tool identifier to check
Expand Down Expand Up @@ -2314,7 +2360,7 @@ export function ToolInput({
mcpTools={mcpTools}
searchQuery={searchQuery || ''}
customFilter={customFilter}
onToolSelect={(tool) => handleMcpToolSelect(tool, false)}
onToolSelect={handleMcpToolSelect}
disabled={false}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface ServerListItemProps {
server: any
tools: any[]
isDeleting: boolean
isLoadingTools?: boolean
onRemove: () => void
onViewDetails: () => void
}
Expand All @@ -39,6 +40,7 @@ export function ServerListItem({
server,
tools,
isDeleting,
isLoadingTools = false,
onRemove,
onViewDetails,
}: ServerListItemProps) {
Expand All @@ -54,7 +56,9 @@ export function ServerListItem({
</span>
<span className='text-[13px] text-[var(--text-secondary)]'>({transportLabel})</span>
</div>
<p className='truncate text-[13px] text-[var(--text-muted)]'>{toolsLabel}</p>
<p className='truncate text-[13px] text-[var(--text-muted)]'>
{isLoadingTools && tools.length === 0 ? 'Loading...' : toolsLabel}
</p>
</div>
<div className='flex flex-shrink-0 items-center gap-[4px]'>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ export function MCP() {
isLoading: serversLoading,
error: serversError,
} = useMcpServers(workspaceId)
const { data: mcpToolsData = [], error: toolsError } = useMcpToolsQuery(workspaceId)
const {
data: mcpToolsData = [],
error: toolsError,
isLoading: toolsLoading,
isFetching: toolsFetching,
} = useMcpToolsQuery(workspaceId)
const createServerMutation = useCreateMcpServer()
const deleteServerMutation = useDeleteMcpServer()
const { testResult, isTestingConnection, testConnection, clearTestResult } = useMcpServerTest()
Expand Down Expand Up @@ -632,13 +637,15 @@ export function MCP() {
{filteredServers.map((server) => {
if (!server?.id) return null
const tools = toolsByServer[server.id] || []
const isLoadingTools = toolsLoading || toolsFetching

return (
<ServerListItem
key={server.id}
server={server}
tools={tools}
isDeleting={deletingServers.has(server.id)}
isLoadingTools={isLoadingTools}
onRemove={() => handleRemoveServer(server.id, server.name || 'this server')}
onViewDetails={() => handleViewDetails(server.id)}
/>
Expand Down
Loading