Pochopení hosta MCP
Co je MCP
MCP je protokol vyvinutý společností Anthropic pro claude. Zkratka MCP znamená Model Context Protocol a jedná se o protokol, který umožňuje LLM aktivně žádat o externí akce nebo zdroje. Protože MCP je doslova jen protokol pro odesílání požadavků a přijímání odpovědí, musí proces a implementaci provést vývojář.
O interním fungování
Než vysvětlíme interní fungování, zmíníme se o Gemini Function Calling. Gemini Function Calling, stejně jako MCP, umožňuje LLM iniciovat volání externích akcí. Možná se ptáte, proč vůbec Function Calling zmiňujeme. Důvodem je, že Function Calling se objevilo dříve než MCP a používá stejné schéma OpenAPI, což zaručuje kompatibilitu, a proto jsme předpokládali, že jejich vzájemné fungování bude podobné. Vzhledem k tomu, že popis Gemini Function Calling je relativně podrobnější, usoudili jsme, že by mohl být užitečný, a proto jsme jej uvedli.
Celkový postup je následující:
- Definujete funkci.
- Odešlete definici funkce spolu s promptem do Gemini.
- "Odešlete uživatelský prompt společně s deklaracemi funkcí modelu. Model analyzuje požadavek a určí, zda by bylo užitečné volání funkce. Pokud ano, odpoví strukturovaným objektem JSON."
- Gemini požádá o volání funkce, pokud je to nutné.
- Pokud Gemini potřebuje volat funkci, volající obdrží její název a parametry.
- Volající se může rozhodnout, zda ji provede, či nikoli.
- Zda ji provede a vrátí platnou hodnotu.
- Zda ji neprovede, ale vrátí data, jako by byla provedena.
- Zda ji jednoduše ignoruje.
- Gemini během tohoto procesu provádí a požaduje akce, jako je volání více funkcí najednou nebo volání dalších funkcí na základě výsledku předchozího volání.
- Proces se ukončí, jakmile je generována uspořádaná odpověď.
Tento postup je obecně v souladu s MCP. Podobně je popsán i v tutoriálu MCP. Podobné je to i u ollama tools.
A co je velmi potěšující, je to, že tato tři nástrojová řešení – ollama tools, MCP a Gemini Function Calling – sdílejí strukturu schématu, takže implementací pouze MCP je možné je použít ve všech třech.
A mimochodem, všechny sdílejí jednu nevýhodu. Protože spuštění provádí model, pokud je model, který používáte, v nestabilním stavu, může dojít k chybným operacím, jako je nevolání funkce, volání funkce nesprávným způsobem nebo provedení DOS útoku na MCP server.
MCP Host v jazyce 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í:
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 poté spusťte s modelem ollama, jak je uvedeno níže.
Samozřejmě, pokud je to nutné, nejprve stáhněte model pomocí ollama pull mistral-small
.
I když se obvykle doporučuje claude nebo qwen2.5, momentálně 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 pro dotazy a odpovědi. Proto upravíme kód mcphost
, aby mohl fungovat programovějším způsobem.
Fork mcphost
Jak již bylo ověřeno, mcphost
obsahuje funkce pro extrakci metadat pomocí MCP a volání funkcí. Proto jsou zapotřebí části pro volání LLM, správu MCP serveru a správu historie zpráv.
Následující Runner
z daného balíčku 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 dané části nebudeme podrobně rozebírat. Nicméně se jedná v podstatě o to, co naznačuje název.
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é zde budou použity, naleznete v tomto souboru.
Protože budeme používat provider
od ollama, podívejte se na tento soubor.
Hlavní částí 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}
Samotný kód je sestaven z částí tohoto souboru.
Obsah je zhruba následující:
- Odešle se prompt a seznam nástrojů s požadavkem na provedení nebo generování odpovědi.
- Pokud je generována odpověď, rekurze se zastaví a vrátí se.
- Pokud LLM požádá o provedení nástroje, hostitel zavolá MCP Server.
- Odpověď se přidá do historie a vrátí se zpět k bodu 1.
Na závěr
Už konec?
Ve skutečnosti toho není moc co říct. Tento text byl napsán s cílem pomoci vám pochopit, jak MCP Server funguje. Doufám, že vám tento text alespoň trochu pomohl porozumět fungování MCP hosta.