MCP简介和应用
1. MCP 简解:
你可以把 MCP 想象成 AI 应用的 USB-C 接口。就像 USB-C 数据线标准化了笔记本电脑连接各种外设(如外置硬盘、显示器或充电器)的方式一样,MCP 为 LLM 连接不同的数据源和工具提供了一个标准化的接口。它是一个开放协议,定义了应用程序如何为 LLM 提供上下文,本质上是赋予了它们“手脚”,让它们能够伸向现实世界并执行各种操作。
MCP 整个运作流程的主要组成部分有客户端、LLM、和 MCP 服务端三部分组成。
- 客户端: 这是你与之交互的界面,通常是集成 LLM 的 AI 助手或应用程序。例如,Claude 的官方客户端,或者 Cherry-Studio 等第三方工具。客户端扮演着视觉载体和整体用户体验的整合者角色。
- LLM(大语言模型): 这是“大脑”——AI 模型的核心。目前主流的大模型,如 DeepSeek、千问 3 等,都提供了 MCP 标准扩展能力。虽然技术上 LLM 通过“主机”(Host)与外部交互,为了便于理解,我们在此统称为 LLM。
- MCP 服务器: 这是实现 LLM 扩展能力的基础。LLM 本身并不具备这些能力,MCP 服务器通过实现这些能力(比如读取桌面文件、查询天气),并将调用方式抽象成接口暴露给 LLM。本质上,它将 LLM 的请求转化为现实世界中的行动。
整个 MCP 流程是通过 LLM 与 MCP 服务器的交互来实现的。客户端则负责提供可视化的载体和工程上的整合。。
现在这种 MCP 流程根据 LLM 和 MCP 服务器在不同的位置有不同的组合
-
本地客户端 + 本地 MCP + 云LLM: 这是常见的设置,例如 Claude 官方建议下载其客户端并集成 MCP 服务器。在这种模式下,客户端和 MCP 服务器都在你的本地设备上运行,处理与本地系统的交互(如访问本地文件),而 LLM 本身则部署在云端。
-
云LLM+云MCP+Web:而在阿里云百练的官网,官方给出了云LLM+云MCP+Web的组合(这是官方主推的方式,他们显然不想把客户端市场拱手让给 Cherry-Studio 、Cline等第三方客户端)。这种 LLM 和 MCP 在一端的理论上延迟很低。但是目前 MCP 处于蓬勃发展期间,这种不自由的方式显然不是各位玩家的热爱(毕竟云托管要付一份额外的钱)。
值的注意的是,虽然阿里云嘴上说本地不支持,实际上预留了标准的MCP调用规范,这为本地化操作提供了更多可能性。
2. MCP实践
2.1 MCP客户端
MCP客户端,往往来说目前市面上相对较少,目前除了各个大厂,免费的第三方客户端集成较好的有 Cherry-Studio 、Cline。
他们在实现集成各种第三方API的同时也实现了MCP的一个中继器,可以转发 LLM 和 MCP 服务器之间的调用操作。
以 Cherry-Studio 为例,客户端在收到 MCP 包含 MCP 命令的消息的时候,会调用 MCP服务器,并发 MCP 服务器反馈的结果组合成新的信息后发送给 LLM
当大语言模型(LLM)需要使用 MCP 工具时,您的客户端与 LLM 之间的通信通常涉及一个两步过程。这就是为什么它通常比简单查询消耗更多 Token 的原因。让我们通过手动模拟一个虚假的 MCP 工具注册和观察,来详细解析这个流程。
- LLM 决定并指令
客户端发送: 你的问题 + 可用的 MCP 工具定义(比如你的“calculate”工具)。
LLM 回复: LLM 分析后,如果需要工具,它会直接返回工具调用指令(例如,调用“calculate”并带参数“2 + 2”)。
注意: 此时 LLM 不会给出任何自然语言的答案或额外信息,它只是发出指令。
- 客户端执行并反馈,LLM 给出答案
客户端行动: 客户端收到指令后,会真正调用你的 MCP 服务器(“calculate”服务)执行操作,并获取结果(例如,“4”)。
客户端发送: 客户端将工具的执行结果添加到对话历史中,并再次发送给 LLM。
LLM 回复: 收到工具结果后,LLM 才能理解并给出最终的、完整的自然语言回答(例如,“2 + 2 等于 4。”)。
为什么两次?
LLM 自身不能执行代码,它只负责“决策”(决定用哪个工具)和“语言生成”(整合结果)。客户端负责“执行”和“协调”。这种“决策-执行-整合”的模式,导致了两次与 LLM 的通信,也因此消耗了更多 Token。
{"Response": {"choices": [{"message": {"content": "","role": "assistant","tool_calls": [{"index": 0,"id": "call_13b96f2bb72d4b54afd0b4","type": "function","function": {"name": "calculate","arguments": "{\"expression\": \"2 + 2\"}"}}]},"finish_reason": "tool_calls","index": 0,"logprobs": null}],"object": "chat.completion","usage": {"prompt_tokens": 190,"completion_tokens": 19,"total_tokens": 209,"prompt_tokens_details": {"cached_tokens": 0}},"created": 1749265696,"system_fingerprint": null,"model": "qwen-plus","id": "chatcmpl-219316df-c150-9053-816b-123437e432bf"}
}
package client_demoimport ("bytes""encoding/json""fmt""io/ioutil""net/http""testing"
)// 定义请求数据结构
type Message struct {Role string `json:"role"`Content string `json:"content,omitempty"` // Content can be omitted for tool callsToolCalls []ToolCall `json:"tool_calls,omitempty"`
}type ToolCall struct {ID string `json:"id"`Type string `json:"type"`Function FunctionCall `json:"function"`
}type FunctionCall struct {Name string `json:"name"`Arguments map[string]interface{} `json:"arguments"`
}type Request struct {Model string `json:"model"`Messages []Message `json:"messages"`// Add tools definition to the requestTools []Tool `json:"tools,omitempty"`
}type Tool struct {Type string `json:"type"`Function Function `json:"function"`
}type Function struct {Name string `json:"name"`Description string `json:"description,omitempty"`Parameters map[string]interface{} `json:"parameters,omitempty"`
}func TestClient(t *testing.T) {// 获取API Key(确保已设置环境变量DASHSCOPE_API_KEY)apiKey := "" // Replace with your actual API Key// Define your MCP tool "calculate"// This describes the tool to the LLMcalculateTool := Tool{Type: "function",Function: Function{Name: "calculate",Description: "A tool to perform mathematical calculations.",Parameters: map[string]interface{}{"type": "object","properties": map[string]interface{}{"expression": map[string]interface{}{"type": "string","description": "The mathematical expression to evaluate.",},},"required": []string{"expression"},},},}// Construct the request body with the tool definitionrequestBody := Request{Model: "qwen-plus",Messages: []Message{{Role: "system",Content: "You are a helpful assistant with access to a calculator tool.",},{Role: "user",Content: "计算 2 + 2 等于多少?", // This is the user's query that might trigger the tool},},Tools: []Tool{calculateTool}, // Crucial: Register your tool with the LLM}// Serialize request bodyjsonBody, err := json.Marshal(requestBody)if err != nil {panic(fmt.Sprintf("Error marshaling JSON: %v", err))}fmt.Printf("Request Body: %s\n", jsonBody) // Print the request body for debugging// Create HTTP requestreq, err := http.NewRequest("POST","https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",bytes.NewBuffer(jsonBody))if err != nil {panic(fmt.Sprintf("Error creating request: %v", err))}// Set request headersreq.Header.Set("Authorization", "Bearer "+apiKey)req.Header.Set("Content-Type", "application/json")// For DashScope, adding X-DashScope-Api-Sequence might be useful for certain features,// but for basic tool calls, it's often not strictly necessary.// Send requestclient := &http.Client{}resp, err := client.Do(req)if err != nil {panic(fmt.Sprintf("Error sending request: %v", err))}defer resp.Body.Close()// Read responsebody, err := ioutil.ReadAll(resp.Body)if err != nil {panic(fmt.Sprintf("Error reading response: %v", err))}// Handle responseif resp.StatusCode != http.StatusOK {panic(fmt.Sprintf("API returned status %d: %s", resp.StatusCode, body))}fmt.Printf("Response: %s\n", body)}
2.2 MCP服务器
上面说了客户端如何执行 MCP 指令的,下面我们来介绍一下如何使用 Go 创建一个MCP 服务,MCP服务器本质来说和GRPC的接口一致,一般来说 MCP 服务器没有状态,所以说也不需要常驻运行。
MCP 通信目前的基本逻辑还是跨进行通信(IPC),和普通引用跨进程通信没有什么区别。
目前主流的交互方式有三种:
- stdio(标准输入输出流)
- sse(服务器发送事件)
- 可流式传输 HTTP
目前运用较多的还是标准输入输出流,我们这里也以这个为主。
这里的实现就是对照上文的计算函数的真实实现。
由于 Go 官方没有 MCP 服务端的 SDK,我们使用这个包实现github.com/mark3labs/mcp-go
,本质来说就是通过输入输出流定义交互接口,和普通跨进程通信无异。
package mainimport ("context""fmt""github.com/mark3labs/mcp-go/mcp""github.com/mark3labs/mcp-go/server"
)func main() {// Create a new MCP servers := server.NewMCPServer("Calculator Demo","1.0.0",server.WithToolCapabilities(false),server.WithRecovery(),)// Add a calculator toolcalculatorTool := mcp.NewTool("calculate",mcp.WithDescription("Perform basic arithmetic operations"),mcp.WithString("operation",mcp.Required(),mcp.Description("The operation to perform (add, subtract, multiply, divide)"),mcp.Enum("add", "subtract", "multiply", "divide"),),mcp.WithNumber("x",mcp.Required(),mcp.Description("First number"),),mcp.WithNumber("y",mcp.Required(),mcp.Description("Second number"),),)// Add the calculator handlers.AddTool(calculatorTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {// Using helper functions for type-safe argument accessop, err := request.RequireString("operation")if err != nil {return mcp.NewToolResultError(err.Error()), nil}x, err := request.RequireFloat("x")if err != nil {return mcp.NewToolResultError(err.Error()), nil}y, err := request.RequireFloat("y")if err != nil {return mcp.NewToolResultError(err.Error()), nil}var result float64switch op {case "add":result = x + ycase "subtract":result = x - ycase "multiply":result = x * ycase "divide":if y == 0 {return mcp.NewToolResultError("cannot divide by zero"), nil}result = x / y}return mcp.NewToolResultText(fmt.Sprintf("%.2f", result)), nil})// Start the serverif err := server.ServeStdio(s); err != nil {fmt.Printf("Server error: %v\n", err)}
}
Go 可以很方便的把 MCP 编译成二进制文件,编译后可以直接把文件放置 /user/local/bin 或者 c:/windows/System32下。
在Cherry Studio可以如此设置
在Cherry Studio中就可以很方便的使用了。
如果想自己体检更好的联动效果,更多更复杂的 MCP 服务器必不可少,可以去现在使用免费的 MCP 服务器,
- MCP.so:https://mcp.so/
- DeepNLP Open MCP Marketplace Plugin:https://github.com/AI-Agent-Hub/mcp-marketplace
如果开发自然少不了调试,在最新版本的 PostMan中,已经集成了 MCP 服务器的调试功能。
3. 当前 MCP 的主要挑战:
-
多工具调用的不准确性与复杂接口的冲突:
目前,LLM 在进行**多工具调用(Multi-tool Calling)**时,准确性仍然是老大难问题。尽管 LLM 能理解何时需要外部工具,但当一个复杂任务需要调用多个工具、甚至按特定顺序或条件进行时,LLM 往往难以精准编排。- 冲突点: 为了让 LLM 能够处理复杂功能,开发者不得不向其暴露(expose)更多的接口(API)。然而,暴露的接口越多,LLM 选择和组合工具的难度就越大,出现误判或调用失败的概率也随之增加。这形成了一个矛盾:要实现复杂功能,就需要更多接口;接口越多,调用的准确性就越难以保证。
-
MCP 能力暴露效率低下,制约 Agent 终极形态:
另一个核心痛点是,当前 MCP 暴露能力的方式效率极其低下。现在,如果 LLM 需要一个新能力,通常就意味着要为其暴露一个独立的接口。- 高 Token 消耗: 想象一下,一个像人类一样具备广泛技能的 AI Agent,需要成千上万个细粒度的“能力”(比如“读取文件”、“发送邮件”、“日程安排”、“预定机票”等等)。如果每一个能力都对应一个独立的 MCP 接口声明,那么这些接口的声明本身就会占据大量的上下文 Token 空间。这不仅大大增加了每次调用的成本,更严重的是,它会迅速耗尽 LLM 的上下文窗口(Context Window),使得 LLM 无法处理更长、更复杂的对话或任务,因为其大部分“记忆”都被接口声明占据了。
- 与理想 Agent 的差距: 这种“一个能力一个接口”的模式,与我们期望的 LLM 最终能完全模仿人类行为的 Agent 形态相去甚远。人类学习一项新技能,并不会为每个微小动作都生成一个独立的“接口说明书”。理想的 Agent 应该能够通过更高级的**抽象(abstraction)和泛化(generalization)**能力,以更高效的方式理解和利用工具,而不是被庞大的接口声明所束缚。
展望与挑战:
显然,要实现真正强大的 AI Agent,MCP 协议和其实现方式需要进行根本性的革新。未来的发展方向可能包括:
- 更高级的工具抽象: 从细粒度的单一接口转向更通用、可组合的“超级工具”或“工作流工具”。
- 智能的工具发现与选择: LLM 能够更智能地理解何时需要工具,以及如何高效地从庞大的工具库中筛选和调用最合适的工具,而无需所有工具的详细声明都常驻上下文。
- 动态的接口加载: 根据任务需求,动态地加载和卸载所需的接口声明,而不是一次性全部提供。
总而言之,目前的 MCP 在拓展 LLM 能力方面迈出了重要一步,但要实现 LLM 模仿人类行为的最终 Agent 形态,其在工具调度准确性和能力暴露效率上的瓶颈是亟待解决的。