---
id: a1fad13de24c90924cfa9e25e9f986cb
author: snowmerak
title: Немного о понимании MCP host
description: Статья призвана способствовать пониманию протокола MCP от Anthropic и MCP-хоста (mcphost), реализованного на Go.
language: ru
date: 2025-04-08T00:59:42.416582136Z
path: /blog/posts/a-little-understanding-of-mcp-host-zb952aae0
category: ai
---

## Что такое MCP

[MCP](https://modelcontextprotocol.io/introduction) это протокол, разработанный компанией Anthropic для Claude. MCP является сокращением от Model Context Protocol и представляет собой протокол, который позволяет LLM активно запрашивать у внешней среды действия или ресурсы. Поскольку MCP, буквально говоря, является протоколом, который лишь передает запросы и ответы, процесс обработки и выполнение должны быть реализованы разработчиком.

### О внутреннем устройстве

Прежде чем описывать внутреннее устройство, я хотел бы остановиться на [Gemini Function Calling](https://ai.google.dev/gemini-api/docs/function-calling). Gemini Function Calling, как и MCP, позволяет LLM инициативно вызывать внешние действия. Возможно, возникнет вопрос, зачем нужно было привлекать Function Calling. Причина в том, что Function Calling появился раньше MCP, а также в том, что оба используют схему OpenAPI, и поэтому они совместимы; я предположил, что их работа будет схожей. Поскольку описание Gemini Function Calling является относительно подробным, оно, вероятно, будет полезным, поэтому я его и привел.

![FunctionCalling](/assets/images/mcp_with_csharp/function-calling-overview.png)

Общий ход работы выглядит следующим образом.

1. Определяется функция.
2. Определение функции вместе с промптом отправляется в Gemini.
   1. "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."
3. Gemini при необходимости запрашивает вызов функции.
   1. Если Gemini считает это необходимым, вызывающая сторона получает имя функции и параметры для вызова.
   2. Вызывающая сторона может решить, выполнять ли вызов или нет:
      1. выполнить вызов и вернуть корректное значение;
      2. не выполнять вызов, а вернуть данные так, как будто вызов был выполнен;
      3. просто проигнорировать запрос.
4. Gemini в ходе описанного процесса может запросить вызов сразу нескольких функций либо, получив результат функции, выполнить еще один вызов и так далее.
5. Когда в итоге получается упорядоченный ответ, процесс завершается.

Этот ход работы в целом соответствует MCP. Аналогичное описание приведено в [туториале MCP](https://modelcontextprotocol.io/tutorials/building-mcp-with-llms). Подобным образом работает и ollama tools.

И, к счастью, эти три инструмента, ollama tools, MCP и Gemini Function Calling, используют практически общую структуру схем, поэтому, реализовав только MCP, можно использовать его во всех трех местах.

Также есть недостаток, общий для всех них. Поскольку вызов фактически выполняет модель, если используемая вами модель работает некорректно, она может не вызывать функцию, вызывать ее неправильно либо устраивать DOS-атаку на MCP-сервер и выдавать подобные сбои.

## MCP-хост на Go

### mcphost от mark3lab

В Go есть [mcphost](https://github.com/mark3labs/mcphost), который разрабатывается организацией mark3lab.

Использовать его очень просто.

```sh
go install github.com/mark3labs/mcphost@latest
```

После установки создайте файл `$HOME/.mcp.json` и заполните его следующим образом.

```json
{
  "mcpServers": {
    "sqlite": {
      "command": "uvx",
      "args": [
        "mcp-server-sqlite",
        "--db-path",
        "/tmp/foo.db"
      ]
    },
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/tmp"
      ]
    }
  }
}
```

Затем запустите его с моделью ollama следующим образом.  
Разумеется, если нужно, предварительно загрузите модель командой `ollama pull mistral-small`.

> По умолчанию рекомендуются Claude или qwen2.5, но на данный момент я рекомендую mistral-small.

```sh
mcphost -m ollama:mistral-small
```

Однако при таком запуске использовать его можно только в CLI-среде в формате вопросов и ответов.  
Поэтому мы изменим код `mcphost`, чтобы сделать его работу более программируемой.

### Форк mcphost

Как уже было показано, в `mcphost` есть функциональность, которая использует MCP для извлечения метаданных и вызова функций. Поэтому необходимы части, отвечающие за вызов LLM, работу с MCP-сервером и управление историей сообщений.

Соответствующие части вынесены в `Runner` из следующего пакета.

```go
package runner

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"strings"
	"time"

	mcpclient "github.com/mark3labs/mcp-go/client"
	"github.com/mark3labs/mcp-go/mcp"

	"github.com/mark3labs/mcphost/pkg/history"
	"github.com/mark3labs/mcphost/pkg/llm"
)

type Runner struct {
	provider   llm.Provider
	mcpClients map[string]*mcpclient.StdioMCPClient
	tools      []llm.Tool

	messages []history.HistoryMessage
}
```

Внутреннее объявление этих частей рассматривать не будем. Однако названия почти соответствуют их назначению.

```go
func NewRunner(systemPrompt string, provider llm.Provider, mcpClients map[string]*mcpclient.StdioMCPClient, tools []llm.Tool) *Runner {
	return &Runner{
		provider:   provider,
		mcpClients: mcpClients,
		tools:      tools,
		messages: []history.HistoryMessage{
			{
				Role: "system",
				Content: []history.ContentBlock{{
					Type: "text",
					Text: systemPrompt,
				}},
			},
		},
	}
}
```

О `mcpClients` и `tools`, которые здесь используются, см. [соответствующий файл](https://github.com/snowmerak/mcphost/blob/main/internal/runner/from.go).  
Для `provider` будет использоваться реализация ollama, см. [соответствующий файл](https://github.com/snowmerak/mcphost/blob/main/pkg/llm/ollama/provider.go).

Главным блюдом является метод `Run`.

```go
func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
	if len(prompt) != 0 {
		r.messages = append(r.messages, history.HistoryMessage{
			Role: "user",
			Content: []history.ContentBlock{{
				Type: "text",
				Text: prompt,
			}},
		})
	}

	llmMessages := make([]llm.Message, len(r.messages))
	for i := range r.messages {
		llmMessages[i] = &r.messages[i]
	}

	const initialBackoff = 1 * time.Second
	const maxRetries int = 5
	const maxBackoff = 30 * time.Second

	var message llm.Message
	var err error
	backoff := initialBackoff
	retries := 0
	for {
		message, err = r.provider.CreateMessage(
			context.Background(),
			prompt,
			llmMessages,
			r.tools,
		)
		if err != nil {
			if strings.Contains(err.Error(), "overloaded_error") {
				if retries >= maxRetries {
					return "", fmt.Errorf(
						"claude is currently overloaded. please wait a few minutes and try again",
					)
				}

				time.Sleep(backoff)
				backoff *= 2
				if backoff > maxBackoff {
					backoff = maxBackoff
				}
				retries++
				continue
			}

			return "", err
		}

		break
	}

	var messageContent []history.ContentBlock

	var toolResults []history.ContentBlock
	messageContent = []history.ContentBlock{}

	if message.GetContent() != "" {
		messageContent = append(messageContent, history.ContentBlock{
			Type: "text",
			Text: message.GetContent(),
		})
	}

	for _, toolCall := range message.GetToolCalls() {
		input, _ := json.Marshal(toolCall.GetArguments())
		messageContent = append(messageContent, history.ContentBlock{
			Type:  "tool_use",
			ID:    toolCall.GetID(),
			Name:  toolCall.GetName(),
			Input: input,
		})

		parts := strings.Split(toolCall.GetName(), "__")

		serverName, toolName := parts[0], parts[1]
		mcpClient, ok := r.mcpClients[serverName]
		if !ok {
			continue
		}

		var toolArgs map[string]interface{}
		if err := json.Unmarshal(input, &toolArgs); err != nil {
			continue
		}

		var toolResultPtr *mcp.CallToolResult
		req := mcp.CallToolRequest{}
		req.Params.Name = toolName
		req.Params.Arguments = toolArgs
		toolResultPtr, err = mcpClient.CallTool(
			context.Background(),
			req,
		)

		if err != nil {
			errMsg := fmt.Sprintf(
				"Error calling tool %s: %v",
				toolName,
				err,
			)
			log.Printf("Error calling tool %s: %v", toolName, err)

			toolResults = append(toolResults, history.ContentBlock{
				Type:      "tool_result",
				ToolUseID: toolCall.GetID(),
				Content: []history.ContentBlock{{
					Type: "text",
					Text: errMsg,
				}},
			})

			continue
		}

		toolResult := *toolResultPtr

		if toolResult.Content != nil {
			resultBlock := history.ContentBlock{
				Type:      "tool_result",
				ToolUseID: toolCall.GetID(),
				Content:   toolResult.Content,
			}

			var resultText string
			for _, item := range toolResult.Content {
				if contentMap, ok := item.(map[string]interface{}); ok {
					if text, ok := contentMap["text"]; ok {
						resultText += fmt.Sprintf("%v ", text)
					}
				}
			}

			resultBlock.Text = strings.TrimSpace(resultText)

			toolResults = append(toolResults, resultBlock)
		}
	}

	r.messages = append(r.messages, history.HistoryMessage{
		Role:    message.GetRole(),
		Content: messageContent,
	})

	if len(toolResults) > 0 {
		r.messages = append(r.messages, history.HistoryMessage{
			Role:    "user",
			Content: toolResults,
		})

		return r.Run(ctx, "")
	}

	return message.GetContent(), nil
}
```

Код самого метода был составлен из фрагментов [соответствующего файла](https://github.com/snowmerak/mcphost/blob/main/cmd/mcp.go).

В общих чертах это выглядит так.

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

## В заключение

> Уже конец?

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