GoSuda

MCP hostin ymmärtäminen paremmin

By snowmerak
views ...

Mikä MCP on?

MCP on Anthropicin Claudea varten kehittämä protokolla. MCP on lyhenne sanoista Model Context Protocol, ja se on protokolla, joka mahdollistaa LLM:n aktiivisen toiminta- tai resurssipyynnön ulkoisille tahoille. Koska MCP on kirjaimellisesti vain pyyntöjä ja vastauksia käsittelevä protokolla, kehittäjän on huolehdittava sen prosessoinnista ja toteutuksesta.

Sisäinen toiminta

Ennen kuin selitämme sisäistä toimintaa, käymme läpi Gemini Function Callingin. Gemini Function Calling mahdollistaa LLM:n kutsuvan aktiivisesti ulkoisia toimintoja, aivan kuten MCP. Saatat ihmetellä, miksi Function Calling on otettu mukaan. Syy on se, että Function Calling julkaistiin ennen MCP:tä, ja koska molemmat käyttävät OpenAPI-skeemaa, ne ovat yhteensopivia, ja niiden toiminnan oletettiin olevan samanlaista. Tämän vuoksi Gemini Function Callingin yksityiskohtaisempi selitys on tuotu esiin, koska sen arveltiin olevan hyödyllinen.

FunctionCalling

Yleinen kulku on seuraava:

  1. Määritellään funktio.
  2. Funktioiden määrittelyt lähetetään Geminille kehotteen kanssa.
    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 pyytää funktion kutsua, jos tarpeen.
    1. Jos Gemini tarvitsee sitä, kutsuja saa funktion kutsuun tarvittavan nimen ja parametrit.
    2. Kutsuja voi päättää, suorittaako kutsun vai ei.
      1. Suorittaako kutsun ja palauttaa oikean arvon
      2. Palauttaako tiedot ikään kuin kutsu olisi suoritettu, vaikka sitä ei suoritettu
      3. Ohittaako sen kokonaan
  4. Gemini suorittaa ja pyytää toimintoja, kuten kutsuu useita funktioita kerralla tai kutsuu funktioita uudelleen tulosten perusteella.
  5. Prosessi päättyy, kun saadaan järjestetty vastaus.

Tämä kulku on yleisesti ottaen samansuuntainen MCP:n kanssa. Tämä selitetään samankaltaisesti myös MCP:n opetusohjelmassa. Tämä pätee myös ollama toolsiin.

Ja mikä todella onnekkainta, näiden kolmen työkalun, ollama toolsin, MCP:n ja Gemini Function Callingin skeemarakenteet ovat lähes jaettuja, joten pelkästään MCP:n toteuttamalla voi käyttää sitä kaikissa kolmessa paikassa.

Ja kaikilla on yhteinen haittapuoli. Koska malli lopulta suorittaa sen, jos käyttämäsi malli on huonossa kunnossa, se voi toimia virheellisesti, kuten olla kutsumatta funktiota, kutsua sitä oudosti tai suorittaa DOS-hyökkäyksen MCP-palvelimeen.

Go-pohjainen MCP-isäntä

mark3labin mcphost

Go:ssa on mark3lab-organisaation kehittämä mcphost.

Käyttö on erittäin yksinkertaista.

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

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

 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}

Ja suorita se sitten ollama-mallilla seuraavasti. Tietenkin, jos tarvitset sitä ennen, lataa malli komennolla ollama pull mistral-small.

Vaikka claudea tai qwen2.5:tä suositellaan yleensä, suosittelen toistaiseksi mistral-smallia.

1mcphost -m ollama:mistral-small

Tällä tavalla suoritettaessa sitä voidaan kuitenkin käyttää vain kysymys-vastaus-tyyppisesti CLI-ympäristössä. Siksi muokkaamme mcphost-koodia, jotta se toimisi ohjelmallisemmin.

mcphost-haarukka

Kuten jo todettiin, mcphost sisältää toiminnot metatietojen poimimiseen ja funktioiden kutsumiseen MCP:n avulla. Siksi tarvitaan osia LLM:n kutsumiseen, MCP-palvelimen käsittelyyn ja viestihistorian hallintaan.

Vastaava osa on seuraavan paketin Runner.

 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}

Emme tarkastele vastaavan osan sisäistä määrittelyä erikseen. Se on kuitenkin melko lailla juuri sitä, mitä nimi antaa ymmärtää.

 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}

Tässä käytettävien mcpClients- ja tools-arvojen osalta tarkista tämä tiedosto.provider käyttää ollaman omaa, joten tarkista tämä tiedosto.

Pääasia 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}

Itse koodi on koottu osittain tämän tiedoston koodista.

Sisältö on karkeasti seuraava:

  1. Lähetä kehote ja työkaluluettelo pyytääksesi suoritusta tai vastauksen luomista.
  2. Jos vastaus on luotu, pysäytä rekursio ja palauta.
  3. Jos LLM jättää työkalun suorituspyynnön, isäntä kutsuu MCP Serveriä.
  4. Lisää vastaus historiaan ja palaa kohtaan 1.

Lopuksi

Jo loppu?

Todellisuudessa sanottavaa ei ole kovin paljon. Tämä artikkeli on kirjoitettu auttamaan ymmärtämään, miten MCP Server toimii. Toivottavasti tämä artikkeli on auttanut teitä edes hieman ymmärtämään MCP hostin toimintaa.