ZWW的博客

大家好,欢迎来到小Z的突发奇想圈,这里有各种小Z发明的小玩具、个人动态、突发奇想的小技巧。

Flowise 修改 Github MCP组件源码解决输入类型错误问题

怎么回事?

准备用Flowise做一个工作流,实现从Github的issue中搜索开源项目的问题的解决方案,以及相关讨论,于是想能否让AI帮我搜集呢?先想到的就是页面抓取,但是经过实践发现只能抓取到用户的提问,无法获取其他人的讨论内容(这不没用吗)。于是换个思路,想起来MCP,于是搜索看看Github是否有官方MCP呢?

还真让我搜到了,突然想起Flowise是不是也集成了呢?看一下——真有,于是就用了,用着用着发现:在传入issue_number参数时总是提示“预期是Number,实际传入是string”

问题根源

Flowise节点间传递参数固定为string类型,无法指定传入参数的类型,那怎么办呢?只能改源码了,那改Flowise还是Github MCP呢?改Flowise源码会影响后续升级,显然改Github MCP的才是正确的。那么开搞。

如何解决

是不是可以在调用特定方法(明确要求传参为Number的方法)前,进行判断,如发现action为issue_read时,我就对传入的参数进行Number(input),是不是就解决了。对,只需要做这样一点改动就可了,开搞⬇️

修改后的Github MCP源码

//@ts-ignore
import { Tool, tool } from '@langchain/core/tools'
import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface'
import { getCredentialData, getCredentialParam, getNodeModulesPackagePath } from '../../../../src/utils'
import { MCPToolkit } from '../core'

class Github_MCP implements INode {
    label: string
    name: string
    version: number
    description: string
    type: string
    icon: string
    category: string
    baseClasses: string[]
    documentation: string
    credential: INodeParams
    inputs: INodeParams[]

    constructor() {
        this.label = 'Github MCP'
        this.name = 'githubMCP'
        this.version = 1.0
        this.type = 'Github MCP Tool'
        this.icon = 'github.svg'
        this.category = 'Tools (MCP)'
        this.description = 'MCP Server for the GitHub API'
        this.documentation = 'https://github.com/modelcontextprotocol/servers/tree/main/src/github'
        this.credential = {
            label: 'Connect Credential',
            name: 'credential',
            type: 'credential',
            credentialNames: ['githubApi']
        }
        this.inputs = [
            {
                label: 'Available Actions',
                name: 'mcpActions',
                type: 'asyncMultiOptions',
                loadMethod: 'listActions',
                refresh: true
            }
        ]
        this.baseClasses = ['Tool']
    }

    //@ts-ignore
    loadMethods = {
        listActions: async (nodeData: INodeData, options: ICommonObject): Promise<INodeOptionsValue[]> => {
            try {
                const toolset = await this.getTools(nodeData, options)
                toolset.sort((a: any, b: any) => a.name.localeCompare(b.name))

                return toolset.map(({ name, ...rest }) => ({
                    label: name.toUpperCase(),
                    name: name,
                    description: rest.description || name
                }))
            } catch (error) {
                console.error('Error listing actions:', error)
                return [
                    {
                        label: 'No Available Actions',
                        name: 'error',
                        description: 'No available actions, please check your Github Access Token and refresh'
                    }
                ]
            }
        }
    }

    async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
        const tools = await this.getTools(nodeData, options)

        const _mcpActions = nodeData.inputs?.mcpActions
        let mcpActions = []
        if (_mcpActions) {
            try {
                mcpActions = typeof _mcpActions === 'string' ? JSON.parse(_mcpActions) : _mcpActions
            } catch (error) {
                console.error('Error parsing mcp actions:', error)
            }
        }

        const filteredTools = tools.filter((tool: any) => mcpActions.includes(tool.name))
        
        // 包装工具以定制 input 参数
        return filteredTools.map((tool: Tool) => {
            return this.wrapToolWithInputCustomization(tool)
        })
    }

    /**
     * 包装工具以定制 input 参数
     * 可以在这里对输入参数进行预处理、验证或转换
     */
    private wrapToolWithInputCustomization(originalTool: Tool): Tool {
        return tool(
            async (input: any): Promise<string> => {
                // 在这里可以定制 input 参数
                const customizedInput = this.customizeInput(originalTool.name, input)
                
                // 调用原始工具
                return await originalTool.invoke(customizedInput)
            },
            {
                name: originalTool.name,
                description: originalTool.description,
                schema: originalTool.schema
            }
        )
    }

    /**
     * 定制输入参数的逻辑
     * 可以根据工具名称和原始输入进行不同的处理
     */
    private customizeInput(toolName: string, input: any): any {
        // 示例:为所有工具添加默认值或进行转换
        // 你可以根据实际需求修改这个函数
        
        // 示例1: 添加默认参数
        // if (toolName === 'some_tool_name') {
        //     return {
        //         ...input,
        //         defaultParam: 'defaultValue'
        //     }
        // }
        
        // 示例2: 转换参数格式
        // if (toolName === 'another_tool') {
        //     return {
        //         ...input,
        //         param: input.param?.toLowerCase()
        //     }
        // }
        
        // 示例3: 验证参数
        // if (toolName === 'github_search' && !input.query) {
        //     throw new Error('Query parameter is required')
        // }
        if (toolName === 'search_repositories') {
            return {
                ...input,
                perPage: Number(input.perPage) || 10
            }
        }
        if (toolName === 'get_issue'){
            return {
                ...input,
                issue_number: Number(input.issue_number)
            }
        }
        // 默认情况下,直接返回原始输入
        return input
    }

    async getTools(nodeData: INodeData, options: ICommonObject): Promise<Tool[]> {
        const credentialData = await getCredentialData(nodeData.credential ?? '', options)
        const accessToken = getCredentialParam('accessToken', credentialData, nodeData)

        if (!accessToken) {
            throw new Error('Missing Github Access Token')
        }

        const packagePath = getNodeModulesPackagePath('@modelcontextprotocol/server-github/dist/index.js')

        const serverParams = {
            command: 'node',
            args: [packagePath],
            env: {
                GITHUB_PERSONAL_ACCESS_TOKEN: accessToken
            }
        }

        const toolkit = new MCPToolkit(serverParams, 'stdio')
        await toolkit.initialize()

        const tools = toolkit.tools ?? []

        return tools as Tool[]
    }
}

module.exports = { nodeClass: Github_MCP }

重点关注“init”,“wrapToolWithInputCustomization”,“customizeInput”这三个方法。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注