GoSuda

Zrozumienie hosta MCP

By snowmerak
views ...

Czym jest MCP

MCP to protokół opracowany przez Anthropic dla claude. MCP to skrót od Model Context Protocol, protokołu, który umożliwia LLM aktywną prośbę o zewnętrzne działania lub zasoby. Ponieważ MCP jest dosłownie tylko protokołem żądań i odpowiedzi, deweloper musi zapewnić proces i wykonanie.

O wewnętrznym działaniu

Zanim wyjaśnimy wewnętrzne działanie, omówimy Gemini Function Calling. Gemini Function Calling, podobnie jak MCP, umożliwia LLM proaktywne wywoływanie zewnętrznych działań. Można by się zastanawiać, dlaczego w ogóle przywołano Function Calling. Powodem jest to, że Function Calling pojawiło się wcześniej niż MCP, i ponieważ wykorzystuje ten sam schemat OpenAPI, są kompatybilne, co sugeruje, że ich wzajemne działanie będzie podobne. Z tego powodu, ponieważ opis Gemini Function Calling jest bardziej szczegółowy, uznano go za pomocny.

FunctionCalling

Ogólny przebieg jest następujący:

  1. Definiuje się funkcję.
  2. Definicja funkcji jest przesył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ć działanie, czy nie.
      1. Czy wywołać i zwrócić uzasadnioną wartość
      2. Czy zwrócić dane, jakby zostały wywołane, bez faktycznego wywołania
      3. Czy po prostu zignorować
  4. Gemini wykonuje i żąda działań, takich jak wywołanie wielu funkcji jednocześnie w trakcie tego procesu, lub wywołanie ich ponownie po otrzymaniu wyników wywołania funkcji.
  5. Proces kończy się, gdy zostanie uzyskana uporządkowana odpowiedź.

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

I co najważniejsze, te trzy narzędzia – ollama tools, MCP i Gemini Function Calling – mają tak bardzo wspólne struktury schematów, że można zaimplementować MCP tylko raz i używać go we wszystkich trzech miejscach.

Aha, i jest jedna wada wspólna dla wszystkich. Ponieważ to model ostatecznie wykonuje operacje, jeśli używany model jest w złym stanie, może wystąpić nieprawidłowe działanie, takie jak niewywoływanie funkcji, wywoływanie jej w dziwny sposób lub wysyłanie ataku DOS na serwer MCP.

Host MCP w Go

mcphost od mark3lab

W języku Go istnieje mcphost rozwijany przez organizację mark3lab.

Sposób użycia jest bardzo prosty.

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

Po instalacji, należy utworzyć plik $HOME/.mcp.json i wpisać w nim następującą treść:

 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 uruchamia się go z modelem ollama w następujący sposób. Oczywiście, jeśli jest to konieczne, najpierw pobiera się model za pomocą ollama pull mistral-small.

Zasadniczo poleca się claude lub qwen2.5, ale ja obecnie polecam mistral-small.

1mcphost -m ollama:mistral-small

Jednak uruchomienie w ten sposób pozwala na użycie tylko w trybie pytań i odpowiedzi w środowisku CLI. Dlatego zmodyfikujemy kod mcphost, aby działał w bardziej programowalny sposób.

Fork mcphost

Jak już zauważono, mcphost zawiera funkcje do ekstrakcji metadanych i wywoływania funkcji przy użyciu MCP. Wymaga to zatem części do wywoływania LLM, zarządzania serwerem MCP i zarządzania historią wiadomości.

Runner z poniższego pakietu to ta część, którą pobraliśmy:

 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}

Nie będziemy szczegółowo omawiać wewnętrznej deklaracji tej części. Niemniej jednak, nazwy są dość 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}

W odniesieniu do używanych tutaj mcpClients i tools, proszę sprawdzić ten plik. Jeśli chodzi o provider, ponieważ użyjemy tego z ollama, proszę sprawdzić ten plik.

Głównym daniem jest 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}

Sam kod jest zlepkiem fragmentów kodu z tego pliku.

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

  1. Żądanie wykonania lub wygenerowania odpowiedzi poprzez przesłanie listy narzędzi wraz z promptem.
  2. Jeśli odpowiedź zostanie wygenerowana, rekursja zatrzymuje się i zwraca wartość.
  3. Jeśli LLM zostawi żądanie wykonania narzędzia, host wywołuje MCP Server.
  4. Odpowiedź jest dodawana do historii i wraca do punktu 1.

Podsumowanie

Już koniec?

W zasadzie nie ma zbyt wiele do powiedzenia. Ten artykuł został napisany, aby pomóc zrozumieć, jak działa MCP Server. Mam nadzieję, że ten artykuł choć trochę pomógł Wam zrozumieć działanie hosta MCP.