Comprendere un po' l'host MCP
Cos'è MCP
Il MCP è un protocollo sviluppato da Anthropic per claude. MCP è l'acronimo di Model Context Protocol ed è un protocollo che consente all'LLM di richiedere attivamente operazioni o risorse esterne. Poiché l'MCP è letteralmente solo un protocollo che fornisce richieste e risposte, il processo e l'esecuzione devono essere gestiti dallo sviluppatore.
Riguardo al Funzionamento Interno
Prima di spiegare il funzionamento interno, esamineremo il Gemini Function Calling. Il Gemini Function Calling, proprio come l'MCP, consente all'LLM di chiamare in modo proattivo operazioni esterne. Ci si potrebbe chiedere perché abbiamo introdotto il Function Calling. Il motivo per cui l'abbiamo introdotto è che il Function Calling è stato rilasciato prima dell'MCP e, poiché entrambi utilizzano lo schema OpenAPI, sono compatibili, e si presumeva che il loro funzionamento reciproco sarebbe stato simile. Pertanto, l'abbiamo introdotto perché la spiegazione del Gemini Function Calling è relativamente più dettagliata e potrebbe essere utile.
Il flusso generale è il seguente:
- Si definisce la funzione.
- Si invia la definizione della funzione a Gemini insieme al prompt.
- "Invia il prompt dell'utente insieme alla dichiarazione/i della funzione al modello. Esso analizza la richiesta e determina se una chiamata di funzione sarebbe utile. In tal caso, risponde con un oggetto JSON strutturato."
- Gemini richiede la chiamata della funzione se necessario.
- Se Gemini ne ha bisogno, il chiamante riceve il nome e i parametri per la chiamata della funzione.
- Il chiamante può decidere se eseguire o meno.
- Se chiamare e restituire un valore legittimo.
- Se non chiamare e restituire i dati come se fosse stato chiamato.
- Se semplicemente ignorare.
- Gemini esegue e richiede operazioni come chiamare più funzioni contemporaneamente in questo processo, o chiamare una funzione e poi chiamarne un'altra in base al risultato.
- Infine, si conclude quando viene prodotta una risposta ben organizzata.
Questo flusso è generalmente coerente con l'MCP. Questo è spiegato in modo simile anche nel tutorial dell'MCP. Anche gli ollama tools sono simili.
Ed è un grande sollievo che questi tre strumenti, ollama tools, MCP e Gemini Function Calling, condividano la struttura dello schema, il che significa che implementando solo l'MCP, si può utilizzare in tutti e tre i contesti.
Ah, e c'è uno svantaggio condiviso da tutti. Poiché è il modello a eseguire, se il modello che state utilizzando non è in buono stato, potrebbe non chiamare la funzione, chiamarla in modo strano, o eseguire operazioni errate come un attacco DOS al server MCP.
Host MCP in Go
mark3lab's mcphost
In Go, c'è mcphost che è in fase di sviluppo da parte dell'organizzazione mark3lab.
L'utilizzo è molto semplice.
1go install github.com/mark3labs/mcphost@latest
Dopo l'installazione, si crea il file $HOME/.mcp.json
e si scrive come segue.
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}
E si esegue con un modello ollama come segue.
Naturalmente, prima, se necessario, si scarica il modello con ollama pull mistral-small
.
Di base si raccomanda claude o qwen2.5, ma io al momento raccomando mistral-small.
1mcphost -m ollama:mistral-small
Tuttavia, eseguendo in questo modo, si può usare solo in modalità domanda-risposta nell'ambiente CLI.
Quindi, modificheremo il codice di questo mcphost
per renderlo più programmabile.
Fork di mcphost
Come già verificato, mcphost
include la funzionalità di estrarre metadati e chiamare funzioni utilizzando l'MCP. Pertanto, sono necessarie le parti che chiamano l'LLM, quelle che gestiscono il server MCP e quelle che gestiscono la cronologia dei messaggi.
Il Runner
del seguente pacchetto è quello che ha acquisito tali parti.
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}
Non esamineremo la dichiarazione interna di questa parte in dettaglio. Tuttavia, è quasi letterale al nome.
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}
Per quanto riguarda mcpClients
e tools
da utilizzare qui, si prega di consultare questo file.
Poiché useremo quello di ollama per provider
, si prega di consultare questo file.
Il piatto forte è il metodo 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}
Il codice stesso è stato ricomposto da alcune parti del file in questione.
Il contenuto è approssimativamente il seguente:
- Si invia l'elenco degli strumenti insieme al prompt per richiedere l'esecuzione o la generazione di una risposta.
- Se viene generata una risposta, la ricorsione si interrompe e viene restituita.
- Se l'LLM lascia una richiesta di esecuzione di uno strumento, l'host chiama l'MCP Server.
- La risposta viene aggiunta alla cronologia e si torna al punto 1.
Conclusione
È già la fine?
In realtà, non c'è molto altro da dire. Questo articolo è stato scritto per aiutarvi a comprendere approssimativamente come funziona l'MCP Server. Spero che questo articolo vi sia stato utile per comprendere, anche se in minima parte, il funzionamento dell'host MCP.