GoSuda

Попытка понять хост MCP

By snowmerak
views ...

Что такое 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 более подробное и может быть полезным.

FunctionCalling

Общая схема такова:

  1. Определяется функция.
  2. Определение функции отправляется в Gemini вместе с промптом.
    1. "Отправьте пользовательский промпт вместе с объявлением(ями) функции в модель. Она анализирует запрос и определяет, будет ли вызов функции полезен. Если да, она отвечает структурированным JSON-объектом."
  3. Gemini запрашивает вызов функции, если это необходимо.
    1. Если Gemini это необходимо, вызывающая сторона получает имя и параметры для вызова функции.
    2. Вызывающая сторона может решить, выполнять или не выполнять вызов.
      1. Вызвать и вернуть допустимое значение.
      2. Вернуть данные, как будто вызов был сделан, но без фактического вызова.
      3. Просто проигнорировать.
  4. Gemini выполняет и запрашивает такие действия, как вызов нескольких функций за один раз или повторный вызов после просмотра результатов вызова функции.
  5. В конечном итоге, когда получен упорядоченный ответ, процесс завершается.

Эта схема в целом соответствует 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}

Сам код представляет собой комбинацию частей кода из этого файла.

Содержание примерно следующее:

  1. Отправляется список инструментов вместе с промптом для запроса выполнения или генерации ответа.
  2. Если ответ сгенерирован, рекурсия останавливается, и ответ возвращается.
  3. Если LLM оставляет запрос на выполнение инструмента, хост вызывает MCP Server.
  4. Ответ добавляется в историю, и процесс возвращается к шагу 1.

В заключение

Уже конец?

На самом деле, особо много сказать нечего. Эта статья была написана, чтобы помочь вам понять, как работает MCP Server. Надеюсь, эта статья хоть немного помогла вам понять работу MCP host.