GoSuda

Zrozumienie hosta MCP

By snowmerak
views ...

Czym jest MCP?

MCP to protokół opracowany przez Anthropic dla claude. MCP jest skrótem od Model Context Protocol i jest protokołem, który umożliwia LLM aktywną prośbę o zewnętrzne działania lub zasoby. Ponieważ MCP jest dosłownie tylko protokołem żądania i odpowiedzi, proces i wykonanie muszą być realizowane przez dewelopera.

Na temat wewnętrznego działania

Zanim wyjaśnię wewnętrzne działanie, krótko omówię Gemini Function Calling. Gemini Function Calling, podobnie jak MCP, umożliwia LLM samodzielne wywoływanie zewnętrznych działań. Można by się zastanawiać, dlaczego w ogóle poruszam temat Function Calling. Powodem jest to, że Function Calling pojawiło się wcześniej niż MCP i jest z nim kompatybilne, ponieważ oba wykorzystują schemat OpenAPI, co sugeruje, że ich wzajemne działanie będzie podobne. Dlatego też, ponieważ opis Gemini Function Calling jest bardziej szczegółowy, uznałem, że będzie pomocny.

FunctionCalling

Ogólny schemat jest następujący:

  1. Definiuje się funkcję.
  2. Definicja funkcji jest wysyłana do Gemini wraz z promptem.
    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. Gemini żąda wywołania funkcji, jeśli jest to konieczne.
    1. Jeśli Gemini uzna to za konieczne, nazwa i parametry do wywołania funkcji są przekazywane do wywołującego.
    2. Wywołujący może zdecydować, czy wykonać, czy nie.
      1. Czy wywołać i zwrócić prawidłową wartość.
      2. Czy zwrócić dane tak, jakby zostały wywołane, bez faktycznego wywoływania.
      3. Czy po prostu zignorować.
  4. Gemini wykonuje i żąda, aby w tym procesie wywołać wiele funkcji jednocześnie, lub wywołać funkcję, a następnie wywołać ją ponownie po przejrzeniu wyników.
  5. Ostatecznie, gdy zostanie uzyskana uporządkowana odpowiedź, proces zostaje zakończony.

Ten schemat jest ogólnie zgodny z MCP. Podobnie jest to wyjaśnione w tutorialu MCP. Podobnie jest w przypadku narzędzi ollama.

I na szczęście, te trzy narzędzia – ollama tools, MCP i Gemini Function Calling – mają tak wspólną strukturę schematu, że implementując tylko MCP, można ich używać we wszystkich trzech miejscach.

Ach, i mają wspólną wadę. Ponieważ to model wykonuje działanie, jeśli model, którego używasz, jest w złym stanie, może nie wywołać funkcji, wywołać ją w dziwny sposób, lub wykonać inne nieprawidłowe operacje, takie jak atak DOS na serwer MCP.

Host MCP w Go

mcphost firmy mark3lab

W Go istnieje mcphost, który jest rozwijany przez organizację mark3lab.

Sposób użycia jest bardzo prosty.

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

Po instalacji utwórz plik $HOME/.mcp.json i napisz w nim następująco:

 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}

Następnie uruchom go z modelem ollama w następujący sposób. Oczywiście, wcześniej, jeśli to konieczne, pobierz model za pomocą ollama pull mistral-small.

Zasadniczo zalecane są claude lub qwen2.5, ale ja obecnie polecam mistral-small.

1mcphost -m ollama:mistral-small

Jednakże, uruchamiając w ten sposób, można go używać tylko w trybie pytanie-odpowiedź w środowisku CLI. Dlatego zmodyfikujemy kod mcphost, aby działał bardziej programistycznie.

Fork mcphost

Jak już potwierdzono, mcphost zawiera funkcje do ekstrakcji metadanych i wywoływania funkcji za pomocą MCP. W związku z tym potrzebne są części do wywoływania LLM, obsługi serwera MCP i zarządzania historią wiadomości.

Runner z poniższego pakietu zawiera te części.

 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}

Wewnętrzne deklaracje tej części nie będą tutaj omawiane. Jednak ich nazwy są niemal dosłowne.

 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}

Aby uzyskać informacje na temat mcpClients i tools używanych tutaj, proszę zapoznać się z tym plikiem. Ponieważ użyjemy provider od ollama, proszę zapoznać się z tym plikiem.

Głównym daniem jest metoda Run.

  1func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
  2	// Jeśli prompt nie jest pusty, dodaj go do wiadomości.
  3	if len(prompt) != 0 {
  4		r.messages = append(r.messages, history.HistoryMessage{
  5			Role: "user",
  6			Content: []history.ContentBlock{{
  7				Type: "text",
  8				Text: prompt,
  9			}},
 10		})
 11	}
 12
 13	// Konwertuj wiadomości na format LLM.
 14	llmMessages := make([]llm.Message, len(r.messages))
 15	for i := range r.messages {
 16		llmMessages[i] = &r.messages[i]
 17	}
 18
 19	// Zdefiniuj parametry ponownych prób.
 20	const initialBackoff = 1 * time.Second
 21	const maxRetries int = 5
 22	const maxBackoff = 30 * time.Second
 23
 24	var message llm.Message
 25	var err error
 26	backoff := initialBackoff
 27	retries := 0
 28	for {
 29		// Utwórz wiadomość za pomocą dostawcy LLM.
 30		message, err = r.provider.CreateMessage(
 31			context.Background(),
 32			prompt,
 33			llmMessages,
 34			r.tools,
 35		)
 36		if err != nil {
 37			// Obsługa błędu przeciążenia.
 38			if strings.Contains(err.Error(), "overloaded_error") {
 39				if retries >= maxRetries {
 40					return "", fmt.Errorf(
 41						"claude is currently overloaded. please wait a few minutes and try again",
 42					)
 43				}
 44
 45				// Poczekaj i zwiększ czas oczekiwania.
 46				time.Sleep(backoff)
 47				backoff *= 2
 48				if backoff > maxBackoff {
 49					backoff = maxBackoff
 50				}
 51				retries++
 52				continue
 53			}
 54
 55			return "", err
 56		}
 57
 58		break
 59	}
 60
 61	var messageContent []history.ContentBlock
 62
 63	var toolResults []history.ContentBlock
 64	messageContent = []history.ContentBlock{}
 65
 66	// Jeśli wiadomość zawiera treść, dodaj ją.
 67	if message.GetContent() != "" {
 68		messageContent = append(messageContent, history.ContentBlock{
 69			Type: "text",
 70			Text: message.GetContent(),
 71		})
 72	}
 73
 74	// Przetwarzaj wywołania narzędzi.
 75	for _, toolCall := range message.GetToolCalls() {
 76		// Marszaluj argumenty narzędzia do JSON-a.
 77		input, _ := json.Marshal(toolCall.GetArguments())
 78		messageContent = append(messageContent, history.ContentBlock{
 79			Type:  "tool_use",
 80			ID:    toolCall.GetID(),
 81			Name:  toolCall.GetName(),
 82			Input: input,
 83		})
 84
 85		// Podziel nazwę narzędzia na nazwę serwera i nazwę narzędzia.
 86		parts := strings.Split(toolCall.GetName(), "__")
 87
 88		serverName, toolName := parts[0], parts[1]
 89		mcpClient, ok := r.mcpClients[serverName]
 90		if !ok {
 91			continue
 92		}
 93
 94		var toolArgs map[string]interface{}
 95		// Rozpakuj argumenty narzędzia.
 96		if err := json.Unmarshal(input, &toolArgs); err != nil {
 97			continue
 98		}
 99
100		var toolResultPtr *mcp.CallToolResult
101		req := mcp.CallToolRequest{}
102		req.Params.Name = toolName
103		req.Params.Arguments = toolArgs
104		// Wywołaj narzędzie za pomocą klienta MCP.
105		toolResultPtr, err = mcpClient.CallTool(
106			context.Background(),
107			req,
108		)
109
110		if err != nil {
111			// Obsługa błędu wywołania narzędzia.
112			errMsg := fmt.Sprintf(
113				"Error calling tool %s: %v",
114				toolName,
115				err,
116			)
117			log.Printf("Error calling tool %s: %v", toolName, err)
118
119			toolResults = append(toolResults, history.ContentBlock{
120				Type:      "tool_result",
121				ToolUseID: toolCall.GetID(),
122				Content: []history.ContentBlock{{
123					Type: "text",
124					Text: errMsg,
125				}},
126			})
127
128			continue
129		}
130
131		toolResult := *toolResultPtr
132
133		// Jeśli wynik narzędzia zawiera treść, dodaj ją.
134		if toolResult.Content != nil {
135			resultBlock := history.ContentBlock{
136				Type:      "tool_result",
137				ToolUseID: toolCall.GetID(),
138				Content:   toolResult.Content,
139			}
140
141			var resultText string
142			for _, item := range toolResult.Content {
143				if contentMap, ok := item.(map[string]interface{}); ok {
144					if text, ok := contentMap["text"]; ok {
145						resultText += fmt.Sprintf("%v ", text)
146					}
147				}
148			}
149
150			resultBlock.Text = strings.TrimSpace(resultText)
151
152			toolResults = append(toolResults, resultBlock)
153		}
154	}
155
156	// Dodaj wiadomość do historii.
157	r.messages = append(r.messages, history.HistoryMessage{
158		Role:    message.GetRole(),
159		Content: messageContent,
160	})
161
162	// Jeśli są wyniki narzędzi, dodaj je do historii i wywołaj rekurencyjnie.
163	if len(toolResults) > 0 {
164		r.messages = append(r.messages, history.HistoryMessage{
165			Role:    "user",
166			Content: toolResults,
167		})
168
169		return r.Run(ctx, "")
170	}
171
172	// Zwróć treść wiadomości.
173	return message.GetContent(), nil
174}

Sam kod jest zlepkiem części kodu z tego pliku.

Treść jest z grubsza następująca:

  1. Wysyła listę narzędzi wraz z promptem, żądając wykonania lub generowania odpowiedzi.
  2. Jeśli odpowiedź zostanie wygenerowana, rekurencja zostaje zatrzymana i zwracana jest odpowiedź.
  3. Jeśli LLM pozostawi żądanie wykonania narzędzia, host wywołuje MCP Server.
  4. Odpowiedź jest dodawana do historii i proces wraca do punktu 1.

Na koniec

Już koniec?

W zasadzie nie ma zbyt wiele do powiedzenia. Jest to artykuł mający na celu pomóc w zrozumieniu, jak działa MCP Server. Mam nadzieję, że ten artykuł pomógł Państwu w niewielkim stopniu zrozumieć działanie hosta MCP.