GoSuda

Spróbujmy nieco zrozumieć MCP host

By snowmerak
views ...

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.

FunctionCalling

Ogólny przepływ jest następujący.

  1. Definiowanie funkcji.
  2. Wysyłanie definicji funkcji 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 tego wymaga, wywołujący otrzymuje nazwę i parametry do wywołania funkcji.
    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, jak gdyby wywołanie nastąpiło, bez faktycznego wywoływania
      3. Czy po prostu zignorować
  4. 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.
  5. 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.

  1. Wysyła się prompt wraz z listą narzędzi, żądając wykonania lub wygenerowania odpowiedzi.
  2. Jeśli odpowiedź zostanie wygenerowana, rekurencja zostaje zatrzymana i zwracana jest wartość.
  3. Jeśli LLM pozostawi żądanie wykonania narzędzia, host wywołuje MCP Server.
  4. 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.