Кратко разбиране на хоста на 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.
И за щастие, тези три инструмента – 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}
Самият код е сбор от части от този файл.
Съдържанието е приблизително следното:
- Изпращане на списък с инструменти заедно с подкана за заявка за изпълнение или генериране на отговор.
- Ако се генерира отговор, рекурсията се спира и той се връща.
- Ако LLM остави заявка за изпълнение на инструмент, хостът извиква MCP Server.
- Отговорът се добавя към историята и се връща към стъпка 1.
В заключение
Вече край?
Всъщност няма много какво да се каже. Тази статия е написана, за да ви помогне да разберете приблизително как работи MCP Server. Надявам се, че тази статия ви е помогнала до известна степен да разберете функционирането на MCP хоста.