GoSuda

Pochopenie hosta MCP

By snowmerak
views ...

Čo je MCP

MCP je protokol vyvinutý spoločnosťou Anthropic pre claude. MCP je skratka pre Model Context Protocol, čo je protokol, ktorý umožňuje LLM aktívne žiadať o externé akcie alebo zdroje. Keďže MCP je doslova len protokol na odosielanie požiadaviek a prijímanie odpovedí, proces a vykonanie musí zabezpečiť vývojár.

O internej prevádzke

Pred vysvetlením internej prevádzky sa budeme venovať Gemini Function Calling. Gemini Function Calling, rovnako ako MCP, umožňuje LLM iniciatívne volať externé akcie. Možno sa pýtate, prečo sme vôbec spomenuli Function Calling. Dôvodom je, že Function Calling sa objavil skôr ako MCP a keďže oba používajú schému OpenAPI, sú kompatibilné a predpokladáme, že ich vzájomná prevádzka bude podobná. Preto sme ho spomenuli, pretože vysvetlenie Gemini Function Calling je podrobnejšie a môže byť užitočné.

FunctionCalling

Celkový tok je nasledovný:

  1. Definuje sa funkcia.
  2. Definícia funkcie sa odošle do Gemini spolu s promptom.
    1. "Odošlite používateľský prompt spolu s deklaráciou(ami) funkcie do modelu. Ten analyzuje požiadavku a určí, či by volanie funkcie bolo užitočné. Ak áno, odpovie štruktúrovaným JSON objektom."
  3. Ak Gemini potrebuje, požiada o volanie funkcie.
    1. Ak Gemini potrebuje, volajúci obdrží názov a parametre pre volanie funkcie.
    2. Volajúci sa môže rozhodnúť, či vykoná volanie alebo nie.
      1. Či zavolať a vrátiť platnú hodnotu.
      2. Či nevolať a vrátiť dáta, akoby bolo volané.
      3. Či to jednoducho ignorovať.
  4. Gemini počas tohto procesu vykoná a požiada o akcie, ako je volanie viacerých funkcií naraz, alebo volanie funkcií po preverení výsledkov.
  5. Nakoniec, keď sa objaví usporiadaná odpoveď, proces sa ukončí.

Tento tok je vo všeobecnosti v súlade s MCP. Podobne je to vysvetlené aj v tutoráli MCP. Podobne je to aj s nástrojmi ollama.

A veľmi šťastne, tieto tri nástroje – ollama tools, MCP a Gemini Function Calling – zdieľajú štruktúru schémy, takže implementáciou len jedného MCP ho možno použiť na všetkých troch miestach.

Ach, a existuje jedna nevýhoda, ktorú zdieľajú všetci. Nakoniec, model to vykonáva, takže ak je model, ktorý používate, v zlom stave, môže zlyhať pri volaní funkcie, volať ju zvláštne, alebo dokonca vykonať útok DOS na server MCP.

MCP host v jazyku Go

mark3lab's mcphost

V Go existuje mcphost, ktorý vyvíja organizácia mark3lab.

Použitie je veľmi jednoduché.

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

Po inštalácii vytvorte súbor $HOME/.mcp.json a napíšte do neho nasledovné:

 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}

A potom spustite s modelom ollama nasledovne. Samozrejme, ak je to potrebné, najprv si stiahnite model pomocou ollama pull mistral-small.

Hoci sa štandardne odporúča claude alebo qwen2.5, ja momentálne odporúčam mistral-small.

1mcphost -m ollama:mistral-small

Ak to však spustíte týmto spôsobom, môžete to použiť iba v prostredí CLI na otázky a odpovede. Preto upravíme kód tohto mcphost, aby fungoval programovateľnejšie.

Fork mcphost

Ako už bolo overené, mcphost obsahuje funkcie na extrakciu metadát a volanie funkcií pomocou MCP. Preto je potrebná časť, ktorá volá LLM, časť, ktorá spracováva server MCP, a časť, ktorá spravuje históriu správ.

Nasledovný balík Runner obsahuje tieto časti:

 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}

Internú deklaráciu príslušnej časti nebudeme samostatne skúmať. Je však takmer doslovná.

 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}

Pre mcpClients a tools, ktoré sa tu použijú, si pozrite tento súbor. Pre provider, ktorý bude používať ollama, si pozrite tento súbor.

Hlavnou časťou je metóda Run.

  1func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
  2	// Ak prompt nie je prázdny
  3	if len(prompt) != 0 {
  4		// Pridajte správu používateľa do histórie
  5		r.messages = append(r.messages, history.HistoryMessage{
  6			Role: "user",
  7			Content: []history.ContentBlock{{
  8				Type: "text",
  9				Text: prompt,
 10			}},
 11		})
 12	}
 13
 14	// Prevedie správy histórie na správy LLM
 15	llmMessages := make([]llm.Message, len(r.messages))
 16	for i := range r.messages {
 17		llmMessages[i] = &r.messages[i]
 18	}
 19
 20	// Konštanty pre exponenciálny backoff
 21	const initialBackoff = 1 * time.Second
 22	const maxRetries int = 5
 23	const maxBackoff = 30 * time.Second
 24
 25	var message llm.Message
 26	var err error
 27	backoff := initialBackoff
 28	retries := 0
 29	// Cyklus pre pokusy o vytvorenie správy
 30	for {
 31		// Vytvorí správu pomocou poskytovateľa LLM
 32		message, err = r.provider.CreateMessage(
 33			context.Background(),
 34			prompt,
 35			llmMessages,
 36			r.tools,
 37		)
 38		// Ak nastala chyba
 39		if err != nil {
 40			// Ak chyba indikuje preťaženie služby Claude
 41			if strings.Contains(err.Error(), "overloaded_error") {
 42				// Ak bol dosiahnutý maximálny počet pokusov
 43				if retries >= maxRetries {
 44					return "", fmt.Errorf(
 45						"claude is currently overloaded. please wait a few minutes and try again",
 46					)
 47				}
 48
 49				// Počká a zvýši backoff
 50				time.Sleep(backoff)
 51				backoff *= 2
 52				if backoff > maxBackoff {
 53					backoff = maxBackoff
 54				}
 55				retries++
 56				continue
 57			}
 58
 59			// Vráti chybu, ak nie je preťaženie
 60			return "", err
 61		}
 62
 63		// Preruší cyklus, ak je správa úspešne vytvorená
 64		break
 65	}
 66
 67	var messageContent []history.ContentBlock
 68
 69	var toolResults []history.ContentBlock
 70	messageContent = []history.ContentBlock{}
 71
 72	// Ak má správa obsah
 73	if message.GetContent() != "" {
 74		// Pridá textový obsah do obsahu správy
 75		messageContent = append(messageContent, history.ContentBlock{
 76			Type: "text",
 77			Text: message.GetContent(),
 78		})
 79	}
 80
 81	// Pre každé volanie nástroja v správe
 82	for _, toolCall := range message.GetToolCalls() {
 83		// Serializuje argumenty nástroja do JSON
 84		input, _ := json.Marshal(toolCall.GetArguments())
 85		// Pridá blok tool_use do obsahu správy
 86		messageContent = append(messageContent, history.ContentBlock{
 87			Type:  "tool_use",
 88			ID:    toolCall.GetID(),
 89			Name:  toolCall.GetName(),
 90			Input: input,
 91		})
 92
 93		// Rozdelí názov nástroja na názov servera a názov nástroja
 94		parts := strings.Split(toolCall.GetName(), "__")
 95
 96		serverName, toolName := parts[0], parts[1]
 97		// Získa MCP klienta pre daný server
 98		mcpClient, ok := r.mcpClients[serverName]
 99		if !ok {
100			continue
101		}
102
103		var toolArgs map[string]interface{}
104		// Deserializuje argumenty nástroja
105		if err := json.Unmarshal(input, &toolArgs); err != nil {
106			continue
107		}
108
109		var toolResultPtr *mcp.CallToolResult
110		req := mcp.CallToolRequest{}
111		req.Params.Name = toolName
112		req.Params.Arguments = toolArgs
113		// Zavolá nástroj pomocou MCP klienta
114		toolResultPtr, err = mcpClient.CallTool(
115			context.Background(),
116			req,
117		)
118
119		// Ak nastala chyba pri volaní nástroja
120		if err != nil {
121			errMsg := fmt.Sprintf(
122				"Error calling tool %s: %v",
123				toolName,
124				err,
125			)
126			log.Printf("Error calling tool %s: %v", toolName, err)
127
128			// Pridá blok s výsledkom chyby nástroja
129			toolResults = append(toolResults, history.ContentBlock{
130				Type:      "tool_result",
131				ToolUseID: toolCall.GetID(),
132				Content: []history.ContentBlock{{
133					Type: "text",
134					Text: errMsg,
135				}},
136			})
137
138			continue
139		}
140
141		toolResult := *toolResultPtr
142
143		// Ak má výsledok nástroja obsah
144		if toolResult.Content != nil {
145			resultBlock := history.ContentBlock{
146				Type:      "tool_result",
147				ToolUseID: toolCall.GetID(),
148				Content:   toolResult.Content,
149			}
150
151			var resultText string
152			// Extrahuje text z obsahu výsledku nástroja
153			for _, item := range toolResult.Content {
154				if contentMap, ok := item.(map[string]interface{}); ok {
155					if text, ok := contentMap["text"]; ok {
156						resultText += fmt.Sprintf("%v ", text)
157					}
158				}
159			}
160
161			resultBlock.Text = strings.TrimSpace(resultText)
162
163			toolResults = append(toolResults, resultBlock)
164		}
165	}
166
167	// Pridá správu s obsahom do histórie
168	r.messages = append(r.messages, history.HistoryMessage{
169		Role:    message.GetRole(),
170		Content: messageContent,
171	})
172
173	// Ak sú k dispozícii výsledky nástrojov
174	if len(toolResults) > 0 {
175		// Pridá výsledky nástrojov ako správu používateľa do histórie
176		r.messages = append(r.messages, history.HistoryMessage{
177			Role:    "user",
178			Content: toolResults,
179		})
180
181		// Rekurzívne zavolá Run pre ďalšie spracovanie
182		return r.Run(ctx, "")
183	}
184
185	// Vráti obsah správy
186	return message.GetContent(), nil
187}

Samotný kód je poskladaný z časti kódu v tomto súbore.

Obsah je zhruba nasledovný:

  1. Odošle prompt a zoznam nástrojov na požiadanie o vykonanie alebo generovanie odpovede.
  2. Ak sa vygeneruje odpoveď, rekurzia sa zastaví a vráti.
  3. Ak LLM požiada o vykonanie nástroja, host zavolá MCP Server.
  4. Odpoveď sa pridá do histórie a vráti sa k bodu 1.

Na záver

Už koniec?

V skutočnosti toho nie je veľa na povedanie. Tento článok bol napísaný s cieľom pomôcť vám pochopiť, ako funguje MCP Server. Dúfam, že vám tento článok aspoň trochu pomohol pochopiť fungovanie MCP hosta.