Comprendre un peu l'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. 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.

Le flux global est le suivant :
- Définir la fonction.
- Envoyer la définition de la fonction à Gemini avec le prompt.
- "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."
- Gemini demande l'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.
- S'il doit appeler et renvoyer une valeur valide.
- S'il doit renvoyer des données comme s'il avait appelé sans appeler réellement.
- S'il doit simplement ignorer.
- 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, 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 :
- Envoyer le prompt avec 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à 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.