GoSuda

Pochopení hosta MCP

By snowmerak
views ...

Co je MCP

MCP je protokol vyvinutý společností Anthropic pro claude. Zkratka MCP znamená Model Context Protocol a jedná se o protokol, který umožňuje LLM aktivně žádat o externí akce nebo zdroje. Protože MCP je doslova jen protokol pro odesílání požadavků a přijímání odpovědí, musí proces a implementaci provést vývojář.

O interním fungování

Než vysvětlíme interní fungování, zmíníme se o Gemini Function Calling. Gemini Function Calling, stejně jako MCP, umožňuje LLM iniciovat volání externích akcí. Možná se ptáte, proč vůbec Function Calling zmiňujeme. Důvodem je, že Function Calling se objevilo dříve než MCP a používá stejné schéma OpenAPI, což zaručuje kompatibilitu, a proto jsme předpokládali, že jejich vzájemné fungování bude podobné. Vzhledem k tomu, že popis Gemini Function Calling je relativně podrobnější, usoudili jsme, že by mohl být užitečný, a proto jsme jej uvedli.

FunctionCalling

Celkový postup je následující:

  1. Definujete funkci.
  2. Odešlete definici funkce spolu s promptem do Gemini.
    1. "Odešlete uživatelský prompt společně s deklaracemi funkcí modelu. Model analyzuje požadavek a určí, zda by bylo užitečné volání funkce. Pokud ano, odpoví strukturovaným objektem JSON."
  3. Gemini požádá o volání funkce, pokud je to nutné.
    1. Pokud Gemini potřebuje volat funkci, volající obdrží její název a parametry.
    2. Volající se může rozhodnout, zda ji provede, či nikoli.
      1. Zda ji provede a vrátí platnou hodnotu.
      2. Zda ji neprovede, ale vrátí data, jako by byla provedena.
      3. Zda ji jednoduše ignoruje.
  4. Gemini během tohoto procesu provádí a požaduje akce, jako je volání více funkcí najednou nebo volání dalších funkcí na základě výsledku předchozího volání.
  5. Proces se ukončí, jakmile je generována uspořádaná odpověď.

Tento postup je obecně v souladu s MCP. Podobně je popsán i v tutoriálu MCP. Podobné je to i u ollama tools.

A co je velmi potěšující, je to, že tato tři nástrojová řešení – ollama tools, MCP a Gemini Function Calling – sdílejí strukturu schématu, takže implementací pouze MCP je možné je použít ve všech třech.

A mimochodem, všechny sdílejí jednu nevýhodu. Protože spuštění provádí model, pokud je model, který používáte, v nestabilním stavu, může dojít k chybným operacím, jako je nevolání funkce, volání funkce nesprávným způsobem nebo provedení DOS útoku na MCP server.

MCP Host v jazyce 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í:

 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 poté spusťte s modelem ollama, jak je uvedeno níže. Samozřejmě, pokud je to nutné, nejprve stáhněte model pomocí ollama pull mistral-small.

I když se obvykle doporučuje claude nebo qwen2.5, momentálně 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 pro dotazy a odpovědi. Proto upravíme kód mcphost, aby mohl fungovat programovějším způsobem.

Fork mcphost

Jak již bylo ověřeno, mcphost obsahuje funkce pro extrakci metadat pomocí MCP a volání funkcí. Proto jsou zapotřebí části pro volání LLM, správu MCP serveru a správu historie zpráv.

Následující Runner z daného balíčku 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 dané části nebudeme podrobně rozebírat. Nicméně se jedná v podstatě o to, co naznačuje název.

 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é zde budou použity, naleznete v tomto souboru. Protože budeme používat provider od ollama, podívejte se na tento soubor.

Hlavní částí 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}

Samotný kód je sestaven z částí tohoto souboru.

Obsah je zhruba následující:

  1. Odešle se prompt a seznam nástrojů s požadavkem na provedení nebo generování odpovědi.
  2. Pokud je generována odpověď, rekurze se zastaví a vrátí se.
  3. Pokud LLM požádá o provedení nástroje, hostitel zavolá MCP Server.
  4. Odpověď se přidá do historie a vrátí se zpět k bodu 1.

Na závěr

Už konec?

Ve skutečnosti toho není moc co říct. Tento text byl napsán s cílem pomoci vám pochopit, jak MCP Server funguje. Doufám, že vám tento text alespoň trochu pomohl porozumět fungování MCP hosta.