GoSuda

Kort forståelse af MCP host

By snowmerak
views ...

Hvad er MCP

MCP er en Protocol, der er udviklet af Anthropic til claude. MCP er en forkortelse for Model Context Protocol, og det er en Protocol, der muliggør for en LLM at anmode om handlinger eller ressourcer eksternt på en aktiv måde. Da MCP bogstaveligt talt blot er en Protocol, der giver anmodninger og svar, skal selve processen og udførelsen varetages af udvikleren.

Om den interne funktionalitet

Før vi forklarer den interne funktionalitet, vil vi kort berøre Gemini Function Calling. Gemini Function Calling giver ligesom MCP en LLM mulighed for proaktivt at kalde eksterne handlinger. Man kunne spørge sig selv, hvorfor Function Calling overhovedet er relevant. Vi har inkluderet det, fordi Function Calling kom før MCP, og da begge bruger OpenAPI skemaer, er de kompatible, og vi antager, at deres indbyrdes funktionalitet er ensartet. Da forklaringen af Gemini Function Calling er forholdsvis mere detaljeret, har vi inkluderet det, da det kan være nyttigt.

FunctionCalling

Den overordnede proces er som følger:

  1. Funktioner defineres.
  2. Funktionsdefinitionen sendes til Gemini sammen 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. Hvis Gemini har brug for det, anmoder den om et funktionskald.
    1. Hvis Gemini har brug for det, modtager den kaldende part navnet og parametrene for funktionskaldet.
    2. Den kaldende part kan beslutte, om handlingen skal udføres eller ej.
      1. Om man skal kalde og returnere en gyldig værdi.
      2. Om man skal returnere data, som om man havde kaldt, uden at kalde.
      3. Om man blot skal ignorere det.
  4. I løbet af denne proces udfører og anmoder Gemini om handlinger såsom at kalde flere funktioner på én gang eller kalde en funktion igen efter at have set resultatet.
  5. Processen afsluttes, når et velstruktureret svar er opnået.

Denne proces er generelt i overensstemmelse med MCP. Dette beskrives også på lignende vis i MCP's tutorial. Dette gælder også for ollama tools.

Og heldigvis deler disse tre værktøjer – ollama tools, MCP og Gemini Function Calling – en så lignende skemastruktur, at man ved at implementere MCP alene kan bruge det i alle tre miljøer.

Åh, og der er en ulempe, som de alle deler. Da det i sidste ende er modellen, der udfører handlingen, kan den, hvis den model, du bruger, er i dårlig stand, undlade at kalde funktionen, kalde den forkert eller udføre fejlfunktioner som at sende DOS-angreb til MCP-serveren.

MCP Host i Go

mark3lab's mcphost

I Go findes mcphost, som er under udvikling af organisationen mark3lab.

Brugen er meget enkel.

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

Efter installationen oprettes filen $HOME/.mcp.json, og følgende indhold skrives:

 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}

Derefter køres det med en ollama model som følger. Selvfølgelig skal modellen hentes med ollama pull mistral-small, hvis det er nødvendigt.

Selvom claude eller qwen2.5 generelt anbefales, anbefaler jeg i øjeblikket mistral-small.

1mcphost -m ollama:mistral-small

Men hvis det køres på denne måde, kan det kun bruges i et CLI-miljø i et spørgsmål-svar-format. Derfor vil vi ændre koden i denne mcphost for at gøre den mere programmerbar.

mcphost Fork

Som allerede bekræftet, inkluderer mcphost funktionalitet til at udtrække metadata og kalde funktioner ved hjælp af MCP. Derfor er der brug for dele til at kalde LLM'en, håndtere MCP-serveren og administrere beskedhistorikken.

De relevante dele er bragt ind i Runner i følgende pakke:

 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}

Vi vil ikke se nærmere på den interne deklaration af disse dele. Men de er stort set som navnene antyder.

 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}

Angående de mcpClients og tools, der skal bruges her, henvises til denne fil. Da vi vil bruge ollama's provider, henvises til denne fil.

Hovedretten er Run metoden.

  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}

Selve koden er sammensat af dele af koden fra denne fil.

Indholdet er i store træk som følger:

  1. Send listen over værktøjer sammen med prompten for at anmode om udførelse eller generering af et svar.
  2. Hvis et svar genereres, stoppes rekursionen, og svaret returneres.
  3. Hvis LLM'en efterlader en anmodning om værktøjsudførelse, kalder hosten MCP Serveren.
  4. Svaret tilføjes til historikken, og processen vender tilbage til trin 1.

Afslutning

Allerede slut?

Faktisk er der ikke så meget at sige. Denne artikel er skrevet for at give en forståelse af, hvordan MCP Serveren fungerer i store træk. Jeg håber, at denne artikel har hjulpet jer, omend blot en smule, med at forstå funktionaliteten af en MCP host.