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.

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

О, и има един общ недостатък. В крайна сметка моделът е този, който извършва изпълнението, така че ако моделът, който използвате, не е в добро състояние, той може да не извика функцията, да я извика неправилно или да извърши злонамерени действия като DOS атака срещу 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, които ще се използват тук, моля, вижте този файл. Тъй като ще използваме доставчика на 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 хоста.