GoSuda

Pochopenie hostiteľa MCP

By snowmerak views ...

Čo je MCP

MCP je protokol, ktorý vyvinula spoločnosť Anthropic pre claude. MCP je skratka pre Model Context Protocol a je to protokol, ktorý umožňuje LLM aktívne žiadať外部 o akcie alebo zdroje. MCP je doslova len protokol na odosielanie žiadostí a prijímanie odpovedí, a preto musí samotný proces a vykonávanie zabezpečiť vývojár.

O vnútornej činnosti

Skôr než vysvetlíme vnútornú činnosť, sa krátko zastavme pri Gemini Function Calling. Gemini Function Calling rovnako ako MCP umožňuje LLM iniciatívne volať externé akcie. Možno sa čitateľ spýta, prečo sme Function Calling vôbec spomenuli. Dôvod je ten, že Function Calling sa objavil skôr ako MCP a keďže oba využívajú schému OpenAPI, sú vzájomne kompatibilné, a preto sme predpokladali, že ich vzájomné správanie bude podobné. Keďže opis Gemini Function Calling je relatívne podrobnejší, predpokladáme, že bude užitočný, a preto sme ho uviedli.

FunctionCalling

Celkový tok je nasledovný.

  1. Definujte funkciu.
  2. Spolu s promptom odošlite definíciu funkcie do Gemini.
    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. Ak Gemini považuje za potrebné, požiada o volanie funkcie.
    1. Ak Gemini považuje za potrebné volanie funkcie, volajúci dostane názov a parametre na volanie.
    2. Volajúci môže rozhodnúť, či sa má vykonať:
      1. zavolať funkciu a vrátiť správnu hodnotu,
      2. nevolať funkciu, ale vrátiť údaje, akoby bola zavolaná,
      3. alebo ju jednoducho ignorovať.
  4. Gemini v rámci uvedeného procesu môže naraz požadovať volanie viacerých funkcií alebo po vyhodnotení výsledku funkcie požadovať ďalšie volania.
  5. Keď je výsledkom upravená odpoveď, proces sa ukončí.

Tento tok je vo všeobecnosti v súlade s MCP. Podobne ho opisuje tutoriál MCP. Podobne funguje aj ollama tools.

A to je naozaj šťastie, že tieto tri nástroje, ollama tools, MCP a Gemini Function Calling, zdieľajú takmer rovnakú štruktúru schémy, takže implementáciou iba MCP ju možno použiť na všetky tri miesta.

Ešte jedna nevýhoda je spoločná pre všetky. Keďže volanie vykonáva model, ak je váš model v zlom stave, môže funkcie nevolať, volať ich nesprávne alebo poslať MCP serveru DOS útok a podobne, čiže správať sa nesprávne.

MCP hostiteľ v Go

mcphost od mark3lab

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 zapíšte doňho 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}

Potom spustite s modelom ollama nasledovne.
Samozrejme, ak je to potrebné, predtým stiahnite model príkazom ollama pull mistral-small.

Všeobecne odporúčame claude alebo qwen2.5, no momentálne odporúčam mistral-small.

1mcphost -m ollama:mistral-small

Ak takto spustíte program, dá sa používať iba formou otázok a odpovedí v prostredí CLI.
Preto teraz upravíme kód mcphost tak, aby sa dal ovládať programovo.

Fork mcphost

Ako sme už videli, mcphost obsahuje funkciu, ktorá využíva MCP na extrakciu metadát a volanie funkcií. Preto sú potrebné časti, ktoré volajú LLM, pracujú so serverom MCP a spravujú históriu správ.

Časť, ktorá obsahuje tieto funkcie, je Runner z nasledujúceho balíka.

 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}

Vnútorné deklarácie tejto časti si podrobne neprezeráme. Takmer všetky však zodpovedajú svojim názvom.

 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}

Ohľadom mcpClients a tools, ktoré sa tu používajú, si prosím pozrite príslušný súbor.
Pre provider použijeme ten od ollama, preto si prosím pozrite príslušný súbor.

Hlavným jedlom je metóda 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}

Samotný kód je zostavený z časti príslušného súboru.

Obsah je približne nasledovný.

  1. Spolu s promptom odošlite zoznam nástrojov a požiadajte o rozhodnutie, či sa majú vykonať, alebo o vygenerovanie odpovede.
  2. Keď sa odpoveď vygeneruje, ukončite rekurziu a vráťte ju.
  3. Ak LLM zanechá požiadavku na vykonanie nástroja, hostiteľ zavolá MCP Server.
  4. Pridajte odpoveď do histórie a vráťte sa na bod 1.

Záver

Už koniec?

Vlastne toho veľa povedať nemám. Tento text vznikol predovšetkým preto, aby pomohol pochopiť, ako funguje MCP Server. Dúfam, že vám aspoň trochu pomohol pochopiť činnosť MCP hostiteľa.