GoSuda

Una breve comprensione di MCP host

By snowmerak
views ...

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.

FunctionCalling

Il flusso generale è il seguente.

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

  1. Inviare il prompt insieme all'elenco degli tools 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 tool, l'host chiama l'MCP Server.
  4. 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.