GoSuda

MCP-isännän hieman ymmärtäminen

By snowmerak views ...

Mitä MCP on

MCP on Anthropicin claudea varten kehittämä protokolla. MCP on lyhenne sanoista Model Context Protocol, ja se on protokolla, jonka avulla LLM voi aktiivisesti pyytää ulkoisia toimintoja tai resursseja. MCP on sanasta sanaan vain protokolla, joka välittää pyyntöjä ja vastauksia, joten prosessin ja toteutuksen tekee kehittäjän itse.

Sisäinen toiminta

Ennen sisäisen toiminnan selittämistä käyn lyhyesti läpi Gemini Function Callingin. Gemini Function Calling mahdollistaa samoin kuin MCP sen, että LLM voi itse aktiivisesti kutsua ulkoisia toimintoja. Saatat kysyä, miksi toin Function Callingin esiin. Syy on se, että Function Calling ilmestyi aiemmin kuin MCP, ja koska se hyödyntää samaa OpenAPI-skeemaa, ne ovat yhteensopivia, joten arvioin niiden toiminnan olevan samankaltaista. Gemini Function Callingin selitys on myös suhteellisen yksityiskohtainen, joten arvioin sen olevan hyödyllinen, ja siksi toin sen esiin.

FunctionCalling

Kokonaisuudessaan kulku on seuraava.

  1. Määritellään funktio.
  2. Lähetetään funktiomääritelmä kehotteen kanssa Geminille.
    1. Lähetä käyttäjän kehote yhdessä funktiomääritelmien kanssa mallille. Se analysoi pyynnön ja päättää, olisiko funktiokutsusta hyötyä. Jos on, se vastaa jäsennellyllä JSON-objektilla.
  3. Gemini pyytää funktiokutsua tarvittaessa.
    1. Jos Gemini tarvitsee funktiokutsua, kutsuja saa sen nimen ja parametrit.
    2. Kutsuja päättää, miten toimii.
      1. Suorittaako kutsun ja palauttaako pätevän arvon
      2. Palauttaako datan ilman kutsua ikään kuin kutsu olisi tehty
      3. Vai jättääkö kutsun kokonaan huomiotta
  4. Gemini pyytää yllä olevassa prosessissa kerralla useita funktioita tai suorittaa lisäkutsuja tulosten perusteella.
  5. Lopulta, kun saadaan jäsennelty vastaus, prosessi päättyy.

Tämä kulku vastaa yleisesti MCP:tä. Sitä selittää samankaltaisesti myös MCP:n opetusohjelma. Sama pätee myös ollama toolsiin.

Onneksi nämä kolme työkalua, ollama tools, MCP ja Gemini Function Calling, jakavat lähes saman skeemarakenteen, joten pelkällä MCP:n toteutuksella voi käyttää sitä kaikissa kolmessa.

Kaikilla on myös yhteinen heikkous. Koska malli itse suorittaa kutsut, jos käyttämäsi malli on heikko, se voi jättää funktiot kutsumatta, kutsua niitä oudosti tai aiheuttaa muita virheitä, kuten lähettää DOS-hyökkäyksen MCP-palvelimelle.

Go-kielinen MCP-isäntä

mark3lab's mcphost

Go-kielellä on mcphost, jota kehitetään mark3lab-organisaatiossa.

Käyttö on hyvin yksinkertaista.

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

Asennuksen jälkeen luo tiedosto $HOME/.mcp.json ja kirjoita siihen seuraavaa:

 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}

Sen jälkeen suoritat sen ollama-mallilla seuraavasti:
Tietenkin tarvittaessa lataat ennen sitä mallin komennolla ollama pull mistral-small.

Yleisesti ottaen suosittelen claudea tai qwen2.5:tä, mutta itse suosittelen tällä hetkellä mistral-smallia.

1mcphost -m ollama:mistral-small

Tällöin sitä voi kuitenkin käyttää vain kysymys-vastaus-tyyppisesti CLI-ympäristössä.
Siksi muokkaamme mcphost-koodia, jotta siitä saadaan ohjelmallisempi.

mcphost-haarukointi

Kuten jo totesimme, mcphost sisältää toiminnon, joka hyödyntää MCP:tä metatietojen poimimiseen ja funktioiden kutsumiseen. Siksi tarvitaan osa, joka kutsuu LLM:ää, osa, joka käsittelee MCP-palvelinta, ja osa, joka hallinnoi viestihistoriaa.

Tämä osa on otettu paketista, jonka Runner on.

 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}

Sen sisäistä määrittelyä ei käsitellä tässä erikseen. Se on kuitenkin lähes nimensä mukainen.

 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}

Käytettävien mcpClients- ja tools-osien osalta katso tämä tiedosto.
provider-osassa käytetään ollaman versiota, joten katso tämä tiedosto.

Pääosa on Run-metodi.

  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}

Koodi on osittain koottu tämän tiedoston koodista.

Sisältö on karkeasti seuraava:

  1. Lähetetään työkalulista kehotteen kanssa ja pyydetään päättämään suorituksesta tai luomaan vastaus.
  2. Kun vastaus on luotu, rekursio pysäytetään ja vastaus palautetaan.
  3. Jos LLM jättää pyynnön työkalun suorittamisesta, isäntä kutsuu MCP-palvelinta.
  4. Vastaus lisätään historiaan ja palataan kohtaan 1.

Lopuksi

Jo loppu?

Itse asiassa minulla ei ole kovin paljon sanottavaa. Tämä teksti on kirjoitettu lähinnä auttamaan ymmärtämään, miten MCP-palvelin karkeasti toimii. Toivottavasti tämä edes vähän auttoi ymmärtämään MCP-isännän toimintaa.