Betekintés az MCP host működésébe
Mi az MCP?
Az MCP egy protokoll, amelyet az Anthropic fejlesztett a claude számára. Az MCP a Model Context Protocol rövidítése, és egy olyan protokoll, amely lehetővé teszi az LLM számára, hogy proaktívan kérjen műveleteket vagy erőforrásokat kívülről. Mivel az MCP szó szerint csupán egy protokoll, amely kéréseket és válaszokat ad, a folyamatot és a végrehajtást a fejlesztőnek kell elvégeznie.
A belső működésről
Mielőtt a belső működést ismertetnénk, tekintsük át a Gemini Function Calling működését. A Gemini Function Calling az MCP-hez hasonlóan lehetővé teszi az LLM számára, hogy proaktívan hívjon külső műveleteket. Felmerülhet tehát a kérdés, hogy miért említjük meg a Function Callingot. Azért említjük meg, mert a Function Calling korábban jelent meg, mint az MCP, és mivel mindkettő az OpenAPI sémát használja, kompatibilisek, így feltételezhető, hogy működésük hasonló. Ezért, mivel a Gemini Function Calling magyarázata részletesebb, hasznosnak bizonyulhat, így ezt vettük alapul.
Az általános folyamat a következő:
- Meghatározza a függvényt.
- Elküldi a függvény definícióját a Gemini-nek a prompttal együtt.
- "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."
- Ha szükséges, a Gemini függvényhívást kér.
- Ha a Gemini szükségesnek ítéli, a hívó fél megkapja a függvényhívás nevét és paramétereit.
- A hívó fél eldöntheti, hogy végrehajtja-e vagy sem.
- Végrehajtja és visszaad egy érvényes értéket.
- Nem hajtja végre, de adatokat ad vissza, mintha végrehajtotta volna.
- Egyszerűen figyelmen kívül hagyja.
- A Gemini ebben a folyamatban egyszerre több függvényt is hívhat, vagy függvényhívás után az eredményt látva újabb hívásokat kezdeményezhet, illetve kérhet ilyen műveleteket.
- Végül, ha rendezett válasz érkezik, a folyamat befejeződik.
Ez a folyamat általában összhangban van az MCP-vel. Ezt az MCP oktatóanyaga is hasonlóan magyarázza. Az ollama tools is hasonlóan működik.
És nagy szerencsére ez a 3 eszköz, az ollama tools, az MCP és a Gemini Function Calling szinte teljesen megosztja a séma szerkezetét, így elegendő csak az MCP-t implementálni ahhoz, hogy mindhárom helyen használható legyen.
Ó, és van egy hátrányuk, amelyet mindannyian megosztanak. Mivel végül is a modell hajtja végre, ha a használt modell állapota nem jó, előfordulhat, hogy nem hívja meg a függvényt, furcsán hívja meg, vagy akár DOS-t indít az MCP szerver ellen, és egyéb hibás működések léphetnek fel.
MCP host Go nyelven
mark3lab's mcphost
Go nyelven létezik a mark3lab nevű szervezet által fejlesztett mcphost.
Használata nagyon egyszerű.
1go install github.com/mark3labs/mcphost@latest
Telepítés után hozzon létre egy $HOME/.mcp.json
nevű fájlt, és írja bele a következőt:
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}
Ezután futtassa az ollama modellel a következőképpen.
Természetesen előtte, ha szükséges, töltse le a modellt az ollama pull mistral-small
paranccsal.
Alapvetően a claude vagy a qwen2.5 ajánlott, de jelenleg a mistral-smallt ajánlom.
1mcphost -m ollama:mistral-small
Azonban így futtatva csak CLI környezetben használható kérdés-válasz módban.
Ezért módosítsuk az mcphost
kódját, hogy programozhatóbb módon működjön.
mcphost fork
Ahogy már láthattuk, az mcphost
tartalmazza a metaadatok kinyerésének és a függvények hívásának funkcióját az MCP használatával. Ezért szükség van az LLM hívását, az MCP szerver kezelését és az üzenetelőzmények kezelését végző részekre.
Az ezeket a részeket tartalmazó Runner
a következő csomagban található:
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}
Az ehhez tartozó belső deklarációkat külön nem nézzük meg. De szinte pontosan a nevüknek megfelelően működnek.
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}
Az itt használt mcpClients
és tools
kapcsán kérjük, tekintse meg a megfelelő fájlt.
A provider
az ollama-tól származik, így kérjük, tekintse meg a megfelelő fájlt.
A fő attrakció a Run
metódus.
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 // Kezdeti visszalépési idő
18 const maxRetries int = 5 // Maximális újrapróbálkozások száma
19 const maxBackoff = 30 * time.Second // Maximális visszalépési idő
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", // claude jelenleg túlterhelt. várjon néhány percet, majd próbálja újra
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", // Hiba a(z) %s eszköz hívásakor: %v
101 toolName,
102 err,
103 )
104 log.Printf("Error calling tool %s: %v", toolName, err) // Hiba a(z) %s eszköz hívásakor: %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}
Maga a kód a megfelelő fájl egyes részeinek összeillesztéséből származik.
A tartalom nagyjából a következő:
- Elküldi a promptot és az eszközlistát, kérve a végrehajtást vagy a válasz generálását.
- Ha válasz generálódik, leállítja a rekurziót és visszaadja azt.
- Ha az LLM eszköz végrehajtási kérést hagy, a host meghívja az MCP Servert.
- A választ hozzáadja az előzményekhez, és visszatér az 1. ponthoz.
Végül
Már vége?
Valójában nincs sok mondanivaló. Ez a cikk azért készült, hogy segítsen megérteni, hogyan működik nagyjából az MCP Server. Remélem, hogy ez a cikk kis mértékben is hozzájárult ahhoz, hogy megértsék az MCP host működését.