diff --git a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts index b24144ea5..c782a357c 100644 --- a/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts +++ b/packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts @@ -1,12 +1,34 @@ import { Tool } from '@langchain/core/tools' -import { INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' +import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' import { MCPToolkit } from '../core' +import { getVars, prepareSandboxVars } from '../../../../src/utils' +import { DataSource } from 'typeorm' const mcpServerConfig = `{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"] }` +const howToUseCode = ` +You can use variables in the MCP Server Config with double curly braces \`{{ }}\` and prefix \`$vars.\`. + +For example, you have a variable called "var1": +\`\`\`json +{ + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", "API_TOKEN" + ], + "env": { + "API_TOKEN": "{{$vars.var1}}" + } +} +\`\`\` +` + class Custom_MCP implements INode { label: string name: string @@ -23,7 +45,7 @@ class Custom_MCP implements INode { constructor() { this.label = 'Custom MCP' this.name = 'customMCP' - this.version = 1.0 + this.version = 1.1 this.type = 'Custom MCP Tool' this.icon = 'customMCP.png' this.category = 'Tools (MCP)' @@ -35,6 +57,10 @@ class Custom_MCP implements INode { name: 'mcpServerConfig', type: 'code', hideCodeExecute: true, + hint: { + label: 'How to use', + value: howToUseCode + }, placeholder: mcpServerConfig }, { @@ -50,9 +76,9 @@ class Custom_MCP implements INode { //@ts-ignore loadMethods = { - listActions: async (nodeData: INodeData): Promise => { + listActions: async (nodeData: INodeData, options: ICommonObject): Promise => { try { - const toolset = await this.getTools(nodeData) + const toolset = await this.getTools(nodeData, options) toolset.sort((a: any, b: any) => a.name.localeCompare(b.name)) return toolset.map(({ name, ...rest }) => ({ @@ -72,8 +98,8 @@ class Custom_MCP implements INode { } } - async init(nodeData: INodeData): Promise { - const tools = await this.getTools(nodeData) + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const tools = await this.getTools(nodeData, options) const _mcpActions = nodeData.inputs?.mcpActions let mcpActions = [] @@ -88,19 +114,29 @@ class Custom_MCP implements INode { return tools.filter((tool: any) => mcpActions.includes(tool.name)) } - async getTools(nodeData: INodeData): Promise { + async getTools(nodeData: INodeData, options: ICommonObject): Promise { const mcpServerConfig = nodeData.inputs?.mcpServerConfig as string - if (!mcpServerConfig) { throw new Error('MCP Server Config is required') } + let sandbox: ICommonObject = {} + + if (mcpServerConfig.includes('$vars')) { + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + const variables = await getVars(appDataSource, databaseEntities, nodeData, options) + sandbox['$vars'] = prepareSandboxVars(variables) + } + try { let serverParams if (typeof mcpServerConfig === 'object') { - serverParams = mcpServerConfig + serverParams = substituteVariablesInObject(mcpServerConfig, sandbox) } else if (typeof mcpServerConfig === 'string') { - const serverParamsString = convertToValidJSONString(mcpServerConfig) + const substitutedString = substituteVariablesInString(mcpServerConfig, sandbox) + const serverParamsString = convertToValidJSONString(substitutedString) serverParams = JSON.parse(serverParamsString) } @@ -123,6 +159,67 @@ class Custom_MCP implements INode { } } +function substituteVariablesInObject(obj: any, sandbox: any): any { + if (typeof obj === 'string') { + // Replace variables in string values + return substituteVariablesInString(obj, sandbox) + } else if (Array.isArray(obj)) { + // Recursively process arrays + return obj.map((item) => substituteVariablesInObject(item, sandbox)) + } else if (obj !== null && typeof obj === 'object') { + // Recursively process object properties + const result: any = {} + for (const [key, value] of Object.entries(obj)) { + result[key] = substituteVariablesInObject(value, sandbox) + } + return result + } + // Return primitive values as-is + return obj +} + +function substituteVariablesInString(str: string, sandbox: any): string { + // Use regex to find {{$variableName.property}} patterns and replace with sandbox values + return str.replace(/\{\{\$([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}\}/g, (match, variablePath) => { + try { + // Split the path into parts (e.g., "vars.testvar1" -> ["vars", "testvar1"]) + const pathParts = variablePath.split('.') + + // Start with the sandbox object + let current = sandbox + + // Navigate through the path + for (const part of pathParts) { + // For the first part, check if it exists with $ prefix + if (current === sandbox) { + const sandboxKey = `$${part}` + if (Object.keys(current).includes(sandboxKey)) { + current = current[sandboxKey] + } else { + // If the key doesn't exist, return the original match + return match + } + } else { + // For subsequent parts, access directly + if (current && typeof current === 'object' && part in current) { + current = current[part] + } else { + // If the property doesn't exist, return the original match + return match + } + } + } + + // Return the resolved value, converting to string if necessary + return typeof current === 'string' ? current : JSON.stringify(current) + } catch (error) { + // If any error occurs during resolution, return the original match + console.warn(`Error resolving variable ${match}:`, error) + return match + } + }) +} + function convertToValidJSONString(inputString: string) { try { const jsObject = Function('return ' + inputString)() diff --git a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx index 21e81d64c..90fbc18a0 100644 --- a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx +++ b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx @@ -743,9 +743,9 @@ export const ExecutionDetails = ({ open, isPublic, execution, metadata, onClose, sx={{ pl: 1 }} icon={} variant='outlined' - label={metadata?.agentflow?.name || metadata?.agentflow?.id || 'Go to AgentFlow'} + label={localMetadata?.agentflow?.name || localMetadata?.agentflow?.id || 'Go to AgentFlow'} className={'button'} - onClick={() => window.open(`/v2/agentcanvas/${metadata?.agentflow?.id}`, '_blank')} + onClick={() => window.open(`/v2/agentcanvas/${localMetadata?.agentflow?.id}`, '_blank')} /> )} diff --git a/packages/ui/src/views/agentexecutions/PublicExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/PublicExecutionDetails.jsx index a0c9fa1eb..b2c95c6ae 100644 --- a/packages/ui/src/views/agentexecutions/PublicExecutionDetails.jsx +++ b/packages/ui/src/views/agentexecutions/PublicExecutionDetails.jsx @@ -38,8 +38,16 @@ const PublicExecutionDetails = () => { const executionDetails = typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData setExecution(executionDetails) - setSelectedMetadata(omit(execution, ['executionData'])) + const newMetadata = { + ...omit(execution, ['executionData']), + agentflow: { + ...selectedMetadata.agentflow + } + } + setSelectedMetadata(newMetadata) } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [getExecutionByIdPublicApi.data]) useEffect(() => { diff --git a/packages/ui/src/views/agentexecutions/index.jsx b/packages/ui/src/views/agentexecutions/index.jsx index fcf902ac2..8d12dd706 100644 --- a/packages/ui/src/views/agentexecutions/index.jsx +++ b/packages/ui/src/views/agentexecutions/index.jsx @@ -225,8 +225,16 @@ const AgentExecutions = () => { const executionDetails = typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData setSelectedExecutionData(executionDetails) - setSelectedMetadata(omit(execution, ['executionData'])) + const newMetadata = { + ...omit(execution, ['executionData']), + agentflow: { + ...selectedMetadata.agentflow + } + } + setSelectedMetadata(newMetadata) } + + // eslint-disable-next-line react-hooks/exhaustive-deps }, [getExecutionByIdApi.data]) return ( diff --git a/packages/ui/src/views/agentflowsv2/AgentFlowNode.jsx b/packages/ui/src/views/agentflowsv2/AgentFlowNode.jsx index a028e8281..d52639d42 100644 --- a/packages/ui/src/views/agentflowsv2/AgentFlowNode.jsx +++ b/packages/ui/src/views/agentflowsv2/AgentFlowNode.jsx @@ -421,23 +421,17 @@ const AgentFlowNode = ({ data }) => { return ( - {toolName} - + /> ) }) } else { @@ -447,23 +441,17 @@ const AgentFlowNode = ({ data }) => { return [ - {toolName} - + /> ] } })} diff --git a/packages/ui/src/views/canvas/NodeInputHandler.jsx b/packages/ui/src/views/canvas/NodeInputHandler.jsx index 59d088057..df22995c6 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.jsx +++ b/packages/ui/src/views/canvas/NodeInputHandler.jsx @@ -1069,7 +1069,7 @@ const NodeInputHandler = ({ )} {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && - (inputParam?.acceptVariable ? ( + (inputParam?.acceptVariable && window.location.href.includes('v2/agentcanvas') ? ( { const theme = useTheme() const canvas = useSelector((state) => state.canvas) + const customization = useSelector((state) => state.customization) const { deleteNode, duplicateNode } = useContext(flowContext) const [inputParam] = data.inputParams @@ -31,12 +32,23 @@ const StickyNote = ({ data }) => { setOpen(true) } + const defaultColor = '#FFE770' // fallback color if data.color is not present + const nodeColor = data.color || defaultColor + const getBorderColor = () => { if (data.selected) return theme.palette.primary.main - else if (theme?.customization?.isDarkMode) return theme.palette.grey[900] + 25 + else if (customization?.isDarkMode) return theme.palette.grey[700] else return theme.palette.grey[900] + 50 } + const getBackgroundColor = () => { + if (customization?.isDarkMode) { + return data.selected ? darken(nodeColor, 0.7) : darken(nodeColor, 0.8) + } else { + return data.selected ? lighten(nodeColor, 0.1) : lighten(nodeColor, 0.2) + } + } + return ( <> { sx={{ padding: 0, borderColor: getBorderColor(), - backgroundColor: data.selected ? '#FFDC00' : '#FFE770' + backgroundColor: getBackgroundColor() }} border={false} > @@ -66,8 +78,12 @@ const StickyNote = ({ data }) => { onClick={() => { duplicateNode(data.id) }} - sx={{ height: '35px', width: '35px', '&:hover': { color: theme?.palette.primary.main } }} - color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'} + sx={{ + height: '35px', + width: '35px', + color: customization?.isDarkMode ? 'white' : 'inherit', + '&:hover': { color: theme?.palette.primary.main } + }} > @@ -76,8 +92,12 @@ const StickyNote = ({ data }) => { onClick={() => { deleteNode(data.id) }} - sx={{ height: '35px', width: '35px', '&:hover': { color: 'red' } }} - color={theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit'} + sx={{ + height: '35px', + width: '35px', + color: customization?.isDarkMode ? 'white' : 'inherit', + '&:hover': { color: theme?.palette.error.main } + }} > diff --git a/packages/ui/src/views/organization/index.jsx b/packages/ui/src/views/organization/index.jsx index 80cb90696..5956ea58b 100644 --- a/packages/ui/src/views/organization/index.jsx +++ b/packages/ui/src/views/organization/index.jsx @@ -287,7 +287,7 @@ const OrganizationSetupPage = () => { <>
- + Existing Username *
@@ -304,7 +304,7 @@ const OrganizationSetupPage = () => {
- + Existing Password *