Comprendre un peu le hôte MCP
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.
Le flux général est le suivant :
- Définir la fonction.
- Envoyer la définition de la fonction à Gemini avec le prompt.
- "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é."
- Gemini demande un appel de fonction si nécessaire.
- Si Gemini en a besoin, l'appelant reçoit le nom et les paramètres pour l'appel de fonction.
- L'appelant peut décider d'exécuter ou non.
- Exécuter et renvoyer une valeur valide.
- Ne pas exécuter, mais renvoyer des données comme si elles avaient été exécutées.
- Simplement ignorer.
- 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.
- 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 :
- Envoyer le prompt et la liste des outils pour demander l'exécution ou la génération d'une réponse.
- Si une réponse est générée, arrêter la récursion et la retourner.
- Si le LLM laisse une requête d'exécution d'outil, l'hôte appelle le MCP Server.
- 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.