GoSuda

MCP host enigszins begrijpen

By snowmerak
views ...

Wat is MCP?

MCP is een protocol dat door Anthropic is ontwikkeld voor Claude. MCP is een afkorting van Model Context Protocol en is een protocol dat LLM's in staat stelt proactief externe acties of bronnen aan te vragen. Aangezien MCP letterlijk slechts een protocol is dat verzoeken en antwoorden verstrekt, moeten het proces en de uitvoering door de ontwikkelaar worden afgehandeld.

Over de interne werking

Voordat ik de interne werking toelicht, zal ik eerst Gemini Function Calling bespreken. Gemini Function Calling stelt, net als MCP, LLM's in staat proactief externe acties aan te roepen. U vraagt zich misschien af waarom ik Function Calling erbij heb gehaald. De reden is dat Function Calling eerder bestond dan MCP, en aangezien beide dezelfde OpenAPI-schema's gebruiken, zijn ze compatibel, en vermoedde ik dat hun onderlinge werking vergelijkbaar zou zijn. Daarom heb ik Gemini Function Calling erbij gehaald, omdat de uitleg ervan gedetailleerder is en nuttig kan zijn.

FunctionCalling

De algemene stroom is als volgt:

  1. Definieer de functie.
  2. Stuur de functiedefinitie samen met de prompt naar Gemini.
    1. "Stuur de gebruikersprompt samen met de functiedeclaratie(s) naar het model. Het analyseert het verzoek en bepaalt of een functieaanroep nuttig zou zijn. Zo ja, dan antwoordt het met een gestructureerd JSON-object."
  3. Gemini vraagt een functieaanroep aan indien nodig.
    1. Indien nodig ontvangt de aanroeper van Gemini de naam en parameters voor de functieaanroep.
    2. De aanroeper kan beslissen of hij de aanroep uitvoert of niet.
      1. Of de aanroep uitvoeren en een geldige waarde retourneren.
      2. Of de aanroep niet uitvoeren, maar gegevens retourneren alsof deze wel is uitgevoerd.
      3. Of deze gewoon negeren.
  4. Gemini voert in het bovenstaande proces meerdere functies tegelijk uit of vraagt deze aan, zoals het aanroepen van een functie en vervolgens opnieuw aanroepen op basis van het resultaat.
  5. Uiteindelijk eindigt het proces wanneer een geordend antwoord wordt verkregen.

Deze stroom komt over het algemeen overeen met MCP. Dit wordt ook op vergelijkbare wijze uitgelegd in de MCP-tutorial. Dit geldt ook voor ollama tools.

En gelukkig delen deze drie tools, ollama tools, MCP en Gemini Function Calling, vrijwel dezelfde schemastructuur, wat betekent dat u door slechts één van deze, MCP, te implementeren, deze op alle drie de plaatsen kunt gebruiken.

Oh, en er is een nadeel dat ze allemaal delen. Uiteindelijk wordt het model uitgevoerd, dus als het model dat u gebruikt in slechte staat verkeert, kan het functies niet aanroepen, ze op een vreemde manier aanroepen, of storingen veroorzaken zoals het uitvoeren van een DOS-aanval op de MCP-server.

MCP-host in Go

mark3lab's mcphost

In Go is er mcphost, dat wordt ontwikkeld door een organisatie genaamd mark3lab.

Het gebruik is zeer eenvoudig.

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

Na installatie, maakt u het bestand $HOME/.mcp.json aan en schrijft u het als volgt:

 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}

En voer het vervolgens uit met het ollama-model. Natuurlijk, als het nodig is, ontvangt u eerst het model met ollama pull mistral-small.

Hoewel claude of qwen2.5 standaard worden aanbevolen, raad ik op dit moment mistral-small aan.

1mcphost -m ollama:mistral-small

Als u het echter op deze manier uitvoert, kunt u het alleen in een CLI-omgeving gebruiken voor vraag-en-antwoord. Daarom zullen we de code van deze mcphost aanpassen om deze programmeerbaarder te maken.

mcphost fork

Zoals reeds vastgesteld, bevat mcphost functionaliteit voor het extraheren van metadata en het aanroepen van functies met behulp van MCP. Daarom zijn onderdelen nodig voor het aanroepen van de LLM, het beheren van de MCP-server en het beheren van de berichtengeschiedenis.

De Runner van het volgende pakket heeft de betreffende onderdelen overgenomen.

 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}

De interne declaratie van dat deel zullen we niet afzonderlijk bekijken. Het is echter vrijwel letterlijk de naam.

 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}

Voor de mcpClients en tools die hier worden gebruikt, zie dit bestand. Aangezien de provider die van ollama zal zijn, zie dit bestand.

Het hoofdgerecht is de Run-methode.

  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}

De code zelf is samengesteld uit delen van dit bestand.

De inhoud is grofweg als volgt:

  1. Verzend de prompt samen met de lijst met tools om de uitvoering of het genereren van een antwoord aan te vragen.
  2. Als er een antwoord wordt gegenereerd, stopt de recursie en wordt het antwoord geretourneerd.
  3. Als de LLM een tooluitvoerverzoek achterlaat, roept de host de MCP Server aan.
  4. Voeg het antwoord toe aan de geschiedenis en keer terug naar stap 1.

Tot slot

Nu al klaar?

Er is eigenlijk niet veel meer te zeggen. Dit artikel is geschreven om u te helpen een globaal begrip te krijgen van de werking van de MCP Server. Ik hoop dat dit artikel u in geringe mate heeft geholpen de werking van de MCP-host te begrijpen.