Zrozumienie hosta MCP
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.
Ogólny przebieg jest następujący:
- Definiuje się funkcję.
- Definicja funkcji jest przesył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ć działanie, czy nie.
- Czy wywołać i zwrócić uzasadnioną wartość
- Czy zwrócić dane, jakby zostały wywołane, bez faktycznego wywołania
- Czy po prostu zignorować
- 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.
- 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:
- Żądanie wykonania lub wygenerowania odpowiedzi poprzez przesłanie listy narzędzi wraz z promptem.
- Jeśli odpowiedź zostanie wygenerowana, rekursja zatrzymuje się i zwraca wartość.
- Jeśli LLM zostawi żądanie wykonania narzędzia, host wywołuje MCP Server.
- 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.