Att förstå MCP-värden lite grundligare

Vad är MCP
MCP är ett protokoll som Anthropic utvecklat för Claude. MCP är en förkortning av Model Context Protocol och är ett protokoll som gör det möjligt för LLM:er att aktivt begära åtgärder eller resurser från externa system. Eftersom MCP i bokstavlig mening endast är ett protokoll för att skicka förfrågningar och svar, måste utvecklaren själv hantera processen och exekveringen.
Om den interna funktionen
Innan vi förklarar den interna funktionen vill jag kort behandla Gemini Function Calling. Gemini Function Calling gör, precis som MCP, att LLM:en självständigt kan anropa externa funktioner. Man kan då undra varför Function Calling överhuvudtaget tas upp. Anledningen är att Function Calling lanserades före MCP och att den, eftersom den också använder OpenAPI-schema, är kompatibel med MCP, vilket gör att deras beteende antas vara likartat. Eftersom beskrivningen av Gemini Function Calling är relativt detaljerad, bedömer jag att den kan vara till hjälp och därför tar jag upp den här.

Det övergripande flödet ser ut så här.
- Funktionen definieras.
- Funktionsdefinitionen skickas till Gemini tillsammans med prompten.
- "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 begär ett funktionsanrop om det behövs.
- Om Gemini behöver ett funktionsanrop tar anroparen emot namnet och parametrarna för anropet.
- Anroparen kan besluta om att utföra anropet eller inte.
- Anropa funktionen och returnera ett giltigt värde
- Anropa inte funktionen utan returnera data som om den hade anropats
- Ignorera den helt
- Gemini kan inom denna process begära att flera funktioner anropas samtidigt, eller att en funktion anropas och därefter, med hänsyn till resultatet, att ytterligare anrop görs.
- När ett välstrukturerat svar erhålls avslutas processen.
Detta flöde överensstämmer i allmänhet med MCP. Det beskrivs på liknande sätt i MCP:s handledning. Det liknar även ollama tools.
Och lyckligtvis är dessa tre verktyg, ollama tools, MCP och Gemini Function Calling, i praktiken baserade på en gemensam schemastruktur, så det är möjligt att implementera endast MCP och använda det i alla tre miljöerna.
Det finns också en nackdel som alla delar. Eftersom det i slutändan är modellen som utför anropen, kan en modell i dålig form underlåta att anropa funktioner, anropa dem felaktigt eller till och med överbelasta MCP-servern med DoS-liknande trafik och därmed bete sig felaktigt.
MCP-värd i Go
mark3lab:s mcphost
I Go finns mcphost, som utvecklas av organisationen mark3lab.
Användningen är mycket enkel.
1go install github.com/mark3labs/mcphost@latest
Efter installationen skapar du filen $HOME/.mcp.json och skriver följande 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}
Kör sedan följande med en ollama-modell.
Om det behövs hämtar du dessförinnan modellen med ollama pull mistral-small.
I allmänhet rekommenderas Claude eller qwen2.5, men för närvarande rekommenderar jag mistral-small.
1mcphost -m ollama:mistral-small
Om du kör på detta sätt kan du dock endast använda det som ett frågesvarssystem i CLI-miljö.
Därför ska vi ändra koden i mcphost så att det kan fungera på ett mer programmerbart sätt.
Fork av mcphost
Som redan konstaterats innehåller mcphost funktioner för att extrahera metadata och anropa funktioner med hjälp av MCP. Därför behövs delar som hanterar anrop till LLM, hanterar MCP-servrar och hanterar meddelandehistoriken.
Den del som motsvarar detta är Runner i följande paket.
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}
Jag går inte igenom de interna deklarationerna i detta avsnitt. De är dock i stort sett namngivna efter sin funktion.
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}
För mcpClients och tools som används här, se denna fil.
För provider använder vi ollamas variant, så se denna fil.
Huvudrätten är 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}
Kodens grundstruktur är en sammanslagning av delar från denna fil.
I huvudsak fungerar det så här.
- Verktygslistan skickas tillsammans med prompten för att begära antingen ett beslut om exekvering eller ett genererat svar.
- När ett svar har genererats avbryts rekursionen och svaret returneras.
- Om LLM:en begär exekvering av ett verktyg anropar värden MCP-servern.
- Resultatet läggs till i historiken och processen återgår till steg 1.
Avslutningsvis
Redan slut?
Egentligen finns det inte så mycket mer att säga. Texten har skrivits för att ge en översiktlig förståelse för hur en MCP-server fungerar. Jag hoppas att den har bidragit något, om än litet, till att förstå hur en MCP-värd fungerar.