GoSuda

Porozumění hostu MCP

By snowmerak
views ...

Co je MCP

MCP je protokol vyvinutý společností Anthropic pro claude. MCP je zkratka pro Model Context Protocol, který umožňuje LLM aktivně žádat o externí akce nebo zdroje. Jelikož MCP je doslova jen protokol pro žádosti a odpovědi, proces a spuštění musí provést vývojář.

O vnitřním fungování

Než se pustíme do vysvětlení vnitřního fungování, Gemini Function Calling si krátce představíme. Gemini Function Calling, stejně jako MCP, umožňuje LLM proaktivně volat externí akce. Možná se ptáte, proč jsme se vůbec k Function Calling vrátili. Důvodem je, že Function Calling vyšlo dříve než MCP a je kompatibilní, protože oba používají schéma OpenAPI, což naznačuje, že jejich vzájemné fungování bude podobné. Protože je popis Gemini Function Calling podrobnější, předpokládali jsme, že bude užitečné ho zmínit.

FunctionCalling

Celkový průběh je následující:

  1. Definuje se funkce.
  2. Definice funkce se odešle do Gemini spolu s promptem.
    1. "Odešlete uživatelský prompt spolu s deklarací(mi) funkce(í) modelu. Ten analyzuje požadavek a určí, zda by volání funkce bylo užitečné. Pokud ano, odpoví strukturovaným objektem JSON."
  3. Gemini v případě potřeby požádá o volání funkce.
    1. Pokud Gemini potřebuje, volající obdrží jméno a parametry pro volání funkce.
    2. Volající se může rozhodnout, zda provede spuštění, či nikoli.
      1. Zda se provede volání a vrátí se platná hodnota.
      2. Zda se vrátí data, jako by volání proběhlo, aniž by se skutečně volalo.
      3. Zda se volání jednoduše ignoruje.
  4. Gemini v průběhu výše uvedeného procesu provádí a požaduje akce, jako je volání více funkcí najednou nebo volání funkcí opakovaně po zhlédnutí výsledků.
  5. V konečném důsledku se proces ukončí, jakmile je k dispozici uspořádaná odpověď.

Tento průběh je obecně v souladu s MCP. Podobně je to popsáno i v tutoriálu MCP. Podobné je to i s nástroji ollama.

A co je opravdu skvělé, je, že tyto tři nástroje – ollama tools, MCP a Gemini Function Calling – sdílejí strukturu schématu, takže implementace pouze jednoho MCP může být použita na všech třech místech.

A mimochodem, existuje nevýhoda, kterou sdílejí všichni. Protože je spuštění nakonec provedeno modelem, pokud je model, který používáte, v špatném stavu, může se stát, že funkce nebude volána, nebo bude volána podivně, nebo dokonce provede chybnou akci, jako je například útok DOS na MCP server.

MCP host v Go

mark3lab's mcphost

V Go existuje mcphost, který je vyvíjen organizací 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í:

 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 s modelem ollama následovně. Předtím si samozřejmě stáhněte model pomocí ollama pull mistral-small, pokud je to nutné.

I když se obecně doporučuje claude nebo qwen2.5, v současné době doporučuji mistral-small.

1mcphost -m ollama:mistral-small

Pokud se však spustí tímto způsobem, lze jej použít pouze v prostředí CLI ve stylu otázek a odpovědí. Proto upravíme kód tohoto mcphost, abychom jej mohli programověji ovládat.

Fork mcphost

Jak již bylo zjištěno, mcphost zahrnuje funkce pro extrakci metadat a volání funkcí pomocí MCP. Proto je nutná část pro volání LLM, část pro správu MCP serveru a část pro správu historie zpráv.

Následující balíček Runner obsahuje tyto části.

 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í deklarace příslušné části nebudeme podrobně zkoumat. Nicméně je to téměř doslova podle názvu.

 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}

Pro mcpClients a tools použité zde se prosím podívejte na tento soubor. Pro provider (použijeme ten od ollama) se prosím podívejte na tento soubor.

Hlavním chodem 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				"Chyba při volání nástroje %s: %v", // Error calling tool %s: %v
101				toolName,
102				err,
103			)
104			log.Printf("Chyba při volání nástroje %s: %v", toolName, err) // Error calling tool %s: %v
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 poskládán z částí tohoto souboru.

Obsah je zhruba následující:

  1. Odešle se seznam nástrojů spolu s promptem, aby se vyžádalo spuštění nebo generování odpovědi.
  2. Jakmile je odpověď vygenerována, rekurze se zastaví a vrátí se.
  3. Pokud LLM vydá požadavek na spuštění nástroje, hostitel zavolá MCP Server.
  4. Odpověď se přidá do historie a vrátí se k bodu 1.

Na závěr

Už konec?

Upřímně řečeno, není toho moc, co bych mohl říci. Tento článek byl napsán, aby vám pomohl pochopit, jak MCP Server funguje. Doufám, že vám tento článek alespoň trochu pomohl pochopit fungování hostitele MCP.