GoSuda

Pochopení hostitele MCP

By snowmerak views ...

Co je MCP

MCP je protokol, který vyvinula společnost Anthropic pro Claude. MCP je zkratka za Model Context Protocol a umožňuje LLM aktivně žádat externí systém o provedení akce nebo poskytnutí zdroje. MCP je doslova pouze protokol pro zasílání žádostí a odpovědí, a proto musí samotný proces a jeho provedení zajistit vývojář.

O vnitřní funkci

Než vysvětlím vnitřní fungování, rád bych se krátce zastavil u Gemini Function Calling. Gemini Function Calling, stejně jako MCP, umožňuje LLM iniciativně volat externí akce. Možná vás napadne, proč jsem Function Calling vůbec zmínil. Důvodem je, že Function Calling se objevil dříve než MCP a oba používají schéma OpenAPI, takže jsou vzájemně kompatibilní a lze předpokládat, že se jejich chování bude podobat. Dokumentace Gemini Function Calling je navíc podrobnější, a proto se může ukázat jako užitečná.

FunctionCalling

Celkový postup vypadá takto.

  1. Definujte funkci.
  2. Odešlete definici funkce spolu s promptem 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. Pokud Gemini potřebuje, požádá o volání funkce.
    1. Pokud Gemini potřebuje volání funkce, volající obdrží název a parametry pro toto volání.
    2. Volající se může rozhodnout, zda volání provede.
      1. Zda funkci skutečně zavolá a vrátí platnou hodnotu.
      2. Zda funkci nezavolá, ale vrátí data, jako by ji zavolal.
      3. Zda ji jednoduše ignoruje.
  4. Gemini v rámci tohoto procesu může najednou požadovat volání několika funkcí nebo může po zhodnocení výsledku volání další funkce provést a požádat o ně.
  5. Jakmile je výsledkem srozumitelná odpověď, proces končí.

Tento postup se v zásadě shoduje s MCP. Podobný popis najdete také v tutoriálu MCP. Podobně funguje i ollama tools.

A co je opravdu šťastné, tyto tři nástroje, ollama tools, MCP a Gemini Function Calling, sdílejí téměř stejnou strukturu schématu, takže implementací pouze MCP lze využít všechny tři.

Všichni sdílejí i jednu nevýhodu. Volání skutečně provádí model, takže pokud je model ve špatném stavu, může funkce nevolat, volat je nesprávně nebo posílat MCP serveru DoS útok a způsobit další chybné chování.

MCP host v Go

mark3lab's mcphost

V Go existuje mcphost, který vyvíjí organizace mark3lab.

Použití je velmi jednoduché.

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

Po instalaci vytvořte soubor $HOME/.mcp.json a zapište do něj následující obsah.

 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}

Poté spusťte následující příkaz s modelem ollama.
Pokud je to nutné, nejprve stáhněte model příkazem ollama pull mistral-small.

Obecně doporučuji Claude nebo qwen2.5, ale v současné době doporučuji mistral-small.

1mcphost -m ollama:mistral-small

Při tomto spuštění lze však používat pouze formu otázek a odpovědí v CLI prostředí.
Proto upravíme kód mcphost tak, aby se dalo chovat programovatelněji.

Fork mcphost

Jak jsme již viděli, mcphost obsahuje funkci, která pomocí MCP extrahuje metadata a volá funkce. Potřebujeme tedy část pro volání LLM, část pro práci s MCP serverem a část pro správu historie zpráv.

Příslušnou část představuje Runner z následujícího balíčku.

 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}

Vnitřní deklarace této části zde podrobně nerozebírám. Jsou však téměř shodné s jejich názvy.

 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}

Informace o mcpClients a tools, které se zde používají, najdete v příslušném souboru.
Pro provider použijeme verzi pro ollama, takže se podívejte do příslušného souboru.

Hlavním jádrem je metoda 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}

Kód samotný je sestaven z částí příslušného souboru.

Obsah lze shrnout takto.

  1. Spolu s promptem odešlete seznam nástrojů a požádejte o rozhodnutí, zda se mají spustit, nebo o vygenerování odpovědi.
  2. Pokud je odpověď vygenerována, ukončete rekurzi a vraťte ji.
  3. Pokud LLM požádá o spuštění nástroje, host zavolá MCP Server.
  4. Přidejte odpověď do historie a vraťte se ke kroku 1.

Závěrem

Už konec?

Ve skutečnosti toho moc říct nemám. Tento článek vznikl hlavně proto, abych vám pomohl pochopit, jak MCP Server funguje. Doufám, že vám alespoň trochu pomohl porozumět tomu, jak funguje MCP host.