Pochopenie hostiteľa MCP

Čo je MCP
MCP je protokol, ktorý vyvinula spoločnosť Anthropic pre claude. MCP je skratka pre Model Context Protocol a je to protokol, ktorý umožňuje LLM aktívne žiadať外部 o akcie alebo zdroje. MCP je doslova len protokol na odosielanie žiadostí a prijímanie odpovedí, a preto musí samotný proces a vykonávanie zabezpečiť vývojár.
O vnútornej činnosti
Skôr než vysvetlíme vnútornú činnosť, sa krátko zastavme pri Gemini Function Calling. Gemini Function Calling rovnako ako MCP umožňuje LLM iniciatívne volať externé akcie. Možno sa čitateľ spýta, prečo sme Function Calling vôbec spomenuli. Dôvod je ten, že Function Calling sa objavil skôr ako MCP a keďže oba využívajú schému OpenAPI, sú vzájomne kompatibilné, a preto sme predpokladali, že ich vzájomné správanie bude podobné. Keďže opis Gemini Function Calling je relatívne podrobnejší, predpokladáme, že bude užitočný, a preto sme ho uviedli.

Celkový tok je nasledovný.
- Definujte funkciu.
- Spolu s promptom odošlite definíciu funkcie do Gemini.
- "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."
- Ak Gemini považuje za potrebné, požiada o volanie funkcie.
- Ak Gemini považuje za potrebné volanie funkcie, volajúci dostane názov a parametre na volanie.
- Volajúci môže rozhodnúť, či sa má vykonať:
- zavolať funkciu a vrátiť správnu hodnotu,
- nevolať funkciu, ale vrátiť údaje, akoby bola zavolaná,
- alebo ju jednoducho ignorovať.
- Gemini v rámci uvedeného procesu môže naraz požadovať volanie viacerých funkcií alebo po vyhodnotení výsledku funkcie požadovať ďalšie volania.
- Keď je výsledkom upravená odpoveď, proces sa ukončí.
Tento tok je vo všeobecnosti v súlade s MCP. Podobne ho opisuje tutoriál MCP. Podobne funguje aj ollama tools.
A to je naozaj šťastie, že tieto tri nástroje, ollama tools, MCP a Gemini Function Calling, zdieľajú takmer rovnakú štruktúru schémy, takže implementáciou iba MCP ju možno použiť na všetky tri miesta.
Ešte jedna nevýhoda je spoločná pre všetky. Keďže volanie vykonáva model, ak je váš model v zlom stave, môže funkcie nevolať, volať ich nesprávne alebo poslať MCP serveru DOS útok a podobne, čiže správať sa nesprávne.
MCP hostiteľ v Go
mcphost od mark3lab
V Go existuje mcphost, ktorý vyvíja organizácia mark3lab.
Použitie je veľmi jednoduché.
1go install github.com/mark3labs/mcphost@latest
Po inštalácii vytvorte súbor $HOME/.mcp.json a zapíšte doňho nasledovné.
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}
Potom spustite s modelom ollama nasledovne.
Samozrejme, ak je to potrebné, predtým stiahnite model príkazom ollama pull mistral-small.
Všeobecne odporúčame claude alebo qwen2.5, no momentálne odporúčam mistral-small.
1mcphost -m ollama:mistral-small
Ak takto spustíte program, dá sa používať iba formou otázok a odpovedí v prostredí CLI.
Preto teraz upravíme kód mcphost tak, aby sa dal ovládať programovo.
Fork mcphost
Ako sme už videli, mcphost obsahuje funkciu, ktorá využíva MCP na extrakciu metadát a volanie funkcií. Preto sú potrebné časti, ktoré volajú LLM, pracujú so serverom MCP a spravujú históriu správ.
Časť, ktorá obsahuje tieto funkcie, je Runner z nasledujúceho balíka.
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}
Vnútorné deklarácie tejto časti si podrobne neprezeráme. Takmer všetky však zodpovedajú svojim názvom.
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}
Ohľadom mcpClients a tools, ktoré sa tu používajú, si prosím pozrite príslušný súbor.
Pre provider použijeme ten od ollama, preto si prosím pozrite príslušný súbor.
Hlavným jedlom je metóda 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}
Samotný kód je zostavený z časti príslušného súboru.
Obsah je približne nasledovný.
- Spolu s promptom odošlite zoznam nástrojov a požiadajte o rozhodnutie, či sa majú vykonať, alebo o vygenerovanie odpovede.
- Keď sa odpoveď vygeneruje, ukončite rekurziu a vráťte ju.
- Ak LLM zanechá požiadavku na vykonanie nástroja, hostiteľ zavolá MCP Server.
- Pridajte odpoveď do histórie a vráťte sa na bod 1.
Záver
Už koniec?
Vlastne toho veľa povedať nemám. Tento text vznikol predovšetkým preto, aby pomohol pochopiť, ako funguje MCP Server. Dúfam, že vám aspoň trochu pomohol pochopiť činnosť MCP hostiteľa.