GoSuda

En indledende forståelse af MCP host

By snowmerak
views ...

Hvad er MCP?

MCP er en protokol udviklet af Anthropic til claude. MCP er en forkortelse for Model Context Protocol og er en protokol, der gør det muligt for en LLM aktivt at anmode om eksterne handlinger eller ressourcer. Da MCP bogstaveligt talt kun er en protokol, der giver anmodninger og svar, skal udvikleren udføre processen og eksekveringen.

Om den interne funktion

Før vi forklarer den interne funktion, vil vi kort berøre Gemini Function Calling. Gemini Function Calling gør, ligesom MCP, det muligt for en LLM proaktivt at kalde eksterne handlinger. Du undrer dig måske over, hvorfor vi har inkluderet Function Calling. Grunden til, at vi har inkluderet det, er, at Function Calling kom før MCP, og da det bruger det samme OpenAPI-skema, er det kompatibelt, og vi antog, at de gensidige handlinger ville være ens. Derfor troede vi, at Gemini Function Callings mere detaljerede forklaring ville være nyttig, og vi har derfor inkluderet den.

FunctionCalling

Det overordnede forløb er som følger:

  1. Definer funktionen.
  2. Send funktionsdefinitionen sammen med prompten til Gemini.
    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 anmoder om et funktionskald, hvis det er nødvendigt.
    1. Hvis Gemini har brug for det, modtager kalderen navnet og parametrene for funktionskaldet.
    2. Kalderen kan beslutte, om den vil udføre det eller ej.
      1. Om den skal kaldes og returnere en gyldig værdi
      2. Om den skal returnere data, som om den var kaldt, uden at kalde den
      3. Om den blot skal ignoreres
  4. I ovenstående proces udfører og anmoder Gemini om handlinger som at kalde flere funktioner på én gang eller kalde en funktion igen efter at have set resultatet.
  5. Når et organiseret svar er modtaget, afsluttes processen.

Dette forløb er generelt i overensstemmelse med MCP. Dette er også beskrevet 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, stort set den samme skemastruktur, så man kan bruge dem alle tre ved kun at implementere MCP.

Åh, og der er en fælles ulempe. Da modellen i sidste ende udfører det, kan jeres model, hvis den er i dårlig stand, undlade at kalde en funktion, kalde den forkert eller udføre fejlfunktioner som at sende en DOS til MCP-serveren.

MCP Host i Go

mark3lab's mcphost

I Go er der mcphost, som udvikles af organisationen mark3lab.

Brugen er meget enkel.

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

Efter installationen skal du oprette filen $HOME/.mcp.json og skrive følgende:

 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}

Og kør det derefter med en ollama-model som følger. Før det skal du selvfølgelig hente modellen med ollama pull mistral-small hvis nødvendigt.

Selvom claude eller qwen2.5 anbefales som standard, anbefaler jeg mistral-small på nuværende tidspunkt.

1mcphost -m ollama:mistral-small

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

mcphost Fork

Som allerede bekræftet, indeholder mcphost funktioner til at udtrække metadata og kalde funktioner ved hjælp af MCP. Derfor er der behov for dele, der kalder LLM, håndterer MCP-serveren og administrerer meddelelseshistorikken.

Runner fra følgende pakke er den del, der er hentet:

 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 gennemgå den interne deklaration af denne del separat. Det er dog næsten bogstaveligt talt navnet.

 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}

For mcpClients og tools, der skal bruges her, se venligst denne fil. For provider, som vil bruge ollamas, se venligst 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 fra denne fil.

Indholdet er cirka som følger:

  1. Sender prompten sammen med en liste over værktøjer for at anmode om udførelse eller generering af et svar.
  2. Hvis et svar genereres, stopper rekursionen og returnerer.
  3. Hvis LLM anmoder om værktøjseksekvering, kalder hosten MCP Server.
  4. Svaret tilføjes til historikken, og processen gentages fra trin 1.

Afslutning

Allerede slut?

Faktisk er der ikke så meget at sige. Dette er en artikel skrevet for at hjælpe dig med at forstå, hvordan MCP Server fungerer. Jeg håber, at denne artikel har hjulpet jer, omend blot en smule, med at forstå funktionen af en MCP host.