动手写个agent(四):实现接入MCP 协议
本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)
本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent
代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)
本节目标:引入MCP协议,实现工具的动态、可插拔加载,解耦 Agent 与工具。
第四部分:构建标准化的工具生态:MCP 协议
随着我们为 Agent 添加的工具越来越多,一个严峻的问题浮出水面:Agent 的核心逻辑与工具的具体实现紧密耦合在了一起。每增加或修改一个工具,我们都可能需要改动 Agent 的主程序。这使得工具的维护和扩展变得异常困难。
问题:如何将工具从 Agent 中解耦出来,形成一个独立、可插拔的“工具生态系统”,让任何符合规范的工具都能被 Agent 即插即用?解决方案:建立一套标准的通信协议。
这个想法借鉴了软件工程中的经典模式:没有什么问题是增加一个抽象层解决不了的。我们可以在 Agent(作为工具的消费者)和工具(作为能力的提供者)之间,定义一套标准的“对话语言”。只要双方都遵循这套语言,就能彼此协作,而无需关心对方的内部实现。
这个协议,就是MCP(Model Context Protocol:模型上下文协议)。你可以把它类比成计算机的 USB 协议:只要你的设备(U盘、鼠标、键盘)遵循 USB 规范,就可以插入任何一台有 USB 接口的电脑上正常工作,而电脑无需为每一种设备都安装专门的驱动。
在 MCP 的世界里,Agent 就是那台“电脑”,而各种外部工具就是“USB设备”。
4.1 MCP 核心概念
MCP 是一套基于 JSON-RPC 2.0 的双向通信规范。这意味着 Agent (Client) 和工具提供方 (Server) 之间的所有交互,都是通过发送和接收结构化的 JSON 消息来完成的。
其核心交互流程如下:
- 连接与初始化 (initialize): Agent 启动时,会连接到各个工具 Server。连接成功后,Agent 会发送一个 initialize 请求,询问对方“你是谁,你有什么能力?”
- 获取工具列表 (tools/list): 初始化成功后,Agent 会向工具 Server 发送 tools/list 请求,获取该 Server 提供的所有工具的详细列表(名称、描述、参数等)。
- 提供给 LLM: Agent 将从所有工具 Server 收集到的工具信息,整合后提供给 LLM。
- 调用工具 (tools/call): 当 LLM 决定使用某个工具时,Agent 会向对应的工具 Server 发送一个 tools/call 请求,其中包含了工具名和参数。
- 返回结果: 工具 Server 执行工具,并将结果返回给 Agent。
- Agent Loop 继续: Agent 将工具执行结果喂给 LLM,继续它的思考循环。

补充说明:JSON-RPC 2.0 消息格式
上述流程中的消息都遵循 JSON-RPC 2.0 格式,示例:
请求json消息:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_pipeline",
"arguments": {
"pipelineId": "pipeline-123",
"projectId": "my-project"
}
}
}
响应json消息:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Pipeline 'deploy-frontend' 状态: SUCCESS\n开始时间: 2026-03-26T10:00:00Z\n结束时间: 2026-03-26T10:05:23Z"
}
],
"isError": false
}
}
Client和Server之间的传输方式,MCP官方目前定义了两种:
本地 stdio(标准输入 / 输出,用子进程标准输入输出通信):-
server作为client的一个子进程
-
所有的 JSON-RPC 消息通过写入子进程的 stdin(标准输入)发送到 Server,并通过读取子进程的 stdout(标准输出)接收 Server 的响应
-
这种方式适合什么场景?
- MCP Server 是本地可执行程序
- 我们把它启动起来
- 通过 stdin/stdout 跟它说话
-
HTTP / SSE(Server-Sent Events)远程传输,有些 MCP Server 不是本地进程,而是远程 HTTP 服务:- 用于客户端与远程服务器的通信。
- 客户端首先通过连接服务器的 SSE 接口建立一个持久化的下行连接(Server 到 Client 的数据流)。
- 服务器在连接建立时,会通过 SSE 下发一个 endpoint 事件,告诉客户端后续的请求应该发送到哪个 HTTP URI。
- 客户端向服务器发送 JSON-RPC 请求时,使用 HTTP POST 方法将请求体发送至上述 endpoint。
4.2 实现 MCP 客户端
要在我们的 Agent 中支持 MCP,核心任务是实现一个 MCP Client。这个客户端负责与外部的 MCP Server 进行通信。
由于 MCP 的实现细节较为繁复(涉及 JSON-RPC 消息封装、stdio 和 HTTP 两种传输方式等),我们在此不展示完整的代码(完整代码可在仓库中查看),而是聚焦于其核心的接口和结构。
配置 (ServerConfig)
首先,我们需要一种方式来配置我们要连接的 MCP Server。
// ServerConfig MCP 服务器配置
type ServerConfig struct {
Name string `json:"name"` // 服务器名称
Transport string `json:"transport"` // 传输方式:"stdio" | "http"
Enabled bool `json:"enabled"` // 是否启用
// stdio 传输配置 (用于本地子进程工具)
Command string `json:"command,omitempty"` // 启动命令
Args []string `json:"args,omitempty"` // 命令参数
// HTTP 传输配置 (用于远程工具服务)
// HTTP 传输配置
URL string `json:"url,omitempty"` // MCP 端点 URL
Headers map[string]string `json:"headers,omitempty"` // 请求头
Timeout int `json:"timeout,omitempty"` // 超时(秒)
}
客户端管理器 (Manager)
当有多个 MCP Server 时,我们需要一个管理器来统一维护与它们的连接,并聚合所有工具。
// internal/mcp/manager.go
package mcp
import (
"chapter4/mcp/transport"
"chapter4/types"
"context"
"fmt"
"sync"
"github.com/rs/zerolog/log"
)
// Manager MCP 服务器管理器
type Manager struct {
clients map[string]*Client // 服务器名称 -> 客户端
mu sync.RWMutex
}
// NewManager 创建管理器
func NewManager() *Manager {
return &Manager{
clients: make(map[string]*Client),
}
}
// AddServer 添加服务器
func (m *Manager) AddServer(config *types.ServerConfig) error {
if !config.Enabled {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
if _, exists := m.clients[config.Name]; exists {
log.Warn().Str("server", config.Name).Msg("服务器已存在")
return fmt.Errorf("服务器已存在: %s", config.Name)
}
client, err := NewClient(config)
if err != nil {
log.Error().Err(err).Str("server", config.Name).Msg("创建客户端失败")
return fmt.Errorf("创建客户端失败: %w", err)
}
m.clients[config.Name] = client
return nil
}
// ConnectAll 连接所有服务器
func (m *Manager) ConnectAll(ctx context.Context) error {
m.mu.RLock()
clients := make([]*Client, 0, len(m.clients))
for _, client := range m.clients {
clients = append(clients, client)
}
m.mu.RUnlock()
var wg sync.WaitGroup
errCh := make(chan error, len(clients))
for _, client := range clients {
wg.Add(1)
go func(c *Client) {
defer wg.Done()
log.Info().Str("server", c.GetServerName()).Msg("[MCP] 尝试连接")
if err := c.Connect(ctx); err != nil {
errCh <- fmt.Errorf("连接 %s 失败: %w", c.GetServerName(), err)
}
}(client)
}
wg.Wait()
close(errCh)
// 收集错误(非致命,只打印警告)
for err := range errCh {
log.Warn().Err(err).Msg("[MCP] 警告")
}
return nil
}
// GetAllTools 获取所有服务器的工具
func (m *Manager) GetAllTools() []MCPTool {
m.mu.RLock()
defer m.mu.RUnlock()
var allTools []MCPTool
for serverName, client := range m.clients {
tools := client.GetTools()
for _, tool := range tools {
allTools = append(allTools, MCPTool{
ServerName: serverName,
Tool: tool,
})
}
}
return allTools
}
// MCPTool 带服务器信息的工具
type MCPTool struct {
ServerName string
Tool transport.Tool
}
// CallTool 调用指定服务器的工具
func (m *Manager) CallTool(ctx context.Context, serverName, toolName string, arguments map[string]interface{}) (*transport.ToolsCallResult, error) {
m.mu.RLock()
client, ok := m.clients[serverName]
m.mu.RUnlock()
if !ok {
log.Error().Str("server", serverName).Msg("服务器不存在")
return nil, fmt.Errorf("服务器不存在: %s", serverName)
}
return client.CallTool(ctx, toolName, arguments)
}
适配器 (MCPToolAdapter)
有了 MCP Manager,我们如何将这些通过 MCP 协议获取的“远程”工具,无缝地集成到我们现有的、基于 Tool 接口的工具注册器(Registry)中呢?
答案是适配器模式。
我们创建一个 MCPToolAdapter,它本身实现了我们的 Tool 接口,但其内部的 Execute 方法,并不是自己执行具体逻辑,而是将调用请求转发给 Manager,由 Manager 去与真正的 MCP Server 通信。
通过这层巧妙的适配,Agent 的核心循环(Agent Loop)完全无需关心一个工具是本地实现的,还是通过 MCP 远程调用的。在它看来,所有的工具都只是实现了 Tool 接口的对象而已。
注意:不是一个MCP服务对应一个MCPToolAdapter,而是MCP服务里面的每一个工具(方法)都会对应一个MCPToolAdapter。
// MCPToolAdapter 将 MCP 工具适配为本地 Tool 接口
type MCPToolAdapter struct {
BaseTool
manager *mcp.Manager
serverName string
tool transport.Tool
}
// NewMCPToolAdapter 创建适配器
func NewMCPToolAdapter(manager *mcp.Manager, serverName string, mcpTool transport.Tool) *MCPToolAdapter {
return &MCPToolAdapter{
BaseTool: BaseTool{
name: fmt.Sprintf("mcp_%s_%s", serverName, mcpTool.Name),
description: fmt.Sprintf("[MCP:%s] %s", serverName, mcpTool.Description),
parameters: mcpTool.InputSchema,
},
manager: manager,
serverName: serverName,
tool: mcpTool,
}
}
// Execute 将执行请求转发给 MCP Manager
func (a *MCPToolAdapter) Execute(ctx context.Context, params json.RawMessage) (string, error) {
var arguments map[string]interface{}
if err := json.Unmarshal(params, &arguments); err != nil {
return "", fmt.Errorf("参数解析失败: %w", err)
}
result, err := a.manager.CallTool(ctx, a.serverName, a.tool.Name, arguments)
if err != nil {
return "", err
}
// ... 格式化返回结果
return formattedResult, nil
}
最后,我们为工具注册器 Registry 添加一个新方法,让它可以方便地将一个 Manager 中的所有 MCP 工具都注册进来。
// RegisterMCPTools 注册所有来自 MCP Manager 的工具
func (r *Registry) RegisterMCPTools(manager *mcp.Manager) {
mcpTools := manager.GetAllTools()
for _, mcpTool := range mcpTools {
adapter := NewMCPToolAdapter(manager, mcpTool.ServerName, mcpTool.Tool)
if err := r.Register(adapter); err != nil {
log.Error().Err(err).Str("tool", adapter.Name()).Msg("[MCP] 注册工具失败")
} else {
log.Info().Str("tool", adapter.Name()).Msg("[MCP] 已注册工具")
}
}
}
4.3 实战:让 Agent 使用 MCP 工具库
现在,我们将为 Agent 配置两个标准的 MCP 工具:filesystem(提供文件系统操作能力)和 git(提供 Git 操作能力)。这两个工具都是开源社区维护的、遵循 MCP 规范的独立程序。
首先,我们改一下Config的结构体,让它支持MCP服务的配置
// Config LLM配置
type Config struct {
APIKey string `json:"api_key" yaml:"api_key"`
BaseURL string `json:"base_url" yaml:"base_url"`
Model string `json:"model" yaml:"model"`
Temperature float64 `json:"temperature" yaml:"temperature"`
MaxTokens int `json:"max_tokens" yaml:"max_tokens"`
Timeout int `json:"timeout" yaml:"timeout"`
MCPServerConfig []ServerConfig `json:"mcp_server_config" yaml:"mcp_server_config"`
}
// ServerConfig MCP 服务器配置
type ServerConfig struct {
Name string `json:"name"` // 服务器名称
Transport string `json:"transport"` // "stdio" | "http"
Enabled bool `json:"enabled"` // 是否启用
// stdio 传输配置
Command string `json:"command,omitempty"` // 启动命令
Args []string `json:"args,omitempty"` // 命令参数
Env map[string]string `json:"env,omitempty"` // 环境变量
// HTTP 传输配置
URL string `json:"url,omitempty"` // MCP 端点 URL
Headers map[string]string `json:"headers,omitempty"` // 请求头
Timeout int `json:"timeout,omitempty"` // 超时(秒)
}
然后,在我们的 config.json 中添加它们的配置。我们使用 stdio 传输方式,这意味着我们的 Agent 会在后台启动这两个工具的进程,并通过标准输入/输出与它们通信。
{
"base_url": "https://ark-cn-beijing.bytedance.net/api/v3/chat/completions",
"api_key": "MY_VOLCENGINE_TOKEN",
"model": "doubao-seed-2-0-code-preview-260215",
"temperature": 0.7,
"max_tokens": 10000,
"timeout": 120,
"mcp_server_config": [
{
"name": "filesystem",
"transport": "stdio",
"command": "npx",
"args":["-y", "@modelcontextprotocol/server-filesystem", "./"],
"enabled": true
},
{
"name": "github",
"transport": "stdio",
"command": "uvx",
"args":["mcp-server-git", "--repository", "./"],
"enabled": true
}
]
}
接着,在 main 函数中,我们初始化 Manager,连接所有 MCP Server,并将它们提供的工具注册到我们的 toolRegistry 中。
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
config, err := loadConfig("../config.json")
if err != nil {
log.Fatal().Err(err).Msg("加载配置失败")
}
// 创建LLM客户端
client := llm.NewOpenAIClient(config)
// Tool注册器
toolRegistry := tool.NewRegistry()
// 注册shell工具
toolRegistry.Register(tool.NewShellTool(10 * time.Second))
// mcp管理器
manager := mcp.NewManager()
// 添加配置中的MCP服务器到MCP管理器中
for _, server := range config.MCPServerConfig {
log.Info().Str("server", server.Name).Msg("[MCP] 配置服务器")
manager.AddServer(server)
}
// 与所有的MCP服务器建立连接
manager.ConnectAll(context.Background())
// 将所有MCP服务的工具通过适配器注册到工具列表
toolRegistry.RegisterMCPTools(manager)
// 创建智能体,持有LLM客户端和工具列表
myAgent := agent.NewAgent("MyAgent", "", 10, client, toolRegistry)
// 启动Agent Loop
answer, err := myAgent.Run(context.Background(), "以歌德的口吻,写一个春天的诗,并用git提交")
if err != nil {
log.Fatal().Err(err).Msg("运行Agent失败")
}
fmt.Println(answer)
}
运行程序,你会看到 Agent 现在拥有了更丰富的工具集。它能够按部就班地:
- 调用 filesystem 工具的 writeFile 方法,将诗歌写入 a.txt。
- 调用 git 工具的 stage 方法,将 a.txt 添加到暂存区。
- 调用 git 工具的 commit 方法,提交这次改动。
MCP初始化
工具注册
Agent Loop
第四部分小结
通过引入 MCP 协议,我们成功地将 Agent 与工具进行了解耦,构建了一个可扩展的工具生态。我们的 Agent 现在可以像使用“即插即用”设备一样,动态地集成任意符合规范的外部工具。
然而,新的问题又出现了:当工具库变得异常庞大时(想象一下成百上千个工具),LLM 会“眼花缭乱”。将所有工具的描述都塞进 Prompt,不仅会急剧消耗宝贵的上下文窗口,还会因为信息过载而导致 LLM 难以准确选择合适的工具。
此外,对于一些固定的、多步骤的流程(比如“代码评审”:先拉取分支、再分析改动、最后给出评论),我们希望 Agent 能有一个“标准作业流程”(SOP),而不是每次都靠自己“临场发挥”。
如何解决这两个问题?下一部分,我们将引入一个更高层次的抽象:Skill。
更多推荐

所有评论(0)