Una breve comprensione di MCP host
Cos'è MCP
MCP è un protocollo sviluppato da Anthropic per claude. MCP è l'acronimo di Model Context Protocol ed è un protocollo che consente agli LLM di richiedere attivamente azioni o risorse esterne. Poiché MCP è letteralmente solo un protocollo che fornisce richieste e risposte, il processo e l'esecuzione devono essere gestiti dallo sviluppatore.
Informazioni sul funzionamento interno
Prima di spiegare il funzionamento interno, esamineremo Gemini Function Calling. Gemini Function Calling, analogamente a MCP, consente agli LLM di richiamare autonomamente azioni esterne. Ci si potrebbe quindi chiedere perché Function Calling sia stato introdotto in questo contesto. Il motivo per cui è stato introdotto è che Function Calling è apparso prima di MCP e, poiché entrambi utilizzano lo schema OpenAPI, sono compatibili e si presume che il loro funzionamento reciproco sia simile. Pertanto, poiché la spiegazione di Gemini Function Calling è relativamente più dettagliata, è stata ritenuta utile e per questo è stata inclusa.
Il flusso generale è il seguente.
- Definire la funzione.
- Inviare la definizione della funzione a Gemini insieme al 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."
- Se necessario, Gemini richiede la chiamata di funzione.
- Se Gemini lo richiede, il chiamante riceve il nome e i parametri per la chiamata di funzione.
- Il chiamante può decidere se eseguire o meno.
- Se chiamare e restituire un valore valido.
- Se non chiamare ma restituire dati come se fosse stato chiamato.
- Se semplicemente ignorare.
- In questo processo, Gemini esegue e richiede azioni come chiamare più funzioni contemporaneamente o chiamare una funzione e poi chiamarne un'altra in base ai risultati.
- Quando finalmente viene prodotta una risposta strutturata, il processo termina.
Questo flusso è generalmente in linea con MCP. Ciò è spiegato in modo simile anche nel tutorial di MCP. Ciò è simile anche per ollama tools.
E fortunatamente, questi 3 strumenti, ollama tools, MCP e Gemini Function Calling, condividono la struttura dello schema in modo tale che implementando solo MCP, è possibile utilizzarlo con tutti e 3.
Ah, e c'è uno svantaggio che tutti condividono. Poiché è il modello a eseguire l'azione, se il modello che state utilizzando non è in buone condizioni, potrebbe non chiamare la funzione, chiamarla in modo strano o causare malfunzionamenti come inviare un DOS al server MCP.
Host MCP in Go
mcphost di mark3lab
In Go, esiste 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, creare il file $HOME/.mcp.json
e scrivere quanto 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 poi eseguirlo con un modello ollama come segue.
Naturalmente, prima di ciò, se necessario, scaricare il modello con ollama pull mistral-small
.
Sebbene claude o qwen2.5 siano generalmente raccomandati, al momento consiglio mistral-small.
1mcphost -m ollama:mistral-small
Tuttavia, eseguendolo in questo modo, è utilizzabile solo in modalità domanda-risposta nell'ambiente CLI.
Pertanto, modificheremo il codice di questo mcphost
per farlo funzionare in modo più programmabile.
Fork di mcphost
Come già verificato, mcphost
include funzionalità per estrarre metadata e chiamare funzioni utilizzando MCP.
Pertanto, sono necessarie le parti che chiamano l'llm, gestiscono il server mcp e gestiscono la message history.
Il Runner
nel seguente package è la parte che è stata presa.
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
18// Runner is the main struct for running the MCP host
19type Runner struct {
20 // provider is the LLM provider
21 provider llm.Provider
22 // mcpClients are the clients for the MCP servers
23 mcpClients map[string]*mcpclient.StdioMCPClient
24 // tools are the available tools
25 tools []llm.Tool
26
27 // messages is the history of messages
28 messages []history.HistoryMessage
29}
Non esamineremo le dichiarazioni interne di questa parte separatamente. Tuttavia, i nomi sono quasi autoesplicativi.
1// NewRunner creates a new Runner
2func NewRunner(systemPrompt string, provider llm.Provider, mcpClients map[string]*mcpclient.StdioMCPClient, tools []llm.Tool) *Runner {
3 return &Runner{
4 provider: provider,
5 mcpClients: mcpClients,
6 tools: tools,
7 messages: []history.HistoryMessage{
8 {
9 Role: "system",
10 Content: []history.ContentBlock{{
11 Type: "text",
12 Text: systemPrompt,
13 }},
14 },
15 },
16 }
17}
Per mcpClients
e tools
che saranno utilizzati qui, si prega di fare riferimento a questo file.
Poiché utilizzeremo quello di ollama per provider
, si prega di fare riferimento a questo file.
Il piatto principale è il metodo Run
.
1// Run executes the prompt and returns the response
2func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
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 llmMessages := make([]llm.Message, len(r.messages))
14 for i := range r.messages {
15 llmMessages[i] = &r.messages[i]
16 }
17
18 const initialBackoff = 1 * time.Second
19 const maxRetries int = 5
20 const maxBackoff = 30 * time.Second
21
22 var message llm.Message
23 var err error
24 backoff := initialBackoff
25 retries := 0
26 for {
27 message, err = r.provider.CreateMessage(
28 context.Background(),
29 prompt,
30 llmMessages,
31 r.tools,
32 )
33 if err != nil {
34 if strings.Contains(err.Error(), "overloaded_error") {
35 if retries >= maxRetries {
36 return "", fmt.Errorf(
37 "claude is currently overloaded. please wait a few minutes and try again",
38 )
39 }
40
41 time.Sleep(backoff)
42 backoff *= 2
43 if backoff > maxBackoff {
44 backoff = maxBackoff
45 }
46 retries++
47 continue
48 }
49
50 return "", err
51 }
52
53 break
54 }
55
56 var messageContent []history.ContentBlock
57
58 var toolResults []history.ContentBlock
59 messageContent = []history.ContentBlock{}
60
61 if message.GetContent() != "" {
62 messageContent = append(messageContent, history.ContentBlock{
63 Type: "text",
64 Text: message.GetContent(),
65 })
66 }
67
68 for _, toolCall := range message.GetToolCalls() {
69 input, _ := json.Marshal(toolCall.GetArguments())
70 messageContent = append(messageContent, history.ContentBlock{
71 Type: "tool_use",
72 ID: toolCall.GetID(),
73 Name: toolCall.GetName(),
74 Input: input,
75 })
76
77 parts := strings.Split(toolCall.GetName(), "__")
78
79 serverName, toolName := parts[0], parts[1]
80 mcpClient, ok := r.mcpClients[serverName]
81 if !ok {
82 continue
83 }
84
85 var toolArgs map[string]interface{}
86 if err := json.Unmarshal(input, &toolArgs); err != nil {
87 continue
88 }
89
90 var toolResultPtr *mcp.CallToolResult
91 req := mcp.CallToolRequest{}
92 req.Params.Name = toolName
93 req.Params.Arguments = toolArgs
94 toolResultPtr, err = mcpClient.CallTool(
95 context.Background(),
96 req,
97 )
98
99 if err != nil {
100 errMsg := fmt.Sprintf(
101 "Error calling tool %s: %v",
102 toolName,
103 err,
104 )
105 log.Printf("Error calling tool %s: %v", toolName, err)
106
107 toolResults = append(toolResults, history.ContentBlock{
108 Type: "tool_result",
109 ToolUseID: toolCall.GetID(),
110 Content: []history.ContentBlock{{
111 Type: "text",
112 Text: errMsg,
113 }},
114 })
115
116 continue
117 }
118
119 toolResult := *toolResultPtr
120
121 if toolResult.Content != nil {
122 resultBlock := history.ContentBlock{
123 Type: "tool_result",
124 ToolUseID: toolCall.GetID(),
125 Content: toolResult.Content,
126 }
127
128 var resultText string
129 for _, item := range toolResult.Content {
130 if contentMap, ok := item.(map[string]interface{}); ok {
131 if text, ok := contentMap["text"]; ok {
132 resultText += fmt.Sprintf("%v ", text)
133 }
134 }
135 }
136
137 resultBlock.Text = strings.TrimSpace(resultText)
138
139 toolResults = append(toolResults, resultBlock)
140 }
141 }
142
143 r.messages = append(r.messages, history.HistoryMessage{
144 Role: message.GetRole(),
145 Content: messageContent,
146 })
147
148 if len(toolResults) > 0 {
149 r.messages = append(r.messages, history.HistoryMessage{
150 Role: "user",
151 Content: toolResults,
152 })
153
154 return r.Run(ctx, "")
155 }
156
157 return message.GetContent(), nil
158}
Il codice stesso è stato assemblato da parte del codice in questo file.
Il contenuto è approssimativamente il seguente.
- Inviare il prompt insieme all'elenco degli tools 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 tool, l'host chiama l'MCP Server.
- Aggiungere la risposta alla history e tornare al punto 1.
In conclusione
Già finito?
In realtà non c'è molto altro da dire. Questo articolo è stato scritto per aiutarvi a comprendere approssimativamente come funziona un MCP Server. Spero che questo articolo vi sia stato di piccolo aiuto nel comprendere il funzionamento di un host MCP.