Zrozumienie hosta MCP
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.

Ogólny schemat jest następujący:
- Definiuje się funkcję.
- Definicja funkcji jest wysyłana do Gemini wraz z promptem.
- „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.”
- Gemini żąda wywołania funkcji, jeśli jest to konieczne.
- Jeśli Gemini uzna to za konieczne, nazwa i parametry do wywołania funkcji są przekazywane do wywołującego.
- Wywołujący może zdecydować, czy wykonać, czy nie.
- Czy wywołać i zwrócić prawidłową wartość.
- Czy zwrócić dane tak, jakby zostały wywołane, bez faktycznego wywoływania.
- Czy po prostu zignorować.
- 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.
- 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:
- Wysyła listę narzędzi wraz z promptem, żądając wykonania lub generowania odpowiedzi.
- Jeśli odpowiedź zostanie wygenerowana, rekurencja zostaje zatrzymana i zwracana jest odpowiedź.
- Jeśli LLM pozostawi żądanie wykonania narzędzia, host wywołuje MCP Server.
- 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.