Feature/Custom MCP vars (#4527)

* add input vars to custom mcp

* add ability to specify vars in custom mcp, fix other ui issues

* update setup org ui
This commit is contained in:
Henry Heng
2025-05-28 12:47:53 +01:00
committed by GitHub
parent 2baa43d66f
commit 3d6bf72e73
8 changed files with 173 additions and 52 deletions
@@ -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.<variableName>\`.
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<INodeOptionsValue[]> => {
listActions: async (nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> => {
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<any> {
const tools = await this.getTools(nodeData)
async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
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<Tool[]> {
async getTools(nodeData: INodeData, options: ICommonObject): Promise<Tool[]> {
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)()