GoSuda

Forstå MCP-hosten lidt bedre

By snowmerak views ...

Hvad er MCP

MCP er en protokol udviklet af Anthropic til Claude. MCP er en forkortelse af Model Context Protocol, og er en protokol, der gør det muligt for en LLM aktivt at anmode om handlinger eller ressourcer fra det eksterne miljø. Da MCP i bogstavelig forstand blot er en protokol til afsendelse af anmodninger og svar, skal udvikleren selv stå for processen og udførelsen.

Om den interne funktionsmåde

Inden vi forklarer den interne funktionsmåde, vil vi kort gennemgå Gemini Function Calling. Gemini Function Calling gør det ligesom MCP muligt for LLM'en selv at tage initiativ til at kalde eksterne handlinger. Man kan så spørge, hvorfor Function Calling overhovedet er medtaget. Grunden er, at Function Calling kom før MCP, og at det, fordi begge benytter OpenAPI-skemaet, er kompatibelt; vi antog derfor, at deres interne funktionsmåde ville ligne hinanden. Da forklaringen af Gemini Function Calling er relativt detaljeret, vurderede vi, at den ville være nyttig, og derfor er den medtaget.

FunctionCalling

Den overordnede proces er som følger:

  1. Definer en funktion.
  2. Send funktionsdefinitionen til Gemini sammen med en prompt.
    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 til funktionskaldet.
    2. Kalderen kan selv beslutte, om kaldet skal udføres.
      1. Udfør kaldet og returner en gyldig værdi
      2. Udfør ikke kaldet, men returner data som om det var udført
      3. Ignorer det helt
  4. Gemini kan i ovenstående proces anmode om flere funktioner på én gang, eller kalde en funktion, se resultatet og derefter kalde igen.
  5. Processen afsluttes, når der foreligger et velformuleret svar.

Denne proces stemmer generelt overens med MCP. Det beskrives også på lignende vis i MCP-vejledningen. Det samme gælder for ollama tools.

Heldigvis har disse tre værktøjer, ollama tools, MCP og Gemini Function Calling, næsten identisk skemastruktur, så det er muligt at bruge alle tre steder ved kun at implementere MCP.

En fælles ulempe skal også nævnes. Da det i sidste ende er modellen, der udfører kaldene, kan en model i dårlig form undlade at kalde funktionen, kalde den forkert eller udføre fejlagtige handlinger, såsom at sende DoS-anmodninger til MCP-serveren.

MCP-værter i Go

mark3labs mcphost

I Go findes mcphost, som udvikles af organisationen mark3lab.

Brugen er meget enkel.

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

Efter installationen opretter du filen $HOME/.mcp.json og skriver følgende 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}

Derefter kører du det med en ollama-model således:
Hvis det er nødvendigt, kan du dog først hente modellen med ollama pull mistral-small.

Generelt anbefales Claude eller qwen2.5, men i øjeblikket anbefaler jeg mistral-small.

1mcphost -m ollama:mistral-small

Men når du kører det på denne måde, kan du kun anvende det som et spørgsmål-og-svar-forløb i CLI-miljøet.
Derfor vil vi nu ændre koden i mcphost, så den kan fungere mere programmerbart.

Fork af mcphost

Som allerede set indeholder mcphost funktionalitet til at udtrække metadata og kalde funktioner via MCP. Der er derfor brug for en del, der kalder LLM'en, en del der håndterer MCP-serveren, og en del der administrerer beskedhistorikken.

Den del er hentet fra 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}

Den interne deklaration af denne del gennemgår vi ikke nærmere. Den er dog næsten identisk med navnene.

 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 anvendes her, se den relevante fil.
Til provider bruger vi ollamas, så se den relevante fil.

Hovedretten er 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}

Koden er sammensat af dele fra den relevante fil.

Indholdet kan overordnet opsummeres således:

  1. Send prompten sammen med værktøjslisten og anmod om enten eksekvering eller generering af et svar.
  2. Når et svar er genereret, stoppes rekursionen, og svaret returneres.
  3. Hvis LLM'en anmoder om værktøjskørsel, kalder værten MCP-serveren.
  4. Tilføj svaret til historikken og gå tilbage til punkt 1.

Afslutning

Allerede slut?

Der er faktisk ikke så meget mere at sige. Artiklen er skrevet for at give et overblik over, hvordan en MCP-server fungerer. Jeg håber, at den har hjulpet dig, bare lidt, med at forstå, hvordan en MCP-host fungerer.