Porozumění hostu MCP
Co je MCP
MCP je protokol vyvinutý společností Anthropic pro claude. MCP je zkratka pro Model Context Protocol, který umožňuje LLM aktivně žádat o externí akce nebo zdroje. Jelikož MCP je doslova jen protokol pro žádosti a odpovědi, proces a spuštění musí provést vývojář.
O vnitřním fungování
Než se pustíme do vysvětlení vnitřního fungování, Gemini Function Calling si krátce představíme. Gemini Function Calling, stejně jako MCP, umožňuje LLM proaktivně volat externí akce. Možná se ptáte, proč jsme se vůbec k Function Calling vrátili. Důvodem je, že Function Calling vyšlo dříve než MCP a je kompatibilní, protože oba používají schéma OpenAPI, což naznačuje, že jejich vzájemné fungování bude podobné. Protože je popis Gemini Function Calling podrobnější, předpokládali jsme, že bude užitečné ho zmínit.

Celkový průběh je následující:
- Definuje se funkce.
- Definice funkce se odešle do Gemini spolu s promptem.
- "Odešlete uživatelský prompt spolu s deklarací(mi) funkce(í) modelu. Ten analyzuje požadavek a určí, zda by volání funkce bylo užitečné. Pokud ano, odpoví strukturovaným objektem JSON."
- Gemini v případě potřeby požádá o volání funkce.
- Pokud Gemini potřebuje, volající obdrží jméno a parametry pro volání funkce.
- Volající se může rozhodnout, zda provede spuštění, či nikoli.
- Zda se provede volání a vrátí se platná hodnota.
- Zda se vrátí data, jako by volání proběhlo, aniž by se skutečně volalo.
- Zda se volání jednoduše ignoruje.
- Gemini v průběhu výše uvedeného procesu provádí a požaduje akce, jako je volání více funkcí najednou nebo volání funkcí opakovaně po zhlédnutí výsledků.
- V konečném důsledku se proces ukončí, jakmile je k dispozici uspořádaná odpověď.
Tento průběh je obecně v souladu s MCP. Podobně je to popsáno i v tutoriálu MCP. Podobné je to i s nástroji ollama.
A co je opravdu skvělé, je, že tyto tři nástroje – ollama tools, MCP a Gemini Function Calling – sdílejí strukturu schématu, takže implementace pouze jednoho MCP může být použita na všech třech místech.
A mimochodem, existuje nevýhoda, kterou sdílejí všichni. Protože je spuštění nakonec provedeno modelem, pokud je model, který používáte, v špatném stavu, může se stát, že funkce nebude volána, nebo bude volána podivně, nebo dokonce provede chybnou akci, jako je například útok DOS na MCP server.
MCP host v Go
mark3lab's mcphost
V Go existuje mcphost, který je vyvíjen organizací mark3lab.
Použití je velmi jednoduché.
1go install github.com/mark3labs/mcphost@latest
Po instalaci vytvořte soubor $HOME/.mcp.json a zapište do něj následující:
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}
Poté spusťte s modelem ollama následovně.
Předtím si samozřejmě stáhněte model pomocí ollama pull mistral-small, pokud je to nutné.
I když se obecně doporučuje claude nebo qwen2.5, v současné době doporučuji mistral-small.
1mcphost -m ollama:mistral-small
Pokud se však spustí tímto způsobem, lze jej použít pouze v prostředí CLI ve stylu otázek a odpovědí.
Proto upravíme kód tohoto mcphost, abychom jej mohli programověji ovládat.
Fork mcphost
Jak již bylo zjištěno, mcphost zahrnuje funkce pro extrakci metadat a volání funkcí pomocí MCP. Proto je nutná část pro volání LLM, část pro správu MCP serveru a část pro správu historie zpráv.
Následující balíček Runner obsahuje tyto části.
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í deklarace příslušné části nebudeme podrobně zkoumat. Nicméně je to téměř doslova podle názvu.
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}
Pro mcpClients a tools použité zde se prosím podívejte na tento soubor.
Pro provider (použijeme ten od ollama) se prosím podívejte na tento soubor.
Hlavním chodem je metoda 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 "Chyba při volání nástroje %s: %v", // Error calling tool %s: %v
101 toolName,
102 err,
103 )
104 log.Printf("Chyba při volání nástroje %s: %v", toolName, err) // Error calling tool %s: %v
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 poskládán z částí tohoto souboru.
Obsah je zhruba následující:
- Odešle se seznam nástrojů spolu s promptem, aby se vyžádalo spuštění nebo generování odpovědi.
- Jakmile je odpověď vygenerována, rekurze se zastaví a vrátí se.
- Pokud LLM vydá požadavek na spuštění nástroje, hostitel zavolá MCP Server.
- Odpověď se přidá do historie a vrátí se k bodu 1.
Na závěr
Už konec?
Upřímně řečeno, není toho moc, co bych mohl říci. Tento článek byl napsán, aby vám pomohl pochopit, jak MCP Server funguje. Doufám, že vám tento článek alespoň trochu pomohl pochopit fungování hostitele MCP.