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 来说,这些全部是外部的——Eino 只认识自己定义的 Tool 接口,不认识 MCP。所以需要一个适配层。
Eino 把工具抽象为两层:
// 最小接口:只提供元数据(告诉 LLM 这个工具叫什么、有什么参数)type BaseTool interface {Info(ctx context.Context) (*schema.ToolInfo, error)}// 可调用工具:在 BaseTool 基础上增加执行能力type InvokableTool interface {BaseToolInvokableRun(ctx context.Context, argumentsInJSON string, opts ...Option) (string, error)}
ToolInfo 是关键结构:
type ToolInfo struct {NamestringDescstring*ParamsOneOf// 参数 schema,支持两种形式}
ParamsOneOf 有两种创建方式:
NewParamsOneOfByParams():简化写法,传 map[string]*ParameterInfoNewParamsOneOfByJSONSchema():完整 JSON Schema,MCP 适配用这个只要你的对象实现了 InvokableTool,Eino 的 ToolsNode 就能调它。目标很清晰:把 MCP 工具包一层,让它实现这个接口。
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() 发出一条 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, 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,中间层完全透明。
// 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. 拉取工具并转换import mcpTool "github.com/cloudwego/eino-ext/components/tool/mcp"tools, _ := mcpTool.GetTools(ctx, &mcpTool.Config{Cli: cli})// 3. 塞进 ToolsNodetoolsNode, _ := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{Tools: tools})
此后 Agent 能透明地调用这个 MCP Server 上的所有工具,新增工具不需要改 Agent 代码——下次 GetTools() 自动发现。
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},}
对于会返回大量文本的工具(网页抓取、数据库查询),这个钩子可以在这里裁剪,避免单次工具输出把 context 撑爆。
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/<namespace>)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():查工具列表 → Schema 类型转换(JSON 序列化绕一圈)→ 包装成 toolHelper 实现 Eino 接口。参数透传,结果透传,没有多余逻辑。
DeepFlux 在此基础上做了反向:把 KB 和 Memory 通过 MCP 协议暴露出去,同时在高风险写操作上接入 HITL 审批。
协议桥接的价值是生态共用:遵守 MCP 协议的工具,不管是谁提供的,接进 Eino 都是同一套代码。
下篇继续。
代码来源:cloudwego/eino-ext · cloudwego/eino-examples