Pochopenie hosta MCP
Čo je MCP
MCP je protokol vyvinutý spoločnosťou Anthropic pre claude. MCP je skratka pre Model Context Protocol, čo je protokol, ktorý umožňuje LLM aktívne žiadať o externé akcie alebo zdroje. Keďže MCP je doslova len protokol na odosielanie požiadaviek a prijímanie odpovedí, proces a vykonanie musí zabezpečiť vývojár.
O internej prevádzke
Pred vysvetlením internej prevádzky sa budeme venovať Gemini Function Calling. Gemini Function Calling, rovnako ako MCP, umožňuje LLM iniciatívne volať externé akcie. Možno sa pýtate, prečo sme vôbec spomenuli Function Calling. Dôvodom je, že Function Calling sa objavil skôr ako MCP a keďže oba používajú schému OpenAPI, sú kompatibilné a predpokladáme, že ich vzájomná prevádzka bude podobná. Preto sme ho spomenuli, pretože vysvetlenie Gemini Function Calling je podrobnejšie a môže byť užitočné.

Celkový tok je nasledovný:
- Definuje sa funkcia.
- Definícia funkcie sa odošle do Gemini spolu s promptom.
- "Odošlite používateľský prompt spolu s deklaráciou(ami) funkcie do modelu. Ten analyzuje požiadavku a určí, či by volanie funkcie bolo užitočné. Ak áno, odpovie štruktúrovaným JSON objektom."
- Ak Gemini potrebuje, požiada o volanie funkcie.
- Ak Gemini potrebuje, volajúci obdrží názov a parametre pre volanie funkcie.
- Volajúci sa môže rozhodnúť, či vykoná volanie alebo nie.
- Či zavolať a vrátiť platnú hodnotu.
- Či nevolať a vrátiť dáta, akoby bolo volané.
- Či to jednoducho ignorovať.
- Gemini počas tohto procesu vykoná a požiada o akcie, ako je volanie viacerých funkcií naraz, alebo volanie funkcií po preverení výsledkov.
- Nakoniec, keď sa objaví usporiadaná odpoveď, proces sa ukončí.
Tento tok je vo všeobecnosti v súlade s MCP. Podobne je to vysvetlené aj v tutoráli MCP. Podobne je to aj s nástrojmi ollama.
A veľmi šťastne, tieto tri nástroje – ollama tools, MCP a Gemini Function Calling – zdieľajú štruktúru schémy, takže implementáciou len jedného MCP ho možno použiť na všetkých troch miestach.
Ach, a existuje jedna nevýhoda, ktorú zdieľajú všetci. Nakoniec, model to vykonáva, takže ak je model, ktorý používate, v zlom stave, môže zlyhať pri volaní funkcie, volať ju zvláštne, alebo dokonca vykonať útok DOS na server MCP.
MCP host v jazyku Go
mark3lab's mcphost
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 napíšte do neho 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}
A potom spustite s modelom ollama nasledovne.
Samozrejme, ak je to potrebné, najprv si stiahnite model pomocou ollama pull mistral-small.
Hoci sa štandardne odporúča claude alebo qwen2.5, ja momentálne odporúčam mistral-small.
1mcphost -m ollama:mistral-small
Ak to však spustíte týmto spôsobom, môžete to použiť iba v prostredí CLI na otázky a odpovede.
Preto upravíme kód tohto mcphost, aby fungoval programovateľnejšie.
Fork mcphost
Ako už bolo overené, mcphost obsahuje funkcie na extrakciu metadát a volanie funkcií pomocou MCP. Preto je potrebná časť, ktorá volá LLM, časť, ktorá spracováva server MCP, a časť, ktorá spravuje históriu správ.
Nasledovný balík Runner obsahuje tieto časti:
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}
Internú deklaráciu príslušnej časti nebudeme samostatne skúmať. Je však takmer doslovná.
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}
Pre mcpClients a tools, ktoré sa tu použijú, si pozrite tento súbor.
Pre provider, ktorý bude používať ollama, si pozrite tento súbor.
Hlavnou časťou je metóda Run.
1func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
2 // Ak prompt nie je prázdny
3 if len(prompt) != 0 {
4 // Pridajte správu používateľa do histórie
5 r.messages = append(r.messages, history.HistoryMessage{
6 Role: "user",
7 Content: []history.ContentBlock{{
8 Type: "text",
9 Text: prompt,
10 }},
11 })
12 }
13
14 // Prevedie správy histórie na správy LLM
15 llmMessages := make([]llm.Message, len(r.messages))
16 for i := range r.messages {
17 llmMessages[i] = &r.messages[i]
18 }
19
20 // Konštanty pre exponenciálny backoff
21 const initialBackoff = 1 * time.Second
22 const maxRetries int = 5
23 const maxBackoff = 30 * time.Second
24
25 var message llm.Message
26 var err error
27 backoff := initialBackoff
28 retries := 0
29 // Cyklus pre pokusy o vytvorenie správy
30 for {
31 // Vytvorí správu pomocou poskytovateľa LLM
32 message, err = r.provider.CreateMessage(
33 context.Background(),
34 prompt,
35 llmMessages,
36 r.tools,
37 )
38 // Ak nastala chyba
39 if err != nil {
40 // Ak chyba indikuje preťaženie služby Claude
41 if strings.Contains(err.Error(), "overloaded_error") {
42 // Ak bol dosiahnutý maximálny počet pokusov
43 if retries >= maxRetries {
44 return "", fmt.Errorf(
45 "claude is currently overloaded. please wait a few minutes and try again",
46 )
47 }
48
49 // Počká a zvýši backoff
50 time.Sleep(backoff)
51 backoff *= 2
52 if backoff > maxBackoff {
53 backoff = maxBackoff
54 }
55 retries++
56 continue
57 }
58
59 // Vráti chybu, ak nie je preťaženie
60 return "", err
61 }
62
63 // Preruší cyklus, ak je správa úspešne vytvorená
64 break
65 }
66
67 var messageContent []history.ContentBlock
68
69 var toolResults []history.ContentBlock
70 messageContent = []history.ContentBlock{}
71
72 // Ak má správa obsah
73 if message.GetContent() != "" {
74 // Pridá textový obsah do obsahu správy
75 messageContent = append(messageContent, history.ContentBlock{
76 Type: "text",
77 Text: message.GetContent(),
78 })
79 }
80
81 // Pre každé volanie nástroja v správe
82 for _, toolCall := range message.GetToolCalls() {
83 // Serializuje argumenty nástroja do JSON
84 input, _ := json.Marshal(toolCall.GetArguments())
85 // Pridá blok tool_use do obsahu správy
86 messageContent = append(messageContent, history.ContentBlock{
87 Type: "tool_use",
88 ID: toolCall.GetID(),
89 Name: toolCall.GetName(),
90 Input: input,
91 })
92
93 // Rozdelí názov nástroja na názov servera a názov nástroja
94 parts := strings.Split(toolCall.GetName(), "__")
95
96 serverName, toolName := parts[0], parts[1]
97 // Získa MCP klienta pre daný server
98 mcpClient, ok := r.mcpClients[serverName]
99 if !ok {
100 continue
101 }
102
103 var toolArgs map[string]interface{}
104 // Deserializuje argumenty nástroja
105 if err := json.Unmarshal(input, &toolArgs); err != nil {
106 continue
107 }
108
109 var toolResultPtr *mcp.CallToolResult
110 req := mcp.CallToolRequest{}
111 req.Params.Name = toolName
112 req.Params.Arguments = toolArgs
113 // Zavolá nástroj pomocou MCP klienta
114 toolResultPtr, err = mcpClient.CallTool(
115 context.Background(),
116 req,
117 )
118
119 // Ak nastala chyba pri volaní nástroja
120 if err != nil {
121 errMsg := fmt.Sprintf(
122 "Error calling tool %s: %v",
123 toolName,
124 err,
125 )
126 log.Printf("Error calling tool %s: %v", toolName, err)
127
128 // Pridá blok s výsledkom chyby nástroja
129 toolResults = append(toolResults, history.ContentBlock{
130 Type: "tool_result",
131 ToolUseID: toolCall.GetID(),
132 Content: []history.ContentBlock{{
133 Type: "text",
134 Text: errMsg,
135 }},
136 })
137
138 continue
139 }
140
141 toolResult := *toolResultPtr
142
143 // Ak má výsledok nástroja obsah
144 if toolResult.Content != nil {
145 resultBlock := history.ContentBlock{
146 Type: "tool_result",
147 ToolUseID: toolCall.GetID(),
148 Content: toolResult.Content,
149 }
150
151 var resultText string
152 // Extrahuje text z obsahu výsledku nástroja
153 for _, item := range toolResult.Content {
154 if contentMap, ok := item.(map[string]interface{}); ok {
155 if text, ok := contentMap["text"]; ok {
156 resultText += fmt.Sprintf("%v ", text)
157 }
158 }
159 }
160
161 resultBlock.Text = strings.TrimSpace(resultText)
162
163 toolResults = append(toolResults, resultBlock)
164 }
165 }
166
167 // Pridá správu s obsahom do histórie
168 r.messages = append(r.messages, history.HistoryMessage{
169 Role: message.GetRole(),
170 Content: messageContent,
171 })
172
173 // Ak sú k dispozícii výsledky nástrojov
174 if len(toolResults) > 0 {
175 // Pridá výsledky nástrojov ako správu používateľa do histórie
176 r.messages = append(r.messages, history.HistoryMessage{
177 Role: "user",
178 Content: toolResults,
179 })
180
181 // Rekurzívne zavolá Run pre ďalšie spracovanie
182 return r.Run(ctx, "")
183 }
184
185 // Vráti obsah správy
186 return message.GetContent(), nil
187}
Samotný kód je poskladaný z časti kódu v tomto súbore.
Obsah je zhruba nasledovný:
- Odošle prompt a zoznam nástrojov na požiadanie o vykonanie alebo generovanie odpovede.
- Ak sa vygeneruje odpoveď, rekurzia sa zastaví a vráti.
- Ak LLM požiada o vykonanie nástroja, host zavolá MCP Server.
- Odpoveď sa pridá do histórie a vráti sa k bodu 1.
Na záver
Už koniec?
V skutočnosti toho nie je veľa na povedanie. Tento článok bol napísaný s cieľom pomôcť vám pochopiť, ako funguje MCP Server. Dúfam, že vám tento článok aspoň trochu pomohol pochopiť fungovanie MCP hosta.