MCP-host eens nader bekijken

Wat is MCP
MCP is een protocol dat door Anthropic is ontwikkeld voor Claude. MCP is de afkorting van Model Context Protocol en stelt een LLM in staat om actief acties of resources bij de buitenwereld op te vragen. Omdat MCP letterlijk niets meer is dan een protocol voor het versturen van verzoeken en antwoorden, moeten het proces en de uitvoering door de ontwikkelaar zelf worden verzorgd.
Over de interne werking
Voordat ik de interne werking uiteenzet, wil ik kort ingaan op Gemini Function Calling. Gemini Function Calling stelt, net als MCP, een LLM in staat om op eigen initiatief externe acties aan te roepen. Je zult zich afvragen waarom Function Calling hier eigenlijk wordt besproken. De reden is dat Function Calling eerder verscheen dan MCP en dat het, doordat beide een OpenAPI-schema gebruiken, compatibel is; daarom veronderstel ik dat de werking van beide sterk op elkaar lijkt. Omdat de uitleg van Gemini Function Calling relatief uitgebreid is, lijkt het nuttig om die hier te behandelen.

De algemene stroom verloopt als volgt.
- Definieer de functie.
- Stuur de functiedefinitie samen met de prompt naar 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."
- Gemini vraagt om een functieaanroep indien nodig.
- Indien Gemini dit nodig acht, ontvangt de aanroeper de naam en parameters voor de functieaanroep.
- De aanroeper kan bepalen of hij de functie wel of niet uitvoert.
- De functie aanroepen en een geldige waarde teruggeven
- De functie niet aanroepen maar wel gegevens teruggeven alsof dit wel is gebeurd
- Het verzoek simpelweg negeren
- Gemini kan in het bovenstaande proces meerdere functies tegelijk aanroepen, of na het bekijken van het resultaat een functie opnieuw aanroepen, en voert dergelijke acties uit en vraagt deze op.
- Wanneer er uiteindelijk een geordend antwoord is, eindigt het proces.
Deze stroom komt over het algemeen overeen met MCP. Dit wordt ook op vergelijkbare wijze beschreven in de MCP-tutorial. Ook ollama tools werkt op vergelijkbare wijze.
En gelukkig, werkelijk een grote meevaller, delen deze drie hulpmiddelen, ollama tools, MCP en Gemini Function Calling, nagenoeg dezelfde schemastructuur, zodat je door alleen MCP te implementeren het op alle drie kunt gebruiken.
Ach, en er is ook een nadeel dat alle drie delen. Omdat het uiteindelijk het model is dat de functies uitvoert, kan een model in een slechte staat functies niet aanroepen, ze onjuist aanroepen, of een DoS-aanval op de MCP-server veroorzaken.
Een MCP-host in Go
mark3lab's mcphost
In Go bestaat er mcphost, dat wordt ontwikkeld door de organisatie mark3lab.
Het gebruik is erg eenvoudig.
1go install github.com/mark3labs/mcphost@latest
Maak na de installatie het bestand $HOME/.mcp.json aan en schrijf 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}
Voer vervolgens het volgende uit met een ollama-model.
Indien nodig haalt u daarvoor eerst het model op met ollama pull mistral-small.
Over het algemeen raad ik claude of qwen2.5 aan, maar op dit moment raad ik mistral-small aan.
1mcphost -m ollama:mistral-small
Als je het zo uitvoert, kun je het echter alleen gebruiken als vraag-en-antwoordsysteem in een CLI-omgeving.
Daarom gaan we de code van deze mcphost aanpassen zodat het op een meer programmeerbare manier kan werken.
Fork van mcphost
Zoals we al hebben gezien, bevat mcphost de functionaliteit om via MCP metadata op te halen en functies aan te roepen. Daarom is er een onderdeel nodig voor het aanroepen van de LLM, het omgaan met de MCP-server en het beheren van de berichtengeschiedenis.
Het onderdeel dat hiervoor is overgenomen, is de Runner uit het volgende pakket.
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 declaraties van dit onderdeel bespreek ik niet afzonderlijk. Ze hebben echter vrijwel dezelfde namen als wat ze representeren.
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, raadpleegt u dit bestand.
Voor provider gebruiken we die van ollama, dus raadpleegt u dit bestand.
Het belangrijkste onderdeel is de methode 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}
De code zelf is samengesteld uit delen van dit bestand.
De inhoud is grofweg als volgt.
- Stuur de lijst met tools samen met de prompt om te vragen of er uitgevoerd moet worden, of om een antwoord te genereren.
- Zodra een antwoord is gegenereerd, stopt de recursie en wordt het geretourneerd.
- Als de LLM een verzoek tot uitvoering van een tool achterlaat, roept de host de MCP-server aan.
- Voeg het resultaat toe aan de geschiedenis en keer terug naar stap 1.
Tot slot
Al klaar?
Eigenlijk heb ik niet zoveel te zeggen. Dit artikel is grofweg bedoeld om u inzicht te geven in hoe een MCP-server werkt. Ik hoop dat dit artikel u enigszins heeft geholpen om de werking van een MCP-host te begrijpen.