GoSuda

Comprendre un peu l'hôte MCP

By snowmerak
views ...

Qu'est-ce que MCP ?

Le MCP est un protocole développé par Anthropic pour claude. MCP est l'abréviation de Model Context Protocol, un protocole qui permet aux LLM de demander activement des actions ou des ressources externes. Le MCP n'est littéralement qu'un protocole de requête et de réponse, de sorte que le processus et l'exécution doivent être effectués par le développeur.

Concernant le fonctionnement interne

Avant d'expliquer le fonctionnement interne, nous allons aborder le Gemini Function Calling. Le Gemini Function Calling, tout comme le MCP, permet aux LLM d'appeler de manière proactive des actions externes. Vous vous demanderez alors pourquoi nous avons spécifiquement introduit le Function Calling. La raison est que le Function Calling est apparu avant le MCP et qu'il est compatible, utilisant le même schéma OpenAPI, ce qui nous a fait supposer que leurs opérations seraient similaires. Par conséquent, les explications du Gemini Function Calling étant plus détaillées, nous l'avons inclus car il pourrait être utile.

FunctionCalling

Le flux global est le suivant :

  1. Définir la fonction.
  2. Envoyer la définition de la fonction à Gemini avec le 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. Gemini demande l'appel de fonction si nécessaire.
    1. Si Gemini en a besoin, l'appelant reçoit le nom et les paramètres pour l'appel de fonction.
    2. L'appelant peut décider d'exécuter ou non.
      1. S'il doit appeler et renvoyer une valeur valide.
      2. S'il doit renvoyer des données comme s'il avait appelé sans appeler réellement.
      3. S'il doit simplement ignorer.
  4. Gemini exécute et demande des actions telles que l'appel de plusieurs fonctions à la fois ou l'appel de fonctions après avoir examiné les résultats.
  5. Finalement, cela se termine lorsqu'une réponse structurée est obtenue.

Ce flux est généralement cohérent avec le MCP. Cela est également expliqué de manière similaire dans le tutoriel du MCP. Il en va de même pour les outils ollama.

Et heureusement, ces trois outils, ollama tools, MCP et Gemini Function Calling, partagent une structure de schéma à tel point qu'il est possible d'utiliser une seule implémentation MCP pour les trois.

Ah, et il y a un inconvénient partagé par tous. Puisque le modèle est finalement celui qui exécute, si le modèle que vous utilisez est dans un mauvais état, il peut ne pas appeler la fonction, l'appeler de manière étrange, ou mal fonctionner, comme lancer une attaque DOS sur le serveur MCP.

Hôte MCP en Go

mcphost de mark3lab

En Go, il existe mcphost développé par l'organisation mark3lab.

Son utilisation est très simple.

1go install github.com/mark3labs/mcphost@latest

Après l'installation, créez un fichier $HOME/.mcp.json et écrivez ce qui suit :

 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}

Puis exécutez-le avec le modèle ollama comme suit. Bien sûr, avant cela, si nécessaire, téléchargez le modèle avec ollama pull mistral-small.

Bien que claude ou qwen2.5 soient généralement recommandés, je recommande actuellement mistral-small.

1mcphost -m ollama:mistral-small

Cependant, si exécuté de cette manière, il ne peut être utilisé qu'en mode question-réponse dans un environnement CLI. Par conséquent, nous allons modifier le code de ce mcphost pour qu'il puisse fonctionner de manière plus programmable.

Fork de mcphost

Comme nous l'avons déjà vérifié, mcphost inclut la fonctionnalité d'extraction de métadonnées et d'appel de fonctions en utilisant le MCP. Par conséquent, il est nécessaire de gérer la partie qui appelle le LLM, la partie qui gère le serveur MCP et la partie qui gère l'historique des messages.

Le Runner du paquet suivant est ce qui a été extrait de ces parties.

 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}

Nous n'examinerons pas la déclaration interne de cette partie. Cependant, elle est presque littérale.

 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}

Pour les mcpClients et tools qui seront utilisés ici, veuillez consulter ce fichier. Pour le provider, nous utiliserons celui d'ollama, veuillez donc consulter ce fichier.

Le plat principal est la méthode 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}

Le code lui-même est une compilation de certaines parties du fichier correspondant.

Le contenu est approximativement le suivant :

  1. Envoyer le prompt avec la liste des outils pour demander l'exécution ou la génération d'une réponse.
  2. Si une réponse est générée, arrêter la récursion et la retourner.
  3. Si le LLM laisse une requête d'exécution d'outil, l'hôte appelle le MCP Server.
  4. Ajouter la réponse à l'historique et revenir à l'étape 1.

En conclusion

Déjà fini ?

En fait, il n'y a pas grand-chose à dire. Cet article a été rédigé pour vous aider à comprendre le fonctionnement général du MCP Server. J'espère que cet article vous aura apporté une petite aide dans la compréhension du fonctionnement de l'hôte MCP.