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 handlinger eller ressourcer eksternt. Da MCP bogstaveligt talt blot er en protokol til at give anmodninger og svar, skal udvikleren håndtere processen og udførelsen.

Om intern funktionalitet

Før jeg forklarer den interne funktionalitet, vil jeg kort berøre Gemini Function Calling. Gemini Function Calling gør ligesom MCP det muligt for LLM'en at initiere eksterne handlinger. Man vil måske undre sig over, hvorfor Function Calling specifikt er blevet nævnt. Årsagen til, at den specifikt er nævnt, er dels, at Function Calling kom før MCP, og dels, at de begge bruger OpenAPI schema, hvilket gør dem kompatible, og derfor antog vi, at deres gensidige funktionalitet ville være ens. Derfor, da forklaringen af Gemini Function Calling er relativt mere detaljeret, blev den medtaget, da den syntes at være nyttig.

FunctionCalling

Det overordnede flow er som følger.

  1. En funktion 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. Gemini anmoder om funktionskald, hvis 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 eller ej.
      1. Om den vil kalde og returnere en gyldig værdi
      2. Om den vil returnere data, som om den kaldte, uden at kalde
      3. Om den blot vil ignorere det
  4. I løbet af processen ovenfor udfører og anmoder Gemini om handlinger såsom at kalde flere funktioner på én gang eller kalde igen efter at have set resultatet af et funktionskald.
  5. Processen afsluttes, når der endeligt fremkommer et velordnet svar.

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

Og heldigvis har disse 3 værktøjer, ollama tools, MCP og Gemini Function Calling, en så delt schema structure, at man kan bruge dem alle 3 ved kun at implementere én MCP.

Åh, og der er en ulempe, som de alle deler. Da det i sidste ende er modellen, der udfører koden, hvis den model, du bruger, er i dårlig stand, kan den undlade at kalde funktioner, kalde dem på en mærkelig måde, eller udføre fejlfunktioner såsom at sende en DOS til MCP serveren.

MCP-host i Go

mark3lab's mcphost

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

Brugen er meget simpel.

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

Efter installationen opretter du filen $HOME/.mcp.json og skriver 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 derefter kører du med en ollama model som følger. Selvfølgelig, hvis nødvendigt, henter du modellen med ollama pull mistral-small først.

claude eller qwen2.5 anbefales som standard, men for nuværende anbefaler jeg mistral-small.

1mcphost -m ollama:mistral-small

Dog, hvis den køres på denne måde, kan den kun bruges i en spørgsmål-svar-stil i et CLI environment. Derfor vil vi ændre koden i denne mcphost for at få den til at fungere mere programmerbart.

mcphost fork

Som allerede bekræftet, indeholder mcphost funktionalitet til at udtrække metadata og kalde functions ved hjælp af MCP. Derfor er der behov for dele til at kalde llm'en, håndtere mcp serveren og administrere message history.

Den del, der er hentet, er Runner i følgende package.

 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
18// type Runner struct
19type Runner struct {
20	// provider for llm
21	provider   llm.Provider
22	// mcp clients
23	mcpClients map[string]*mcpclient.StdioMCPClient
24	// tools for llm
25	tools      []llm.Tool
26
27	// message history
28	messages []history.HistoryMessage
29}

Vi vil ikke se separat på den interne deklaration af den pågældende del. Den er dog næsten identisk med 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 bruges her, se venligst denne fil. For provider'en, da vi vil bruge ollama's, se venligst denne fil.

Hovedretten er Run method'en.

  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 lappet sammen fra dele af koden i denne fil.

Indholdet er omtrent som følger.

  1. En liste over tools sendes sammen med prompten for at anmode om udførelse eller response generation.
  2. Hvis et svar genereres, stoppes rekursionen, og svaret returneres.
  3. Hvis LLM'en efterlader en tool execution request, kalder Host'en MCP Serveren.
  4. Svaret tilføjes til history'en, og processen vender tilbage til trin 1.

Afslutning

Allerede slut?

Der er faktisk ikke så meget at sige. Dette er en artikel skrevet for at hjælpe med at forstå, hvordan en MCP Server omtrent fungerer. Jeg håber, at denne artikel har været en lille hjælp til jeres forståelse af en MCP host's funktionalitet.