通过本文掌握 MCP 协议与 Eino 工具集成的核心要点
MCP(Model Context Protocol)近期热度极高,行业内外广泛讨论。但坦白说,该协议本身并不复杂——它解决的核心问题其实很朴素:当 AI 模型需要调用外部工具时,双方必须建立一套公认的“通信规范”。
本文将从协议基础讲起,深入解析 Eino 框架如何将 MCP 工具“转换”为自身可识别的 Tool 接口。最后补充 DeepFlux 在双向桥接方面的实践方案,展示如何将平台内部能力通过 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(子进程管道)、SSE(HTTP 长连接)、Streamable HTTP。
关键挑战在于:Eino 并不直接支持 MCP,它只识别自身定义的 Tool 接口。因此中间需要一层适配器来完成“协议转换”。
二、Eino 的 Tool 接口设计
Eino 将工具抽象为两个层级:
// 最小接口:仅提供元数据(告知 LLM 工具名称及参数信息)
type BaseTool interface {
Info(ctx context.Context) (*schema.ToolInfo, error)
}
// 可调用工具:在 BaseTool 基础上增加执行能力
type InvokableTool interface {
BaseTool
InvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)
}
其中 ToolInfo 是关键数据结构:
type ToolInfo struct {
Name string
Desc string
ParamsOneOf // 参数 schema,支持两种表现形式
}
ParamsOneOf 有两种创建方式:使用 NewParamsOneOfByParams() 通过简化写法传入 map[string]*ParameterInfo,或使用 NewParamsOneOfByJSONSchema() 传入完整 JSON Schema——后者正是适配 MCP 时所需的方式。
只要对象实现了 InvokableTool 接口,Eino 的 ToolsNode 即可调用它。因此目标非常明确:将 MCP 工具封装一层,使其实现该接口。
三、GetTools():30 行代码完成三个关键任务
eino-ext 的核心函数位于 components/tool/mcp/mcp.go:
func GetTools(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 := range listResults.Tools {
// ② 将 MCP 的 InputSchema 转换为 Eino 兼容格式
marshaledInputSchema, _ := sonic.Marshal(t.InputSchema)
inputSchema := &jsonschema.Schema{}
sonic.Unmarshal(marshaledInputSchema, inputSchema)
// ③ 创建包装器,实现 InvokableTool 接口
ret = append(ret, &toolHelper{
cli: conf.Cli,
info: &schema.ToolInfo{
Name: t.Name,
Desc: t.Description,
ParamsOneOf: schema.NewParamsOneOfByJSONSchema(inputSchema),
},
})
}
return ret, nil
}
第一步:查询工具列表。conf.Cli.ListTools() 发送 tools/list 的 JSON-RPC 请求,获取所有工具的名称、描述及参数 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, argumentsInJSON string, 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), // 直接透传,不解析
},
})
// ...
return sonic.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 作为 ToolMessage
Eino 仅看到一个实现了接口的对象,MCP Server 仅看到标准 JSON-RPC 消息,中间的适配层完全透明。
五、三步完成 MCP 工具接入
实际使用中,接入 MCP 工具仅需三步:
// 1. 创建 MCP 客户端并完成握手
cli, _ := client.NewSSEMCPClient("https://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. 拉取工具列表并完成转换
import mcpTool "github.com/cloudwego/eino-ext/components/tool/mcp"
tools, _ := mcpTool.GetTools(ctx, &mcpTool.Config{Cli: cli})
// 3. 注入 ToolsNode
toolsNode, _ := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{Tools: tools})
之后 Agent 即可透明地调用该 MCP Server 上的所有工具。新增工具无需修改 Agent 代码——下一次 GetTools() 调用时会自动发现。
六、两套 SDK 如何选择
eino-ext 同时提供了两套实现,这一细节值得关注:
| 对比维度 | mark3labs 版本 | 官方 SDK 版本 |
|---|---|---|
| 包路径 | components/tool/mcp | components/tool/mcp/officialmcp |
| 底层依赖 | github.com/mark3labs/mcp-go | github.com/modelcontextprotocol/go-sdk |
| 运行时选项 | 支持(自定义 Header、Meta) | 不支持 |
| 分页能力 | 不支持 | 支持(Cursor 参数) |
两套核心逻辑几乎一致,差异主要体现在选项支持上。mark3labs 版本额外提供了 ToolCallResultHandler,这个功能非常实用:
conf := &mcpTool.Config{
Cli: cli,
ToolCallResultHandler: func(ctx context.Context, name string, result *mcp.CallToolResult) (*mcp.CallToolResult, error) {
// 工具返回后、交给 LLM 前的拦截点
// 可用于:截断超长输出、过滤敏感内容、记录日志
return result, nil
},
}
对于返回大量文本的工具(例如网页抓取、数据库查询),该钩子可在返回前进行裁剪,避免单次工具输出撑爆上下文。
七、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 Resource(deepflux://kb/)
type kbResources struct { svc KBService }
// 知识库搜索 → MCP Tool(kb_search)
type kbSearchTool struct { 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,同时配置了 HITL(人工介入审批):
func RegisterAll(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, tool string, input json.RawMessage) (bool, string) {
if highRisk[tool] && opts.HITL != nil {
return opts.HITL.Decide(ctx, tool, input)
}
return true, ""
})
}
两者对比总结:
| 维度 | eino-ext MCP 适配 | DeepFlux MCP 桥接 |
|---|---|---|
| 方向 | 单向(入) | 双向(入 + 出) |
| 职责 | 协议转换(通用) | 业务语义暴露(领域特定) |
| 暴露类型 | Tool | Tool + Resource + Prompt |
| 安全控制 | 无 | HITL 审批高风险工具 |
| 传输层 | SSE / stdio | stdio / SSE / Streamable HTTP |
简而言之,eino-ext 解决的是“语言不通”的问题——MCP 与 Eino 接口不兼容;DeepFlux 解决的是“门没开”的问题——让外部工具能够访问平台自有数据。
小结
回到开篇的观点——MCP 协议其实并不复杂。底层依赖 JSON-RPC 2.0,核心只有三个方法(initialize / tools/list / tools/call),三种传输方式(stdio / SSE / Streamable HTTP),仅此而已。
eino-ext 的适配核心集中在 GetTools() 这 30 行代码:查询工具列表 → Schema 类型转换(通过 JSON 序列化绕一圈)→ 包装为 toolHelper 实现 Eino 接口。参数透传,结果透传,无冗余逻辑。
DeepFlux 在此基础上增加了反向能力,将 KB 和 Memory 通过 MCP 协议暴露,同时对高风险写操作引入了 HITL 审批机制。
协议桥接的真正价值在于生态复用:凡是遵守 MCP 协议的工具,无论由谁提供,接入 Eino 都使用同一套代码。这正是标准化的魅力——它让“连接”变得可预期、可复用。
下篇继续。
代码来源:cloudwego/eino-ext · cloudwego/eino-examples
