GoSuda

Betekintés az MCP host működésébe

By snowmerak
views ...

Mi az MCP?

Az MCP egy protokoll, amelyet az Anthropic fejlesztett a claude számára. Az MCP a Model Context Protocol rövidítése, és egy olyan protokoll, amely lehetővé teszi az LLM számára, hogy proaktívan kérjen műveleteket vagy erőforrásokat kívülről. Mivel az MCP szó szerint csupán egy protokoll, amely kéréseket és válaszokat ad, a folyamatot és a végrehajtást a fejlesztőnek kell elvégeznie.

A belső működésről

Mielőtt a belső működést ismertetnénk, tekintsük át a Gemini Function Calling működését. A Gemini Function Calling az MCP-hez hasonlóan lehetővé teszi az LLM számára, hogy proaktívan hívjon külső műveleteket. Felmerülhet tehát a kérdés, hogy miért említjük meg a Function Callingot. Azért említjük meg, mert a Function Calling korábban jelent meg, mint az MCP, és mivel mindkettő az OpenAPI sémát használja, kompatibilisek, így feltételezhető, hogy működésük hasonló. Ezért, mivel a Gemini Function Calling magyarázata részletesebb, hasznosnak bizonyulhat, így ezt vettük alapul.

FunctionCalling

Az általános folyamat a következő:

  1. Meghatározza a függvényt.
  2. Elküldi a függvény definícióját a Gemini-nek a prompttal együtt.
    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. Ha szükséges, a Gemini függvényhívást kér.
    1. Ha a Gemini szükségesnek ítéli, a hívó fél megkapja a függvényhívás nevét és paramétereit.
    2. A hívó fél eldöntheti, hogy végrehajtja-e vagy sem.
      1. Végrehajtja és visszaad egy érvényes értéket.
      2. Nem hajtja végre, de adatokat ad vissza, mintha végrehajtotta volna.
      3. Egyszerűen figyelmen kívül hagyja.
  4. A Gemini ebben a folyamatban egyszerre több függvényt is hívhat, vagy függvényhívás után az eredményt látva újabb hívásokat kezdeményezhet, illetve kérhet ilyen műveleteket.
  5. Végül, ha rendezett válasz érkezik, a folyamat befejeződik.

Ez a folyamat általában összhangban van az MCP-vel. Ezt az MCP oktatóanyaga is hasonlóan magyarázza. Az ollama tools is hasonlóan működik.

És nagy szerencsére ez a 3 eszköz, az ollama tools, az MCP és a Gemini Function Calling szinte teljesen megosztja a séma szerkezetét, így elegendő csak az MCP-t implementálni ahhoz, hogy mindhárom helyen használható legyen.

Ó, és van egy hátrányuk, amelyet mindannyian megosztanak. Mivel végül is a modell hajtja végre, ha a használt modell állapota nem jó, előfordulhat, hogy nem hívja meg a függvényt, furcsán hívja meg, vagy akár DOS-t indít az MCP szerver ellen, és egyéb hibás működések léphetnek fel.

MCP host Go nyelven

mark3lab's mcphost

Go nyelven létezik a mark3lab nevű szervezet által fejlesztett mcphost.

Használata nagyon egyszerű.

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

Telepítés után hozzon létre egy $HOME/.mcp.json nevű fájlt, és írja bele a következőt:

 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}

Ezután futtassa az ollama modellel a következőképpen. Természetesen előtte, ha szükséges, töltse le a modellt az ollama pull mistral-small paranccsal.

Alapvetően a claude vagy a qwen2.5 ajánlott, de jelenleg a mistral-smallt ajánlom.

1mcphost -m ollama:mistral-small

Azonban így futtatva csak CLI környezetben használható kérdés-válasz módban. Ezért módosítsuk az mcphost kódját, hogy programozhatóbb módon működjön.

mcphost fork

Ahogy már láthattuk, az mcphost tartalmazza a metaadatok kinyerésének és a függvények hívásának funkcióját az MCP használatával. Ezért szükség van az LLM hívását, az MCP szerver kezelését és az üzenetelőzmények kezelését végző részekre.

Az ezeket a részeket tartalmazó Runner a következő csomagban található:

 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}

Az ehhez tartozó belső deklarációkat külön nem nézzük meg. De szinte pontosan a nevüknek megfelelően működnek.

 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}

Az itt használt mcpClients és tools kapcsán kérjük, tekintse meg a megfelelő fájlt. A provider az ollama-tól származik, így kérjük, tekintse meg a megfelelő fájlt.

A fő attrakció a Run metódus.

  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 // Kezdeti visszalépési idő
 18	const maxRetries int = 5 // Maximális újrapróbálkozások száma
 19	const maxBackoff = 30 * time.Second // Maximális visszalépési idő
 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", // claude jelenleg túlterhelt. várjon néhány percet, majd próbálja újra
 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", // Hiba a(z) %s eszköz hívásakor: %v
101				toolName,
102				err,
103			)
104			log.Printf("Error calling tool %s: %v", toolName, err) // Hiba a(z) %s eszköz hívásakor: %v
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}

Maga a kód a megfelelő fájl egyes részeinek összeillesztéséből származik.

A tartalom nagyjából a következő:

  1. Elküldi a promptot és az eszközlistát, kérve a végrehajtást vagy a válasz generálását.
  2. Ha válasz generálódik, leállítja a rekurziót és visszaadja azt.
  3. Ha az LLM eszköz végrehajtási kérést hagy, a host meghívja az MCP Servert.
  4. A választ hozzáadja az előzményekhez, és visszatér az 1. ponthoz.

Végül

Már vége?

Valójában nincs sok mondanivaló. Ez a cikk azért készült, hogy segítsen megérteni, hogyan működik nagyjából az MCP Server. Remélem, hogy ez a cikk kis mértékben is hozzájárult ahhoz, hogy megértsék az MCP host működését.