GoSuda

Att förstå MCP-värden lite grundligare

By snowmerak views ...

Vad är MCP

MCP är ett protokoll som Anthropic utvecklat för Claude. MCP är en förkortning av Model Context Protocol och är ett protokoll som gör det möjligt för LLM:er att aktivt begära åtgärder eller resurser från externa system. Eftersom MCP i bokstavlig mening endast är ett protokoll för att skicka förfrågningar och svar, måste utvecklaren själv hantera processen och exekveringen.

Om den interna funktionen

Innan vi förklarar den interna funktionen vill jag kort behandla Gemini Function Calling. Gemini Function Calling gör, precis som MCP, att LLM:en självständigt kan anropa externa funktioner. Man kan då undra varför Function Calling överhuvudtaget tas upp. Anledningen är att Function Calling lanserades före MCP och att den, eftersom den också använder OpenAPI-schema, är kompatibel med MCP, vilket gör att deras beteende antas vara likartat. Eftersom beskrivningen av Gemini Function Calling är relativt detaljerad, bedömer jag att den kan vara till hjälp och därför tar jag upp den här.

FunctionCalling

Det övergripande flödet ser ut så här.

  1. Funktionen definieras.
  2. Funktionsdefinitionen skickas till Gemini tillsammans med prompten.
    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 begär ett funktionsanrop om det behövs.
    1. Om Gemini behöver ett funktionsanrop tar anroparen emot namnet och parametrarna för anropet.
    2. Anroparen kan besluta om att utföra anropet eller inte.
      1. Anropa funktionen och returnera ett giltigt värde
      2. Anropa inte funktionen utan returnera data som om den hade anropats
      3. Ignorera den helt
  4. Gemini kan inom denna process begära att flera funktioner anropas samtidigt, eller att en funktion anropas och därefter, med hänsyn till resultatet, att ytterligare anrop görs.
  5. När ett välstrukturerat svar erhålls avslutas processen.

Detta flöde överensstämmer i allmänhet med MCP. Det beskrivs på liknande sätt i MCP:s handledning. Det liknar även ollama tools.

Och lyckligtvis är dessa tre verktyg, ollama tools, MCP och Gemini Function Calling, i praktiken baserade på en gemensam schemastruktur, så det är möjligt att implementera endast MCP och använda det i alla tre miljöerna.

Det finns också en nackdel som alla delar. Eftersom det i slutändan är modellen som utför anropen, kan en modell i dålig form underlåta att anropa funktioner, anropa dem felaktigt eller till och med överbelasta MCP-servern med DoS-liknande trafik och därmed bete sig felaktigt.

MCP-värd i Go

mark3lab:s mcphost

I Go finns mcphost, som utvecklas av organisationen mark3lab.

Användningen är mycket enkel.

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

Efter installationen skapar du filen $HOME/.mcp.json och skriver följande i den.

 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}

Kör sedan följande med en ollama-modell.
Om det behövs hämtar du dessförinnan modellen med ollama pull mistral-small.

I allmänhet rekommenderas Claude eller qwen2.5, men för närvarande rekommenderar jag mistral-small.

1mcphost -m ollama:mistral-small

Om du kör på detta sätt kan du dock endast använda det som ett frågesvarssystem i CLI-miljö.
Därför ska vi ändra koden i mcphost så att det kan fungera på ett mer programmerbart sätt.

Fork av mcphost

Som redan konstaterats innehåller mcphost funktioner för att extrahera metadata och anropa funktioner med hjälp av MCP. Därför behövs delar som hanterar anrop till LLM, hanterar MCP-servrar och hanterar meddelandehistoriken.

Den del som motsvarar detta är Runner i följande paket.

 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}

Jag går inte igenom de interna deklarationerna i detta avsnitt. De är dock i stort sett namngivna efter sin funktion.

 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}

För mcpClients och tools som används här, se denna fil.
För provider använder vi ollamas variant, så se denna fil.

Huvudrätten är metoden 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}

Kodens grundstruktur är en sammanslagning av delar från denna fil.

I huvudsak fungerar det så här.

  1. Verktygslistan skickas tillsammans med prompten för att begära antingen ett beslut om exekvering eller ett genererat svar.
  2. När ett svar har genererats avbryts rekursionen och svaret returneras.
  3. Om LLM:en begär exekvering av ett verktyg anropar värden MCP-servern.
  4. Resultatet läggs till i historiken och processen återgår till steg 1.

Avslutningsvis

Redan slut?

Egentligen finns det inte så mycket mer att säga. Texten har skrivits för att ge en översiktlig förståelse för hur en MCP-server fungerar. Jag hoppas att den har bidragit något, om än litet, till att förstå hur en MCP-värd fungerar.