GoSuda

Comprendere un po' l'host MCP

By snowmerak
views ...

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.

FunctionCalling

Il flusso generale è il seguente:

  1. Si definisce la funzione.
  2. Si invia la definizione della funzione a Gemini insieme al prompt.
    1. "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."
  3. Gemini richiede la chiamata della funzione se necessario.
    1. Se Gemini ne ha bisogno, il chiamante riceve il nome e i parametri per la chiamata della funzione.
    2. Il chiamante può decidere se eseguire o meno.
      1. Se chiamare e restituire un valore legittimo.
      2. Se non chiamare e restituire i dati come se fosse stato chiamato.
      3. Se semplicemente ignorare.
  4. 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.
  5. 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:

  1. Si invia l'elenco degli strumenti insieme al prompt per richiedere l'esecuzione o la generazione di una risposta.
  2. Se viene generata una risposta, la ricorsione si interrompe e viene restituita.
  3. Se l'LLM lascia una richiesta di esecuzione di uno strumento, l'host chiama l'MCP Server.
  4. 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.