MCP 工具集成:外部工具变 Eino Tool

📅 2026/7/2 11:40:20
MCP 工具集成:外部工具变 Eino Tool
系列「企业级 AI Agent 实现拆解」E25 篇。上一篇讲了中间件系统在 Agent 执行流中插入自定义逻辑。这篇讲MCP 工具集成——把遵守 MCP 协议的任意外部工具直接变成 Eino Agent 能用的 Tool以及 DeepFlux 在此基础上额外做了什么。读完这篇你会知道MCP 是什么JSON-RPC 2.0 打底三种传输方式Eino 的 Tool 接口体系BaseTool/InvokableTool两层GetTools()的核心逻辑30 行代码里发生了什么Schema 转换MCP 的InputSchema怎么变成 Eino 的*jsonschema.Schema两套 MCP SDK 适配的区别mark3labs vs 官方 SDKToolCallResultHandler工具返回后的拦截钩子DeepFlux 双向桥接出方向把 KB/Memory 暴露给外部 MCP 客户端一、MCP 协议是什么MCP 全称 Model Context Protocol规定了 AI 模型和外部工具之间的通信格式。底层是 JSON-RPC 2.0——所有消息长一个样{jsonrpc:2.0,id:1,method:tools/call,params:{name:calculate,arguments:{operation:add,x:3,y:5}}}你只需要关心三个方法方法用途initialize握手交换协议版本和能力tools/list查询 MCP Server 有哪些工具tools/call执行某个工具传输方式有三种stdio子进程管道、SSEHTTP 长连接、Streamable HTTP。对 Eino 来说这些全部是外部的——Eino 只认识自己定义的 Tool 接口不认识 MCP。所以需要一个适配层。二、Eino 的 Tool 接口Eino 把工具抽象为两层// 最小接口只提供元数据告诉 LLM 这个工具叫什么、有什么参数typeBaseToolinterface{Info(ctx context.Context)(*schema.ToolInfo,error)}// 可调用工具在 BaseTool 基础上增加执行能力typeInvokableToolinterface{BaseToolInvokableRun(ctx context.Context,argumentsInJSONstring,opts...Option)(string,error)}ToolInfo是关键结构typeToolInfostruct{NamestringDescstring*ParamsOneOf// 参数 schema支持两种形式}ParamsOneOf有两种创建方式NewParamsOneOfByParams()简化写法传map[string]*ParameterInfoNewParamsOneOfByJSONSchema()完整 JSON SchemaMCP 适配用这个只要你的对象实现了InvokableToolEino 的ToolsNode就能调它。目标很清晰把 MCP 工具包一层让它实现这个接口。三、GetTools()30 行代码做了三件事eino-ext 的核心函数在components/tool/mcp/mcp.gofuncGetTools(ctx context.Context,conf*Config)([]tool.BaseTool,error){// ① 向 MCP Server 查询工具列表listResults,err:conf.Cli.ListTools(ctx,mcp.ListToolsRequest{})ret:make([]tool.BaseTool,0)for_,t:rangelistResults.Tools{// ② 把 MCP 的 InputSchema 转换成 Eino 的格式marshaledInputSchema,_:sonic.Marshal(t.InputSchema)inputSchema:jsonschema.Schema{}sonic.Unmarshal(marshaledInputSchema,inputSchema)// ③ 创建包装器实现 InvokableTool 接口retappend(ret,toolHelper{cli:conf.Cli,info:schema.ToolInfo{Name:t.Name,Desc:t.Description,ParamsOneOf:schema.NewParamsOneOfByJSONSchema(inputSchema),},})}returnret,nil}第一步查工具列表conf.Cli.ListTools()发出一条 JSON-RPC 请求tools/list得到所有工具的名称、描述和参数 schema。第二步Schema 转换这是最容易出问题的地方。MCP SDK 里的InputSchema类型是map[string]interface{}动态类型而 Eino 需要的是强类型的*jsonschema.Schema。转换方式先 Marshal 成 JSON 字节再 Unmarshal 成目标类型。看起来绕实际上是最稳妥的做法——不依赖字段名映射不受 struct tag 影响。第三步包装器toolHelper是私有结构体持有 MCP 客户端引用。当 Eino 的ToolsNode需要执行工具时调用它的InvokableRun()func(m*toolHelper)InvokableRun(ctx context.Context,argumentsInJSONstring,opts...tool.Option)(string,error){result,err:m.cli.CallTool(ctx,mcp.CallToolRequest{Request:mcp.Request{Method:tools/call},Params:mcp.CallToolParams{Name:m.info.Name,Arguments:json.RawMessage(argumentsInJSON),// 直接透传不解析},})// ...returnsonic.MarshalString(result)}参数是 Eino 传来的 JSON 字符串直接作为json.RawMessage扔给 MCP结果序列化成字符串返回。没有额外解析、没有类型转换——整条路径的数据就是 JSON两端透传。四、完整调用链路用户输入 → LLM 决定调工具 ↓ Eino ToolsNode接收 ToolCall 消息工具名 JSON 参数 ↓ 找到对应的 toolHelper ↓ InvokableRun(argumentsInJSON) ↓ MCP Client 发 JSON-RPC: tools/call ↓ MCP Server 执行工具逻辑 ↓ JSON-RPC 响应 ↓ 序列化为字符串 → 返回给 LLM 作为 ToolMessageEino 看到的只是一个实现了接口的对象MCP Server 看到的只是标准 JSON-RPC中间层完全透明。五、接入只需三步// 1. 创建 MCP 客户端并握手cli,_:client.NewSSEMCPClient(http://your-mcp-server/sse)cli.Start(ctx)cli.Initialize(ctx,mcp.InitializeRequest{Params:mcp.InitializeRequestParams{ProtocolVersion:mcp.LATEST_PROTOCOL_VERSION,ClientInfo:mcp.Implementation{Name:my-agent,Version:1.0.0},},})// 2. 拉取工具并转换importmcpToolgithub.com/cloudwego/eino-ext/components/tool/mcptools,_:mcpTool.GetTools(ctx,mcpTool.Config{Cli:cli})// 3. 塞进 ToolsNodetoolsNode,_:compose.NewToolNode(ctx,compose.ToolsNodeConfig{Tools:tools})此后 Agent 能透明地调用这个 MCP Server 上的所有工具新增工具不需要改 Agent 代码——下次GetTools()自动发现。六、两套 SDK选哪个eino-ext 同时提供两套实现mark3labs 版本官方 SDK 版本包路径components/tool/mcpcomponents/tool/mcp/officialmcp底层github.com/mark3labs/mcp-gogithub.com/modelcontextprotocol/go-sdk运行时选项支持自定义 Header、Meta不支持分页不支持支持Cursor 参数两套核心逻辑几乎一样区别在选项支持。mark3labs 版本多了一个ToolCallResultHandlerconf:mcpTool.Config{Cli:cli,ToolCallResultHandler:func(ctx context.Context,namestring,result*mcp.CallToolResult)(*mcp.CallToolResult,error){// 工具返回后、交给 LLM 前的拦截点// 可以截断超长结果、过滤敏感内容、记日志returnresult,nil},}对于会返回大量文本的工具网页抓取、数据库查询这个钩子可以在这里裁剪避免单次工具输出把 context 撑爆。七、DeepFlux 的双向桥接eino-ext 的适配是单向的外部 MCP → Eino Tool入方向。DeepFlux 在此基础上做了出方向把平台自己的知识库KB、长期记忆Memory、提示词模板暴露给外部 MCP 客户端Cursor、Cline 等。入方向eino-ext 已解决外部 MCP 工具 → Eino Tool → DeepFlux Agent 能用出方向DeepFlux 新增server/internal/mcp/bridge.go// 知识库 → MCP Resourcedeepflux://kb/namespacetypekbResourcesstruct{svc KBService}// 知识库搜索 → MCP Toolkb_searchtypekbSearchToolstruct{svc KBService}func(k*kbSearchTool)Call(ctx context.Context,raw json.RawMessage)(json.RawMessage,error){// 解析参数调 KB 服务返回 top-K 结果}// 长期记忆 → 两个 MCP Tool// memory_recall召回// memory_write写入高风险接了 HITL 审批RegisterAll把这些挂载到 MCP Server同时配置 HITLfuncRegisterAll(srv*Server,kb KBService,mem MemoryService,opts BridgeOptions){srv.RegisterResources(kbResources{svc:kb})srv.RegisterTool(kbSearchTool{svc:kb})srv.RegisterTool(memoryRecallTool{svc:mem})srv.RegisterTool(memoryWriteTool{svc:mem})// memory_write 是高风险操作触发人工审批srv.SetHITL(func(ctx context.Context,toolstring,input json.RawMessage)(bool,string){ifhighRisk[tool]opts.HITL!nil{returnopts.HITL.Decide(ctx,tool,input)}returntrue,})}两者对比维度eino-ext MCP 适配DeepFlux MCP 桥接方向单向入双向入 出职责协议转换通用业务语义暴露领域特定暴露类型ToolTool Resource Prompt安全控制无HITL 审批高风险工具传输层SSE / stdiostdio / SSE / Streamable HTTPeino-ext 解决语言不通MCP 和 Eino 接口不兼容DeepFlux 解决门没开让外部工具能访问平台自己的数据。小结MCP 协议不复杂JSON-RPC 2.0 打底三个方法initialize / tools/list / tools/call三种传输方式stdio / SSE / Streamable HTTP。eino-ext 的适配核心是GetTools()查工具列表 → Schema 类型转换JSON 序列化绕一圈→ 包装成toolHelper实现 Eino 接口。参数透传结果透传没有多余逻辑。DeepFlux 在此基础上做了反向把 KB 和 Memory 通过 MCP 协议暴露出去同时在高风险写操作上接入 HITL 审批。协议桥接的价值是生态共用遵守 MCP 协议的工具不管是谁提供的接进 Eino 都是同一套代码。下篇继续。代码来源cloudwego/eino-ext · cloudwego/eino-examples