Forstå MCP-hosten lidt bedre

Hvad er MCP
MCP er en protokol udviklet af Anthropic til Claude. MCP er en forkortelse af Model Context Protocol, og er en protokol, der gør det muligt for en LLM aktivt at anmode om handlinger eller ressourcer fra det eksterne miljø. Da MCP i bogstavelig forstand blot er en protokol til afsendelse af anmodninger og svar, skal udvikleren selv stå for processen og udførelsen.
Om den interne funktionsmåde
Inden vi forklarer den interne funktionsmåde, vil vi kort gennemgå Gemini Function Calling. Gemini Function Calling gør det ligesom MCP muligt for LLM'en selv at tage initiativ til at kalde eksterne handlinger. Man kan så spørge, hvorfor Function Calling overhovedet er medtaget. Grunden er, at Function Calling kom før MCP, og at det, fordi begge benytter OpenAPI-skemaet, er kompatibelt; vi antog derfor, at deres interne funktionsmåde ville ligne hinanden. Da forklaringen af Gemini Function Calling er relativt detaljeret, vurderede vi, at den ville være nyttig, og derfor er den medtaget.

Den overordnede proces er som følger:
- Definer en funktion.
- Send funktionsdefinitionen til Gemini sammen med en prompt.
- "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 anmoder om et funktionskald, hvis det er nødvendigt.
- Hvis Gemini har brug for det, modtager kalderen navnet og parametrene til funktionskaldet.
- Kalderen kan selv beslutte, om kaldet skal udføres.
- Udfør kaldet og returner en gyldig værdi
- Udfør ikke kaldet, men returner data som om det var udført
- Ignorer det helt
- Gemini kan i ovenstående proces anmode om flere funktioner på én gang, eller kalde en funktion, se resultatet og derefter kalde igen.
- Processen afsluttes, når der foreligger et velformuleret svar.
Denne proces stemmer generelt overens med MCP. Det beskrives også på lignende vis i MCP-vejledningen. Det samme gælder for ollama tools.
Heldigvis har disse tre værktøjer, ollama tools, MCP og Gemini Function Calling, næsten identisk skemastruktur, så det er muligt at bruge alle tre steder ved kun at implementere MCP.
En fælles ulempe skal også nævnes. Da det i sidste ende er modellen, der udfører kaldene, kan en model i dårlig form undlade at kalde funktionen, kalde den forkert eller udføre fejlagtige handlinger, såsom at sende DoS-anmodninger til MCP-serveren.
MCP-værter i Go
mark3labs mcphost
I Go findes mcphost, som udvikles af organisationen mark3lab.
Brugen er meget enkel.
1go install github.com/mark3labs/mcphost@latest
Efter installationen opretter du filen $HOME/.mcp.json og skriver følgende i den:
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}
Derefter kører du det med en ollama-model således:
Hvis det er nødvendigt, kan du dog først hente modellen med ollama pull mistral-small.
Generelt anbefales Claude eller qwen2.5, men i øjeblikket anbefaler jeg mistral-small.
1mcphost -m ollama:mistral-small
Men når du kører det på denne måde, kan du kun anvende det som et spørgsmål-og-svar-forløb i CLI-miljøet.
Derfor vil vi nu ændre koden i mcphost, så den kan fungere mere programmerbart.
Fork af mcphost
Som allerede set indeholder mcphost funktionalitet til at udtrække metadata og kalde funktioner via MCP. Der er derfor brug for en del, der kalder LLM'en, en del der håndterer MCP-serveren, og en del der administrerer beskedhistorikken.
Den del er hentet fra Runner i følgende pakke.
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}
Den interne deklaration af denne del gennemgår vi ikke nærmere. Den er dog næsten identisk med navnene.
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}
For mcpClients og tools, der anvendes her, se den relevante fil.
Til provider bruger vi ollamas, så se den relevante fil.
Hovedretten er metoden 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}
Koden er sammensat af dele fra den relevante fil.
Indholdet kan overordnet opsummeres således:
- Send prompten sammen med værktøjslisten og anmod om enten eksekvering eller generering af et svar.
- Når et svar er genereret, stoppes rekursionen, og svaret returneres.
- Hvis LLM'en anmoder om værktøjskørsel, kalder værten MCP-serveren.
- Tilføj svaret til historikken og gå tilbage til punkt 1.
Afslutning
Allerede slut?
Der er faktisk ikke så meget mere at sige. Artiklen er skrevet for at give et overblik over, hvordan en MCP-server fungerer. Jeg håber, at den har hjulpet dig, bare lidt, med at forstå, hvordan en MCP-host fungerer.