GoSuda

Comprendre un peu le 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. Comme le MCP n'est littéralement qu'un protocole de requête et de réponse, 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. Gemini Function Calling permet également aux LLM d'appeler de manière proactive des actions externes, tout comme le MCP. Vous vous demandez peut-être pourquoi nous avons spécifiquement introduit le Function Calling. La raison en est que le Function Calling est apparu avant le MCP, et comme il utilise le même schéma OpenAPI, il est compatible, et nous avons supposé que leurs opérations seraient similaires. Par conséquent, l'explication de Gemini Function Calling étant plus détaillée, elle pourrait être utile.

FunctionCalling

Le flux général est le suivant :

  1. Définir la fonction.
  2. Envoyer la définition de la fonction à Gemini avec le prompt.
    1. "Envoyer le prompt de l'utilisateur ainsi que la ou les déclarations de fonction au modèle. Il analyse la requête et détermine si un appel de fonction serait utile. Si c'est le cas, il répond avec un objet JSON structuré."
  3. Gemini demande un 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. Exécuter et renvoyer une valeur valide.
      2. Ne pas exécuter, mais renvoyer des données comme si elles avaient été exécutées.
      3. Simplement ignorer.
  4. Au cours du processus ci-dessus, 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, si une réponse structurée est obtenue, le processus se termine.

Ce flux est généralement cohérent avec le MCP. Cela est également expliqué de manière similaire dans le tutoriel du MCP. C'est également similaire 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'en implémentant seulement le MCP, il est possible de l'utiliser pour les trois.

Ah, et il y a un inconvénient partagé par tous. Puisque c'est le modèle qui exécute finalement, si le modèle que vous utilisez est en mauvais état, il peut ne pas appeler la fonction, l'appeler étrangement, ou effectuer des opérations incorrectes comme des attaques DOS sur le serveur MCP.

Hôte MCP en Go

mcphost de mark3lab

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

L'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}

Et 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, en l'exécutant 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 cet mcphost pour qu'il puisse fonctionner de manière plus programmable.

Fork de mcphost

Comme déjà confirmé, mcphost inclut des fonctionnalités pour extraire des métadonnées et appeler des fonctions en utilisant le MCP. Par conséquent, les parties pour appeler le LLM, gérer le serveur MCP, et gérer l'historique des messages sont nécessaires.

Le Runner du paquet suivant contient les parties correspondantes :

 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 pratiquement identique à son nom.

 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 mcpClients et tools utilisés ici, veuillez consulter ce fichier. Pour provider, qui utilisera celui d'ollama, veuillez 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 // Délai d'attente initial
 18	const maxRetries int = 5 // Nombre maximal de tentatives
 19	const maxBackoff = 30 * time.Second // Délai d'attente maximal
 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 est actuellement surchargé. veuillez patienter quelques minutes et réessayer",
 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				"Erreur lors de l'appel de l'outil %s: %v",
101				toolName,
102				err,
103			)
104			log.Printf("Erreur lors de l'appel de l'outil %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 a été assemblé à partir de certaines parties du fichier correspondant.

Le contenu est approximativement le suivant :

  1. Envoyer le prompt et 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à la fin ?

En fait, il n'y a pas grand-chose à dire. Cet article a été écrit pour vous aider à comprendre approximativement comment fonctionne le MCP Server. J'espère que cet article vous a été d'une petite aide pour comprendre le fonctionnement de l'hôte MCP.