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

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.

Kokonaisuudessaan kulku on seuraava.
- Määritellään funktio.
- Lähetetään funktiomääritelmä kehotteen kanssa Geminille.
- 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.
- Gemini pyytää funktiokutsua tarvittaessa.
- Jos Gemini tarvitsee funktiokutsua, kutsuja saa sen nimen ja parametrit.
- Kutsuja päättää, miten toimii.
- Suorittaako kutsun ja palauttaako pätevän arvon
- Palauttaako datan ilman kutsua ikään kuin kutsu olisi tehty
- Vai jättääkö kutsun kokonaan huomiotta
- Gemini pyytää yllä olevassa prosessissa kerralla useita funktioita tai suorittaa lisäkutsuja tulosten perusteella.
- 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:
- Lähetetään työkalulista kehotteen kanssa ja pyydetään päättämään suorituksesta tai luomaan vastaus.
- Kun vastaus on luotu, rekursio pysäytetään ja vastaus palautetaan.
- Jos LLM jättää pyynnön työkalun suorittamisesta, isäntä kutsuu MCP-palvelinta.
- 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.