Spróbujmy nieco zrozumieć MCP host
Czym jest MCP
MCP jest protokołem opracowanym przez Anthropic dla claude. MCP jest skrótem od Model Context Protocol i jest protokołem, który umożliwia LLM aktywne żądanie działań lub zasobów od zewnętrznych systemów. Ponieważ MCP jest dosłownie tylko protokołem żądania i odpowiedzi, proces i wykonanie muszą być zrealizowane przez dewelopera.
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że nasunąć się pytanie, dlaczego w ogóle wprowadziliśmy Function Calling. Powodem wprowadzenia jest fakt, że Function Calling pojawił się wcześniej niż MCP, a jego kompatybilność dzięki wykorzystaniu tej samej schemy OpenAPI sugeruje podobieństwo w działaniu. W związku z tym, ponieważ opis Gemini Function Calling jest stosunkowo bardziej szczegółowy, uznaliśmy, że będzie pomocny, dlatego go przedstawiliśmy.
Ogólny przepływ jest następujący.
- Definiowanie funkcji.
- Wysyłanie definicji funkcji 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 tego wymaga, wywołujący otrzymuje nazwę i parametry do wywołania funkcji.
- Wywołujący może zdecydować, czy wykonać, czy nie.
- Czy wywołać i zwrócić prawidłową wartość
- Czy zwrócić dane, jak gdyby wywołanie nastąpiło, bez faktycznego wywoływania
- Czy po prostu zignorować
- W powyższym procesie Gemini wykonuje i żąda działań takich jak wywoływanie wielu funkcji jednocześnie lub ponowne wywoływanie funkcji po przejrzeniu wyników wywołania.
- Ostatecznie, gdy pojawi się uporządkowana odpowiedź, proces się kończy.
Ten przepływ jest generalnie zgodny z MCP. Podobnie opisano to również w tutorialu MCP. Podobnie jest w przypadku ollama tools.
Co naprawdę szczęśliwe, te 3 narzędzia: ollama tools, MCP i Gemini Function Calling, mają na tyle wspólną strukturę schemy, że implementując tylko jedno MCP, można go używać we wszystkich 3 miejscach.
Ach, i jest wada, którą wszystkie dzielą. Ponieważ ostatecznie to model wykonuje operacje, jeśli używany przez Państwa model jest w złym stanie, może wystąpić nieprawidłowe działanie, takie jak niewywoływanie funkcji, wywoływanie ich w dziwny sposób lub przeprowadzanie ataków DOS na serwer MCP.
Host MCP w Go
mcphost 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 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}
A następnie uruchomić go z modelem ollama w następujący sposób.
Oczywiście wcześniej, jeśli to konieczne, należy pobrać model za pomocą ollama pull mistral-small
.
Zazwyczaj zaleca się claude lub qwen2.5, ale ja osobiście w tej chwili polecam mistral-small.
1mcphost -m ollama:mistral-small
Jednak przy takim uruchomieniu można go używać tylko w trybie pytanie-odpowiedź w środowisku CLI.
Dlatego zmodyfikujemy kod tego mcphost
, aby działał w sposób bardziej programowalny.
Fork mcphost
Jak już potwierdzono, mcphost
zawiera funkcje wykorzystujące MCP do ekstrakcji metadanych i wywoływania funkcji. W związku z tym potrzebne są części do wywoływania llm, obsługi serwera mcp i zarządzania historią wiadomości.
Część odpowiadająca za to została przeniesiona do Runner
w następującym pakiecie.
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 oglądać wewnętrznych deklaracji tej części oddzielnie. Jednak nazwy są praktycznie zgodne z ich przeznaczeniem.
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}
Odnośnie mcpClients
i tools
używanych tutaj, proszę sprawdzić ten plik.provider
będzie używał tego od ollama, więc 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 "Error calling tool %s: %v",
102 toolName,
103 err,
104 )
105 log.Printf("Error calling tool %s: %v", toolName, err)
106
107 toolResults = append(toolResults, history.ContentBlock{
108 Type: "tool_result",
109 ToolUseID: toolCall.GetID(),
110 Content: []history.ContentBlock{{
111 Type: "text",
112 Text: errMsg,
113 }},
114 })
115
116 continue
117 }
118
119 toolResult := *toolResultPtr
120
121 if toolResult.Content != nil {
122 resultBlock := history.ContentBlock{
123 Type: "tool_result",
124 ToolUseID: toolCall.GetID(),
125 Content: toolResult.Content,
126 }
127
128 var resultText string
129 for _, item := range toolResult.Content {
130 if contentMap, ok := item.(map[string]interface{}); ok {
131 if text, ok := contentMap["text"]; ok {
132 resultText += fmt.Sprintf("%v ", text)
133 }
134 }
135 }
136
137 resultBlock.Text = strings.TrimSpace(resultText)
138
139 toolResults = append(toolResults, resultBlock)
140 }
141 }
142
143 r.messages = append(r.messages, history.HistoryMessage{
144 Role: message.GetRole(),
145 Content: messageContent,
146 })
147
148 if len(toolResults) > 0 {
149 r.messages = append(r.messages, history.HistoryMessage{
150 Role: "user",
151 Content: toolResults,
152 })
153
154 return r.Run(ctx, "")
155 }
156
157 return message.GetContent(), nil
158}
Sam kod został skompilowany z części kodu z tego pliku.
Treść jest w przybliżeniu następująca.
- Wysyła się prompt wraz z listą narzędzi, żądając wykonania lub wygenerowania odpowiedzi.
- Jeśli odpowiedź zostanie wygenerowana, rekurencja zostaje zatrzymana i zwracana jest wartość.
- Jeśli LLM pozostawi żądanie wykonania narzędzia, host wywołuje MCP Server.
- Odpowiedź jest dodawana do historii i wraca się do punktu 1.
Na koniec
Już koniec?
Szczerze mówiąc, nie ma zbyt wiele do powiedzenia. Jest to tekst napisany w celu przybliżenia Państwu zrozumienia, w jaki sposób działa MCP Server. Mam nadzieję, że ten tekst choć trochę pomógł Państwu w zrozumieniu działania hosta MCP.