本系列将从零开始,用 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 消息来完成的。

其核心交互流程如下:

  1. 连接与初始化 (initialize): Agent 启动时,会连接到各个工具 Server。连接成功后,Agent 会发送一个 initialize 请求,询问对方“你是谁,你有什么能力?”
  2. 获取工具列表 (tools/list): 初始化成功后,Agent 会向工具 Server 发送 tools/list 请求,获取该 Server 提供的所有工具的详细列表(名称、描述、参数等)。
  3. 提供给 LLM: Agent 将从所有工具 Server 收集到的工具信息,整合后提供给 LLM。
  4. 调用工具 (tools/call): 当 LLM 决定使用某个工具时,Agent 会向对应的工具 Server 发送一个 tools/call 请求,其中包含了工具名和参数。
  5. 返回结果: 工具 Server 执行工具,并将结果返回给 Agent。
  6. 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 现在拥有了更丰富的工具集。它能够按部就班地:

  1. 调用 filesystem 工具的 writeFile 方法,将诗歌写入 a.txt。
  2. 调用 git 工具的 stage 方法,将 a.txt 添加到暂存区。
  3. 调用 git 工具的 commit 方法,提交这次改动。

MCP初始化
在这里插入图片描述

工具注册
在这里插入图片描述

Agent Loop
在这里插入图片描述

第四部分小结

通过引入 MCP 协议,我们成功地将 Agent 与工具进行了解耦,构建了一个可扩展的工具生态。我们的 Agent 现在可以像使用“即插即用”设备一样,动态地集成任意符合规范的外部工具。

然而,新的问题又出现了:当工具库变得异常庞大时(想象一下成百上千个工具),LLM 会“眼花缭乱”。将所有工具的描述都塞进 Prompt,不仅会急剧消耗宝贵的上下文窗口,还会因为信息过载而导致 LLM 难以准确选择合适的工具。

此外,对于一些固定的、多步骤的流程(比如“代码评审”:先拉取分支、再分析改动、最后给出评论),我们希望 Agent 能有一个“标准作业流程”(SOP),而不是每次都靠自己“临场发挥”。

如何解决这两个问题?下一部分,我们将引入一个更高层次的抽象:Skill。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐