Pochopení hostitele MCP

Co je MCP
MCP je protokol, který vyvinula společnost Anthropic pro Claude. MCP je zkratka za Model Context Protocol a umožňuje LLM aktivně žádat externí systém o provedení akce nebo poskytnutí zdroje. MCP je doslova pouze protokol pro zasílání žádostí a odpovědí, a proto musí samotný proces a jeho provedení zajistit vývojář.
O vnitřní funkci
Než vysvětlím vnitřní fungování, rád bych se krátce zastavil u Gemini Function Calling. Gemini Function Calling, stejně jako MCP, umožňuje LLM iniciativně volat externí akce. Možná vás napadne, proč jsem Function Calling vůbec zmínil. Důvodem je, že Function Calling se objevil dříve než MCP a oba používají schéma OpenAPI, takže jsou vzájemně kompatibilní a lze předpokládat, že se jejich chování bude podobat. Dokumentace Gemini Function Calling je navíc podrobnější, a proto se může ukázat jako užitečná.

Celkový postup vypadá takto.
- Definujte funkci.
- Odešlete definici funkce spolu s promptem 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."
- Pokud Gemini potřebuje, požádá o volání funkce.
- Pokud Gemini potřebuje volání funkce, volající obdrží název a parametry pro toto volání.
- Volající se může rozhodnout, zda volání provede.
- Zda funkci skutečně zavolá a vrátí platnou hodnotu.
- Zda funkci nezavolá, ale vrátí data, jako by ji zavolal.
- Zda ji jednoduše ignoruje.
- Gemini v rámci tohoto procesu může najednou požadovat volání několika funkcí nebo může po zhodnocení výsledku volání další funkce provést a požádat o ně.
- Jakmile je výsledkem srozumitelná odpověď, proces končí.
Tento postup se v zásadě shoduje s MCP. Podobný popis najdete také v tutoriálu MCP. Podobně funguje i ollama tools.
A co je opravdu šťastné, tyto tři nástroje, ollama tools, MCP a Gemini Function Calling, sdílejí téměř stejnou strukturu schématu, takže implementací pouze MCP lze využít všechny tři.
Všichni sdílejí i jednu nevýhodu. Volání skutečně provádí model, takže pokud je model ve špatném stavu, může funkce nevolat, volat je nesprávně nebo posílat MCP serveru DoS útok a způsobit další chybné chování.
MCP host v Go
mark3lab's mcphost
V Go existuje mcphost, který vyvíjí organizace 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í obsah.
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 následující příkaz s modelem ollama.
Pokud je to nutné, nejprve stáhněte model příkazem ollama pull mistral-small.
Obecně doporučuji Claude nebo qwen2.5, ale v současné době doporučuji mistral-small.
1mcphost -m ollama:mistral-small
Při tomto spuštění lze však používat pouze formu otázek a odpovědí v CLI prostředí.
Proto upravíme kód mcphost tak, aby se dalo chovat programovatelněji.
Fork mcphost
Jak jsme již viděli, mcphost obsahuje funkci, která pomocí MCP extrahuje metadata a volá funkce. Potřebujeme tedy část pro volání LLM, část pro práci s MCP serverem a část pro správu historie zpráv.
Příslušnou část představuje Runner z následujícího balíčku.
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}
Vnitřní deklarace této části zde podrobně nerozebírám. Jsou však téměř shodné s jejich názvy.
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}
Informace o mcpClients a tools, které se zde používají, najdete v příslušném souboru.
Pro provider použijeme verzi pro ollama, takže se podívejte do příslušného souboru.
Hlavním jádrem 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 "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}
Kód samotný je sestaven z částí příslušného souboru.
Obsah lze shrnout takto.
- Spolu s promptem odešlete seznam nástrojů a požádejte o rozhodnutí, zda se mají spustit, nebo o vygenerování odpovědi.
- Pokud je odpověď vygenerována, ukončete rekurzi a vraťte ji.
- Pokud LLM požádá o spuštění nástroje, host zavolá MCP Server.
- Přidejte odpověď do historie a vraťte se ke kroku 1.
Závěrem
Už konec?
Ve skutečnosti toho moc říct nemám. Tento článek vznikl hlavně proto, abych vám pomohl pochopit, jak MCP Server funguje. Doufám, že vám alespoň trochu pomohl porozumět tomu, jak funguje MCP host.