Försök att förstå MCP host lite grann
Vad är MCP?
MCP är ett protokoll utvecklat av Anthropic för Claude. MCP är en förkortning för Model Context Protocol, och det är ett protokoll som gör det möjligt för en LLM att aktivt begära åtgärder eller resurser externt. Eftersom MCP bokstavligen bara är ett protokoll för att skicka förfrågningar och få svar, måste processen och exekveringen hanteras av utvecklaren.
Intern funktion
Innan vi förklarar den interna funktionen, låt oss kort nämna Gemini Function Calling. Gemini Function Calling, precis som MCP, gör det möjligt för en LLM att autonomt anropa externa åtgärder. Man kan undra varför Function Calling togs upp. Anledningen är att Function Calling kom före MCP, och eftersom båda använder OpenAPI-schemat är de kompatibla, vilket antyder att deras interaktioner skulle vara likartade. Därför togs Gemini Function Calling upp, då dess förklaringar är mer detaljerade och kan vara till hjälp.
Det övergripande flödet är följande:
- Definiera funktionen.
- Skicka funktionsdefinitionen 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 det, tar anroparen emot namnet och parametrarna för funktionsanropet.
- Anroparen kan bestämma om anropet ska utföras eller inte.
- Om ett legitimt värde ska returneras efter anropet.
- Om data ska returneras som om anropet hade utförts, utan att faktiskt anropa.
- Om det bara ska ignoreras.
- Gemini utför och begär åtgärder som att anropa flera funktioner samtidigt under ovanstående process, eller anropa igen efter att ha granskat resultatet av ett funktionsanrop.
- När ett välorganiserat svar erhålls, avslutas processen.
Detta flöde är generellt sett i linje med MCP. Detta beskrivs även på liknande sätt i MCP:s handledning. Detta liknar även ollama tools.
Och lyckligtvis delar dessa tre verktyg – ollama tools, MCP och Gemini Function Calling – en liknande schemastruktur, vilket innebär att genom att implementera MCP kan det användas på alla tre ställen.
Åh, och det finns en gemensam nackdel för alla. Eftersom modellen i slutändan utför exekveringen, om modellen du använder är i dåligt skick, kan den misslyckas med att anropa funktioner, anropa dem på ett konstigt sätt, eller utföra felaktiga åtgärder som att utföra en DOS-attack mot MCP-servern.
MCP Host 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, skapa filen $HOME/.mcp.json
och skriv följande:
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}
Och kör sedan med ollama-modellen enligt följande:
Naturligtvis, om det behövs, ladda ner modellen med ollama pull mistral-small
först.
Även om claude eller qwen2.5 rekommenderas som standard, rekommenderar jag för närvarande mistral-small.
1mcphost -m ollama:mistral-small
Men om du kör det på detta sätt kan det bara användas i en CLI-miljö för frågor och svar.
Därför kommer vi att modifiera koden för mcphost
för att få den att fungera mer programmerbart.
mcphost Fork
Som redan konstaterats inkluderar mcphost
funktionalitet för att extrahera metadata och anropa funktioner med hjälp av MCP. Därför behövs delar för att anropa LLM, hantera MCP-servern och hantera meddelandehistoriken.
Runner
i följande paket är den del som har hämtats:
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 interna deklarationen av denna del kommer inte att granskas separat. Dock är namnen nästan självförklarande.
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 kommer att användas här, vänligen se denna fil.
Eftersom provider
kommer att använda ollama:s, vänligen se denna fil.
Huvudrätten är metoden Run
.
1func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
2 // Om prompten inte är tom, lägg till den i meddelandehistoriken.
3 if len(prompt) != 0 {
4 r.messages = append(r.messages, history.HistoryMessage{
5 Role: "user",
6 Content: []history.ContentBlock{{
7 Type: "text",
8 Text: prompt,
9 }},
10 })
11 }
12
13 // Konvertera meddelandehistoriken till LLM-meddelandeformat.
14 llmMessages := make([]llm.Message, len(r.messages))
15 for i := range r.messages {
16 llmMessages[i] = &r.messages[i]
17 }
18
19 // Definiera backoff-konstanter för återförsök.
20 const initialBackoff = 1 * time.Second
21 const maxRetries int = 5
22 const maxBackoff = 30 * time.Second
23
24 var message llm.Message
25 var err error
26 backoff := initialBackoff
27 retries := 0
28 // Loop för att skapa meddelanden med återförsök vid fel.
29 for {
30 message, err = r.provider.CreateMessage(
31 context.Background(),
32 prompt,
33 llmMessages,
34 r.tools,
35 )
36 if err != nil {
37 // Hantera överbelastningsfel specifikt.
38 if strings.Contains(err.Error(), "overloaded_error") {
39 if retries >= maxRetries {
40 return "", fmt.Errorf(
41 "claude is currently overloaded. please wait a few minutes and try again",
42 )
43 }
44
45 time.Sleep(backoff)
46 backoff *= 2
47 if backoff > maxBackoff {
48 backoff = maxBackoff
49 }
50 retries++
51 continue
52 }
53
54 return "", err
55 }
56
57 break // Bryt loopen om meddelandet skapades framgångsrikt.
58 }
59
60 var messageContent []history.ContentBlock
61
62 var toolResults []history.ContentBlock
63 messageContent = []history.ContentBlock{}
64
65 // Lägg till meddelandets innehåll om det finns.
66 if message.GetContent() != "" {
67 messageContent = append(messageContent, history.ContentBlock{
68 Type: "text",
69 Text: message.GetContent(),
70 })
71 }
72
73 // Iterera över verktygsanrop och utför dem.
74 for _, toolCall := range message.GetToolCalls() {
75 input, _ := json.Marshal(toolCall.GetArguments())
76 // Lägg till verktygsanropet till meddelandets innehåll.
77 messageContent = append(messageContent, history.ContentBlock{
78 Type: "tool_use",
79 ID: toolCall.GetID(),
80 Name: toolCall.GetName(),
81 Input: input,
82 })
83
84 // Dela upp verktygsnamnet i servernamn och verktygsnamn.
85 parts := strings.Split(toolCall.GetName(), "__")
86
87 serverName, toolName := parts[0], parts[1]
88 mcpClient, ok := r.mcpClients[serverName]
89 if !ok { // Hoppa över om MCP-klienten inte hittas.
90 continue
91 }
92
93 var toolArgs map[string]interface{}
94 // Avkoda verktygsargumenten från JSON.
95 if err := json.Unmarshal(input, &toolArgs); err != nil {
96 continue
97 }
98
99 var toolResultPtr *mcp.CallToolResult
100 req := mcp.CallToolRequest{}
101 req.Params.Name = toolName
102 req.Params.Arguments = toolArgs
103 // Anropa verktyget via MCP-klienten.
104 toolResultPtr, err = mcpClient.CallTool(
105 context.Background(),
106 req,
107 )
108
109 if err != nil {
110 // Hantera fel vid verktygsanrop.
111 errMsg := fmt.Sprintf(
112 "Error calling tool %s: %v",
113 toolName,
114 err,
115 )
116 log.Printf("Error calling tool %s: %v", toolName, err)
117
118 // Lägg till felmeddelandet som ett verktygsresultat.
119 toolResults = append(toolResults, history.ContentBlock{
120 Type: "tool_result",
121 ToolUseID: toolCall.GetID(),
122 Content: []history.ContentBlock{{
123 Type: "text",
124 Text: errMsg,
125 }},
126 })
127
128 continue
129 }
130
131 toolResult := *toolResultPtr
132
133 // Om verktygsresultatet har innehåll, lägg till det.
134 if toolResult.Content != nil {
135 resultBlock := history.ContentBlock{
136 Type: "tool_result",
137 ToolUseID: toolCall.GetID(),
138 Content: toolResult.Content,
139 }
140
141 var resultText string
142 // Extrahera textinnehåll från verktygsresultatet.
143 for _, item := range toolResult.Content {
144 if contentMap, ok := item.(map[string]interface{}); ok {
145 if text, ok := contentMap["text"]; ok {
146 resultText += fmt.Sprintf("%v ", text)
147 }
148 }
149 }
150
151 resultBlock.Text = strings.TrimSpace(resultText)
152
153 toolResults = append(toolResults, resultBlock)
154 }
155 }
156
157 // Lägg till det genererade meddelandet till historiken.
158 r.messages = append(r.messages, history.HistoryMessage{
159 Role: message.GetRole(),
160 Content: messageContent,
161 })
162
163 // Om det finns verktygsresultat, lägg till dem som ett nytt användarmeddelande och kör igen rekursivt.
164 if len(toolResults) > 0 {
165 r.messages = append(r.messages, history.HistoryMessage{
166 Role: "user",
167 Content: toolResults,
168 })
169
170 return r.Run(ctx, "")
171 }
172
173 // Returnera det slutliga meddelandets innehåll.
174 return message.GetContent(), nil
175}
Själva koden är en sammanställning av delar av koden från denna fil.
Innehållet är ungefär följande:
- Skicka prompten och verktygslistan för att begära exekvering eller svarsgenerering.
- Om ett svar genereras, stoppa rekursionen och returnera.
- Om LLM begär verktygsexekvering, anropar hosten MCP Server.
- Lägg till svaret i historiken och återgå till steg 1.
Avslutningsvis
Redan slut?
Faktum är att det inte finns så mycket att säga. Detta inlägg är skrivet för att hjälpa dig att förstå hur en MCP Server fungerar i stora drag. Jag hoppas att detta inlägg har varit till viss hjälp för att förstå hur en MCP host fungerar.