MCP host 略解
什么是 MCP
MCP 是 Anthropic 为 claude 开发的一种协议。MCP 是 Model Context Protocol 的缩写,它是一种允许 LLM 主动请求外部操作或资源的协议。由于 MCP 仅仅是一个用于请求和响应的协议,因此其过程和执行需要由开发人员完成。
关于内部运作
在解释内部运作之前,我们先来了解一下 Gemini Function Calling。Gemini Function Calling 与 MCP 相同,都允许 LLM 主动调用外部操作。那么,您可能会疑惑,为什么非要引入 Function Calling 呢?引入它的原因在于 Function Calling 比 MCP 更早出现,并且它与 MCP 同样使用 OpenAPI 模式,因此它们之间具有兼容性,我们推测它们的操作会相似。正因为如此,Gemini Function Calling 的说明更为详细,因此引入它会有所帮助。
整体流程如下:
- 定义函数。
- 将函数定义与提示一同发送给 Gemini。
- "将用户提示以及函数声明发送给模型。模型会分析请求并判断函数调用是否有益。如果判断有益,则会返回一个结构化的 JSON 对象。"
- 如果 Gemini 需要,它会请求函数调用。
- 如果 Gemini 需要,调用者会收到函数调用的名称和参数。
- 调用者可以选择执行或不执行。
- 是否执行并返回有效值
- 不执行但返回如同已执行的数据
- 直接忽略
- 在上述过程中,Gemini 可以一次调用多个函数,或者在调用函数并查看结果后再次调用,执行并请求此类操作。
- 最终,如果得到一个整洁的答案,则终止。
这个流程与 MCP 通常是相通的。在 MCP 的教程中也有类似的说明。ollama tools 也是如此。
幸运的是,这三种工具——ollama tools、MCP 和 Gemini Function Calling——它们的模式结构几乎是共享的,因此只需实现一个 MCP,就可以在所有三个地方使用。
哦,它们还有一个共同的缺点。由于最终是由模型来执行的,如果您的模型状态不佳,它可能会出现误操作,例如不调用函数、异常调用函数,或者对 MCP 服务器发起 DOS 攻击等。
Go 语言编写的 MCP 主机
mark3lab 的 mcphost
在 Go 语言中,有一个由 mark3lab 组织开发的 mcphost。
使用方法非常简单。
1go install github.com/mark3labs/mcphost@latest
安装后,创建 $HOME/.mcp.json
文件并按如下方式编写:
1{
2 "mcpServers": {
3 "sqlite": {
4 "command": "uvx",
5 "args": [
6 "mcp-server-sqlite",
7 "--db-path",
8 "/tmp/foo.db"
9 ]
10 },
11 "filesystem": {
12 "command": "npx",
13 "args": [
14 "-y",
15 "@modelcontextprotocol/server-filesystem",
16 "/tmp"
17 ]
18 }
19 }
20}
然后,像这样使用 ollama 模型运行。当然,如果需要,在此之前先用 ollama pull mistral-small
拉取模型。
尽管通常推荐使用 claude 或 qwen2.5,但我目前推荐 mistral-small。
1mcphost -m ollama:mistral-small
然而,这样运行只能在 CLI 环境中以问答形式使用。因此,我们将修改 mcphost
的代码,使其能够以更具编程性的方式运行。
mcphost 分支
正如已经确认的,mcphost
包含利用 MCP 提取元数据和调用函数的功能。因此,需要有调用 LLM 的部分、处理 MCP 服务器的部分以及管理消息历史记录的部分。
以下包的 Runner
中包含了相应的部分。
1package runner
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "log"
8 "strings"
9 "time"
10
11 mcpclient "github.com/mark3labs/mcp-go/client"
12 "github.com/mark3labs/mcp-go/mcp"
13
14 "github.com/mark3labs/mcphost/pkg/history"
15 "github.com/mark3labs/mcphost/pkg/llm"
16)
17
18type Runner struct {
19 provider llm.Provider
20 mcpClients map[string]*mcpclient.StdioMCPClient
21 tools []llm.Tool
22
23 messages []history.HistoryMessage
24}
我们不会单独查看相应部分的内部声明。不过,它们几乎都是名副其实的。
1func NewRunner(systemPrompt string, provider llm.Provider, mcpClients map[string]*mcpclient.StdioMCPClient, tools []llm.Tool) *Runner {
2 return &Runner{
3 provider: provider,
4 mcpClients: mcpClients,
5 tools: tools,
6 messages: []history.HistoryMessage{
7 {
8 Role: "system",
9 Content: []history.ContentBlock{{
10 Type: "text",
11 Text: systemPrompt,
12 }},
13 },
14 },
15 }
16}
关于这里使用的 mcpClients
和 tools
,请参阅 该文件。provider
将使用 ollama 的,请参阅 该文件。
主要内容是 Run
方法。
1func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
2 if len(prompt) != 0 {
3 r.messages = append(r.messages, history.HistoryMessage{
4 Role: "user",
5 Content: []history.ContentBlock{{
6 Type: "text",
7 Text: prompt,
8 }},
9 })
10 }
11
12 llmMessages := make([]llm.Message, len(r.messages))
13 for i := range r.messages {
14 llmMessages[i] = &r.messages[i]
15 }
16
17 const initialBackoff = 1 * time.Second
18 const maxRetries int = 5
19 const maxBackoff = 30 * time.Second
20
21 var message llm.Message
22 var err error
23 backoff := initialBackoff
24 retries := 0
25 for {
26 message, err = r.provider.CreateMessage(
27 context.Background(),
28 prompt,
29 llmMessages,
30 r.tools,
31 )
32 if err != nil {
33 if strings.Contains(err.Error(), "overloaded_error") {
34 if retries >= maxRetries {
35 return "", fmt.Errorf(
36 "claude is currently overloaded. please wait a few minutes and try again",
37 )
38 }
39
40 time.Sleep(backoff)
41 backoff *= 2
42 if backoff > maxBackoff {
43 backoff = maxBackoff
44 }
45 retries++
46 continue
47 }
48
49 return "", err
50 }
51
52 break
53 }
54
55 var messageContent []history.ContentBlock
56
57 var toolResults []history.ContentBlock
58 messageContent = []history.ContentBlock{}
59
60 if message.GetContent() != "" {
61 messageContent = append(messageContent, history.ContentBlock{
62 Type: "text",
63 Text: message.GetContent(),
64 })
65 }
66
67 for _, toolCall := range message.GetToolCalls() {
68 input, _ := json.Marshal(toolCall.GetArguments())
69 messageContent = append(messageContent, history.ContentBlock{
70 Type: "tool_use",
71 ID: toolCall.GetID(),
72 Name: toolCall.GetName(),
73 Input: input,
74 })
75
76 parts := strings.Split(toolCall.GetName(), "__")
77
78 serverName, toolName := parts[0], parts[1]
79 mcpClient, ok := r.mcpClients[serverName]
80 if !ok {
81 continue
82 }
83
84 var toolArgs map[string]interface{}
85 if err := json.Unmarshal(input, &toolArgs); err != nil {
86 continue
87 }
88
89 var toolResultPtr *mcp.CallToolResult
90 req := mcp.CallToolRequest{}
91 req.Params.Name = toolName
92 req.Params.Arguments = toolArgs
93 toolResultPtr, err = mcpClient.CallTool(
94 context.Background(),
95 req,
96 )
97
98 if err != nil {
99 errMsg := fmt.Sprintf(
100 "Error calling tool %s: %v",
101 toolName,
102 err,
103 )
104 log.Printf("Error calling tool %s: %v", toolName, err)
105
106 toolResults = append(toolResults, history.ContentBlock{
107 Type: "tool_result",
108 ToolUseID: toolCall.GetID(),
109 Content: []history.ContentBlock{{
110 Type: "text",
111 Text: errMsg,
112 }},
113 })
114
115 continue
116 }
117
118 toolResult := *toolResultPtr
119
120 if toolResult.Content != nil {
121 resultBlock := history.ContentBlock{
122 Type: "tool_result",
123 ToolUseID: toolCall.GetID(),
124 Content: toolResult.Content,
125 }
126
127 var resultText string
128 for _, item := range toolResult.Content {
129 if contentMap, ok := item.(map[string]interface{}); ok {
130 if text, ok := contentMap["text"]; ok {
131 resultText += fmt.Sprintf("%v ", text)
132 }
133 }
134 }
135
136 resultBlock.Text = strings.TrimSpace(resultText)
137
138 toolResults = append(toolResults, resultBlock)
139 }
140 }
141
142 r.messages = append(r.messages, history.HistoryMessage{
143 Role: message.GetRole(),
144 Content: messageContent,
145 })
146
147 if len(toolResults) > 0 {
148 r.messages = append(r.messages, history.HistoryMessage{
149 Role: "user",
150 Content: toolResults,
151 })
152
153 return r.Run(ctx, "")
154 }
155
156 return message.GetContent(), nil
157}
代码本身是从 该文件 的部分代码拼凑而成的。
内容大致如下:
- 连同工具列表一起发送提示,请求执行或生成响应。
- 如果生成了响应,则停止递归并返回。
- 如果 LLM 留下工具执行请求,则主机调用 MCP Server。
- 将响应添加到历史记录中,然后返回到第 1 步。
结语
这么快就结束了?
其实没有太多可说的。本文旨在帮助您大致理解 MCP Server 的运作方式。希望本文能对您理解 MCP host 的运作有所帮助。