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

Общая схема такова:
- Определяется функция.
- Определение функции отправляется в Gemini вместе с промптом.
- "Отправьте пользовательский промпт вместе с объявлением(ями) функции в модель. Она анализирует запрос и определяет, будет ли вызов функции полезен. Если да, она отвечает структурированным JSON-объектом."
- Gemini запрашивает вызов функции, если это необходимо.
- Если Gemini это необходимо, вызывающая сторона получает имя и параметры для вызова функции.
- Вызывающая сторона может решить, выполнять или не выполнять вызов.
- Вызвать и вернуть допустимое значение.
- Вернуть данные, как будто вызов был сделан, но без фактического вызова.
- Просто проигнорировать.
- Gemini выполняет и запрашивает такие действия, как вызов нескольких функций за один раз или повторный вызов после просмотра результатов вызова функции.
- В конечном итоге, когда получен упорядоченный ответ, процесс завершается.
Эта схема в целом соответствует MCP. Это также аналогично объясняется в учебнике MCP. Это также похоже на ollama tools.
И, к счастью, эти три инструмента — ollama tools, MCP и Gemini Function Calling — настолько разделяют структуру схемы, что, реализовав только MCP, можно использовать его во всех трех местах.
Ах да, у всех них есть общий недостаток. В конечном итоге, это модель, которая выполняет действия, поэтому, если используемая вами модель находится в плохом состоянии, она может не вызывать функции, вызывать их странным образом или выполнять некорректные действия, например, осуществлять DDoS-атаку на MCP-сервер.
MCP хост на Go
mcphost от mark3lab
На 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, чтобы он мог работать более программируемым образом.
Форк 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.