Кратко запознаване с MCP host

Какво е MCP
MCP е протокол, разработен от Anthropic за claude. MCP е съкращение от Model Context Protocol и е протокол, който позволява на LLM активно да изисква действия или ресурси от външния свят. Тъй като MCP буквално е протокол, който само изпраща заявки и отговори, процесът по обработка и изпълнение трябва да бъде реализиран от разработчика.
За вътрешната работа
Преди да обясня вътрешната работа, ще отбележа Gemini Function Calling. Gemini Function Calling също позволява на LLM самостоятелно да извиква външни действия, по същия начин като MCP. Вероятно възниква въпросът защо изобщо е необходимо да разглеждаме Function Calling. Причината е, че Function Calling се появява по-рано от MCP и тъй като и двата използват OpenAPI схема, те са съвместими, поради което предполагам, че поведението им е сходно. Освен това описанието на Gemini Function Calling е по-подробно, което смятам, че ще бъде полезно, затова го включих.

Общият процес изглежда така.
- Дефинира се функция.
- Дефиницията на функцията се изпраща към Gemini заедно с промпта.
- "Send user prompt along with the function declaration(s) to the model. It analyzes the request and determines if a function call would be helpful. If so, it responds with a structured JSON object."
- Gemini изисква извикване на функция, ако е необходимо.
- Ако Gemini прецени, че е необходимо извикване на функция, извикващата страна получава името и параметрите за извикването.
- Извикващата страна може да реши дали да изпълни извикването или не.
- Да извика функцията и да върне валидна стойност
- Да не извиква функцията, но да върне данни, сякаш е извикана
- Просто да я игнорира
- Gemini може в рамките на този процес да извика няколко функции едновременно или да извика функция, да прегледа резултата и след това да извика отново, като изпълнява и изисква подобни действия.
- Процесът завършва, когато се получи структуриран окончателен отговор.
Този процес по принцип съвпада с MCP. Подобно описание се съдържа и в урока за MCP. Също така ollama tools работи по подобен начин.
И, за щастие, тези три инструмента, ollama tools, MCP и Gemini Function Calling, споделят почти една и съща структура на схемата, така че чрез реализиране само на MCP може да се използва във всеки от трите.
Освен това има недостатък, общ за всички тях. Тъй като изпълнението се извършва от самия модел, ако използваният от вас модел е с лошо качество, той може да не извика функция, да я извика неправилно или да претовари MCP сървъра (DOS), което води до неправилно поведение.
MCP хост на Go
mark3lab's mcphost
В Go съществува mcphost, който се разработва от организацията mark3lab.
Употребата е много проста.
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, за да може да работи по-програмируемо.
Fork на 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 сървъра.
- Резултатът се добавя към историята и процесът се връща към стъпка 1.
Накрая
Вече край?
Всъщност не е имало кой знае колко да се каже. Тази статия е написана, за да помогне за общо разбиране на начина, по който работи MCP сървърът. Надявам се този текст да е допринесъл поне малко за разбирането на работата на MCP хоста.