MCP hostin ymmärtäminen paremmin
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.
Yleinen kulku on seuraava:
- Määritellään funktio.
- Funktioiden määrittelyt lähetetään Geminille kehotteen kanssa.
- "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."
- Gemini pyytää funktion kutsua, jos tarpeen.
- Jos Gemini tarvitsee sitä, kutsuja saa funktion kutsuun tarvittavan nimen ja parametrit.
- Kutsuja voi päättää, suorittaako kutsun vai ei.
- Suorittaako kutsun ja palauttaa oikean arvon
- Palauttaako tiedot ikään kuin kutsu olisi suoritettu, vaikka sitä ei suoritettu
- Ohittaako sen kokonaan
- Gemini suorittaa ja pyytää toimintoja, kuten kutsuu useita funktioita kerralla tai kutsuu funktioita uudelleen tulosten perusteella.
- 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:
- Lähetä kehote ja työkaluluettelo pyytääksesi suoritusta tai vastauksen luomista.
- Jos vastaus on luotu, pysäytä rekursio ja palauta.
- Jos LLM jättää työkalun suorituspyynnön, isäntä kutsuu MCP Serveriä.
- 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.