MCP host enigszins begrijpen
Wat is MCP?
MCP is een protocol dat door Anthropic is ontwikkeld voor Claude. MCP is een afkorting van Model Context Protocol en is een protocol dat LLM's in staat stelt proactief externe acties of bronnen aan te vragen. Aangezien MCP letterlijk slechts een protocol is dat verzoeken en antwoorden verstrekt, moeten het proces en de uitvoering door de ontwikkelaar worden afgehandeld.
Over de interne werking
Voordat ik de interne werking toelicht, zal ik eerst Gemini Function Calling bespreken. Gemini Function Calling stelt, net als MCP, LLM's in staat proactief externe acties aan te roepen. U vraagt zich misschien af waarom ik Function Calling erbij heb gehaald. De reden is dat Function Calling eerder bestond dan MCP, en aangezien beide dezelfde OpenAPI-schema's gebruiken, zijn ze compatibel, en vermoedde ik dat hun onderlinge werking vergelijkbaar zou zijn. Daarom heb ik Gemini Function Calling erbij gehaald, omdat de uitleg ervan gedetailleerder is en nuttig kan zijn.
De algemene stroom is als volgt:
- Definieer de functie.
- Stuur de functiedefinitie samen met de prompt naar Gemini.
- "Stuur de gebruikersprompt samen met de functiedeclaratie(s) naar het model. Het analyseert het verzoek en bepaalt of een functieaanroep nuttig zou zijn. Zo ja, dan antwoordt het met een gestructureerd JSON-object."
- Gemini vraagt een functieaanroep aan indien nodig.
- Indien nodig ontvangt de aanroeper van Gemini de naam en parameters voor de functieaanroep.
- De aanroeper kan beslissen of hij de aanroep uitvoert of niet.
- Of de aanroep uitvoeren en een geldige waarde retourneren.
- Of de aanroep niet uitvoeren, maar gegevens retourneren alsof deze wel is uitgevoerd.
- Of deze gewoon negeren.
- Gemini voert in het bovenstaande proces meerdere functies tegelijk uit of vraagt deze aan, zoals het aanroepen van een functie en vervolgens opnieuw aanroepen op basis van het resultaat.
- Uiteindelijk eindigt het proces wanneer een geordend antwoord wordt verkregen.
Deze stroom komt over het algemeen overeen met MCP. Dit wordt ook op vergelijkbare wijze uitgelegd in de MCP-tutorial. Dit geldt ook voor ollama tools.
En gelukkig delen deze drie tools, ollama tools, MCP en Gemini Function Calling, vrijwel dezelfde schemastructuur, wat betekent dat u door slechts één van deze, MCP, te implementeren, deze op alle drie de plaatsen kunt gebruiken.
Oh, en er is een nadeel dat ze allemaal delen. Uiteindelijk wordt het model uitgevoerd, dus als het model dat u gebruikt in slechte staat verkeert, kan het functies niet aanroepen, ze op een vreemde manier aanroepen, of storingen veroorzaken zoals het uitvoeren van een DOS-aanval op de MCP-server.
MCP-host in Go
mark3lab's mcphost
In Go is er mcphost, dat wordt ontwikkeld door een organisatie genaamd mark3lab.
Het gebruik is zeer eenvoudig.
1go install github.com/mark3labs/mcphost@latest
Na installatie, maakt u het bestand $HOME/.mcp.json
aan en schrijft u het als volgt:
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}
En voer het vervolgens uit met het ollama-model.
Natuurlijk, als het nodig is, ontvangt u eerst het model met ollama pull mistral-small
.
Hoewel claude of qwen2.5 standaard worden aanbevolen, raad ik op dit moment mistral-small aan.
1mcphost -m ollama:mistral-small
Als u het echter op deze manier uitvoert, kunt u het alleen in een CLI-omgeving gebruiken voor vraag-en-antwoord.
Daarom zullen we de code van deze mcphost
aanpassen om deze programmeerbaarder te maken.
mcphost fork
Zoals reeds vastgesteld, bevat mcphost
functionaliteit voor het extraheren van metadata en het aanroepen van functies met behulp van MCP. Daarom zijn onderdelen nodig voor het aanroepen van de LLM, het beheren van de MCP-server en het beheren van de berichtengeschiedenis.
De Runner
van het volgende pakket heeft de betreffende onderdelen overgenomen.
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}
De interne declaratie van dat deel zullen we niet afzonderlijk bekijken. Het is echter vrijwel letterlijk de naam.
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}
Voor de mcpClients
en tools
die hier worden gebruikt, zie dit bestand.
Aangezien de provider
die van ollama zal zijn, zie dit bestand.
Het hoofdgerecht is de Run
-methode.
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}
De code zelf is samengesteld uit delen van dit bestand.
De inhoud is grofweg als volgt:
- Verzend de prompt samen met de lijst met tools om de uitvoering of het genereren van een antwoord aan te vragen.
- Als er een antwoord wordt gegenereerd, stopt de recursie en wordt het antwoord geretourneerd.
- Als de LLM een tooluitvoerverzoek achterlaat, roept de host de MCP Server aan.
- Voeg het antwoord toe aan de geschiedenis en keer terug naar stap 1.
Tot slot
Nu al klaar?
Er is eigenlijk niet veel meer te zeggen. Dit artikel is geschreven om u te helpen een globaal begrip te krijgen van de werking van de MCP Server. Ik hoop dat dit artikel u in geringe mate heeft geholpen de werking van de MCP-host te begrijpen.