GoSuda

Försök att förstå MCP host lite grann

By snowmerak
views ...

Vad är MCP?

MCP är ett protokoll utvecklat av Anthropic för Claude. MCP är en förkortning för Model Context Protocol, och det är ett protokoll som gör det möjligt för en LLM att aktivt begära åtgärder eller resurser externt. Eftersom MCP bokstavligen bara är ett protokoll för att skicka förfrågningar och få svar, måste processen och exekveringen hanteras av utvecklaren.

Intern funktion

Innan vi förklarar den interna funktionen, låt oss kort nämna Gemini Function Calling. Gemini Function Calling, precis som MCP, gör det möjligt för en LLM att autonomt anropa externa åtgärder. Man kan undra varför Function Calling togs upp. Anledningen är att Function Calling kom före MCP, och eftersom båda använder OpenAPI-schemat är de kompatibla, vilket antyder att deras interaktioner skulle vara likartade. Därför togs Gemini Function Calling upp, då dess förklaringar är mer detaljerade och kan vara till hjälp.

FunctionCalling

Det övergripande flödet är följande:

  1. Definiera funktionen.
  2. Skicka funktionsdefinitionen till Gemini tillsammans 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 begär ett funktionsanrop om det behövs.
    1. Om Gemini behöver det, tar anroparen emot namnet och parametrarna för funktionsanropet.
    2. Anroparen kan bestämma om anropet ska utföras eller inte.
      1. Om ett legitimt värde ska returneras efter anropet.
      2. Om data ska returneras som om anropet hade utförts, utan att faktiskt anropa.
      3. Om det bara ska ignoreras.
  4. Gemini utför och begär åtgärder som att anropa flera funktioner samtidigt under ovanstående process, eller anropa igen efter att ha granskat resultatet av ett funktionsanrop.
  5. När ett välorganiserat svar erhålls, avslutas processen.

Detta flöde är generellt sett i linje med MCP. Detta beskrivs även på liknande sätt i MCP:s handledning. Detta liknar även ollama tools.

Och lyckligtvis delar dessa tre verktyg – ollama tools, MCP och Gemini Function Calling – en liknande schemastruktur, vilket innebär att genom att implementera MCP kan det användas på alla tre ställen.

Åh, och det finns en gemensam nackdel för alla. Eftersom modellen i slutändan utför exekveringen, om modellen du använder är i dåligt skick, kan den misslyckas med att anropa funktioner, anropa dem på ett konstigt sätt, eller utföra felaktiga åtgärder som att utföra en DOS-attack mot MCP-servern.

MCP Host i Go

mark3lab's mcphost

I Go finns mcphost som utvecklas av organisationen mark3lab.

Användningen är mycket enkel.

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

Efter installationen, skapa filen $HOME/.mcp.json och skriv följande:

 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}

Och kör sedan med ollama-modellen enligt följande: Naturligtvis, om det behövs, ladda ner modellen med ollama pull mistral-small först.

Även om claude eller qwen2.5 rekommenderas som standard, rekommenderar jag för närvarande mistral-small.

1mcphost -m ollama:mistral-small

Men om du kör det på detta sätt kan det bara användas i en CLI-miljö för frågor och svar. Därför kommer vi att modifiera koden för mcphost för att få den att fungera mer programmerbart.

mcphost Fork

Som redan konstaterats inkluderar mcphost funktionalitet för att extrahera metadata och anropa funktioner med hjälp av MCP. Därför behövs delar för att anropa LLM, hantera MCP-servern och hantera meddelandehistoriken.

Runner i följande paket är den del som har hämtats:

 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 interna deklarationen av denna del kommer inte att granskas separat. Dock är namnen nästan självförklarande.

 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}

För mcpClients och tools som kommer att användas här, vänligen se denna fil. Eftersom provider kommer att använda ollama:s, vänligen se denna fil.

Huvudrätten är metoden Run.

  1func (r *Runner) Run(ctx context.Context, prompt string) (string, error) {
  2	// Om prompten inte är tom, lägg till den i meddelandehistoriken.
  3	if len(prompt) != 0 {
  4		r.messages = append(r.messages, history.HistoryMessage{
  5			Role: "user",
  6			Content: []history.ContentBlock{{
  7				Type: "text",
  8				Text: prompt,
  9			}},
 10		})
 11	}
 12
 13	// Konvertera meddelandehistoriken till LLM-meddelandeformat.
 14	llmMessages := make([]llm.Message, len(r.messages))
 15	for i := range r.messages {
 16		llmMessages[i] = &r.messages[i]
 17	}
 18
 19	// Definiera backoff-konstanter för återförsök.
 20	const initialBackoff = 1 * time.Second
 21	const maxRetries int = 5
 22	const maxBackoff = 30 * time.Second
 23
 24	var message llm.Message
 25	var err error
 26	backoff := initialBackoff
 27	retries := 0
 28	// Loop för att skapa meddelanden med återförsök vid fel.
 29	for {
 30		message, err = r.provider.CreateMessage(
 31			context.Background(),
 32			prompt,
 33			llmMessages,
 34			r.tools,
 35		)
 36		if err != nil {
 37			// Hantera överbelastningsfel specifikt.
 38			if strings.Contains(err.Error(), "overloaded_error") {
 39				if retries >= maxRetries {
 40					return "", fmt.Errorf(
 41						"claude is currently overloaded. please wait a few minutes and try again",
 42					)
 43				}
 44
 45				time.Sleep(backoff)
 46				backoff *= 2
 47				if backoff > maxBackoff {
 48					backoff = maxBackoff
 49				}
 50				retries++
 51				continue
 52			}
 53
 54			return "", err
 55		}
 56
 57		break // Bryt loopen om meddelandet skapades framgångsrikt.
 58	}
 59
 60	var messageContent []history.ContentBlock
 61
 62	var toolResults []history.ContentBlock
 63	messageContent = []history.ContentBlock{}
 64
 65	// Lägg till meddelandets innehåll om det finns.
 66	if message.GetContent() != "" {
 67		messageContent = append(messageContent, history.ContentBlock{
 68			Type: "text",
 69			Text: message.GetContent(),
 70		})
 71	}
 72
 73	// Iterera över verktygsanrop och utför dem.
 74	for _, toolCall := range message.GetToolCalls() {
 75		input, _ := json.Marshal(toolCall.GetArguments())
 76		// Lägg till verktygsanropet till meddelandets innehåll.
 77		messageContent = append(messageContent, history.ContentBlock{
 78			Type:  "tool_use",
 79			ID:    toolCall.GetID(),
 80			Name:  toolCall.GetName(),
 81			Input: input,
 82		})
 83
 84		// Dela upp verktygsnamnet i servernamn och verktygsnamn.
 85		parts := strings.Split(toolCall.GetName(), "__")
 86
 87		serverName, toolName := parts[0], parts[1]
 88		mcpClient, ok := r.mcpClients[serverName]
 89		if !ok { // Hoppa över om MCP-klienten inte hittas.
 90			continue
 91		}
 92
 93		var toolArgs map[string]interface{}
 94		// Avkoda verktygsargumenten från JSON.
 95		if err := json.Unmarshal(input, &toolArgs); err != nil {
 96			continue
 97		}
 98
 99		var toolResultPtr *mcp.CallToolResult
100		req := mcp.CallToolRequest{}
101		req.Params.Name = toolName
102		req.Params.Arguments = toolArgs
103		// Anropa verktyget via MCP-klienten.
104		toolResultPtr, err = mcpClient.CallTool(
105			context.Background(),
106			req,
107		)
108
109		if err != nil {
110			// Hantera fel vid verktygsanrop.
111			errMsg := fmt.Sprintf(
112				"Error calling tool %s: %v",
113				toolName,
114				err,
115			)
116			log.Printf("Error calling tool %s: %v", toolName, err)
117
118			// Lägg till felmeddelandet som ett verktygsresultat.
119			toolResults = append(toolResults, history.ContentBlock{
120				Type:      "tool_result",
121				ToolUseID: toolCall.GetID(),
122				Content: []history.ContentBlock{{
123					Type: "text",
124					Text: errMsg,
125				}},
126			})
127
128			continue
129		}
130
131		toolResult := *toolResultPtr
132
133		// Om verktygsresultatet har innehåll, lägg till det.
134		if toolResult.Content != nil {
135			resultBlock := history.ContentBlock{
136				Type:      "tool_result",
137				ToolUseID: toolCall.GetID(),
138				Content:   toolResult.Content,
139			}
140
141			var resultText string
142			// Extrahera textinnehåll från verktygsresultatet.
143			for _, item := range toolResult.Content {
144				if contentMap, ok := item.(map[string]interface{}); ok {
145					if text, ok := contentMap["text"]; ok {
146						resultText += fmt.Sprintf("%v ", text)
147					}
148				}
149			}
150
151			resultBlock.Text = strings.TrimSpace(resultText)
152
153			toolResults = append(toolResults, resultBlock)
154		}
155	}
156
157	// Lägg till det genererade meddelandet till historiken.
158	r.messages = append(r.messages, history.HistoryMessage{
159		Role:    message.GetRole(),
160		Content: messageContent,
161	})
162
163	// Om det finns verktygsresultat, lägg till dem som ett nytt användarmeddelande och kör igen rekursivt.
164	if len(toolResults) > 0 {
165		r.messages = append(r.messages, history.HistoryMessage{
166			Role:    "user",
167			Content: toolResults,
168		})
169
170		return r.Run(ctx, "")
171	}
172
173	// Returnera det slutliga meddelandets innehåll.
174	return message.GetContent(), nil
175}

Själva koden är en sammanställning av delar av koden från denna fil.

Innehållet är ungefär följande:

  1. Skicka prompten och verktygslistan för att begära exekvering eller svarsgenerering.
  2. Om ett svar genereras, stoppa rekursionen och returnera.
  3. Om LLM begär verktygsexekvering, anropar hosten MCP Server.
  4. Lägg till svaret i historiken och återgå till steg 1.

Avslutningsvis

Redan slut?

Faktum är att det inte finns så mycket att säga. Detta inlägg är skrivet för att hjälpa dig att förstå hur en MCP Server fungerar i stora drag. Jag hoppas att detta inlägg har varit till viss hjälp för att förstå hur en MCP host fungerar.