Entendendo um pouco o host MCP
O que é MCP?
MCP é um protocolo desenvolvido pela Anthropic para claude. MCP é a abreviação de Model Context Protocol, e é um protocolo que permite que um LLM solicite ativamente ações ou recursos externos. Como o MCP é literalmente apenas um protocolo de solicitação e resposta, o processo e a execução devem ser feitos pelo desenvolvedor.
Sobre o funcionamento interno
Antes de explicar o funcionamento interno, vamos abordar o Gemini Function Calling. O Gemini Function Calling, assim como o MCP, permite que o LLM chame proativamente ações externas. Você deve estar se perguntando por que o Function Calling foi trazido à discussão. A razão é que o Function Calling surgiu antes do MCP, e como ambos usam o esquema OpenAPI, eles são compatíveis, e supõe-se que suas operações sejam semelhantes. Por isso, o Gemini Function Calling foi trazido, pois sua explicação é mais detalhada e pode ser útil.
O fluxo geral é o seguinte:
- Define-se a função.
- Envia-se a definição da função para o Gemini junto com o prompt.
- "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."
- O Gemini solicita a chamada da função se necessário.
- Se o Gemini precisar, o chamador recebe o nome e os parâmetros para a chamada da função.
- O chamador pode decidir se executa ou não.
- Se deve executar e retornar um valor válido.
- Se deve retornar dados como se tivesse sido chamado, sem realmente chamar.
- Se deve simplesmente ignorar.
- O Gemini executa e solicita ações como chamar várias funções de uma vez ou chamar uma função e, após ver o resultado, chamar novamente.
- Finalmente, quando uma resposta organizada é gerada, o processo termina.
Esse fluxo geralmente é consistente com o MCP. Isso também é explicado de forma semelhante no tutorial do MCP. Isso também é semelhante para as ferramentas ollama.
E, felizmente, essas três ferramentas, ollama tools, MCP e Gemini Function Calling, compartilham uma estrutura de esquema, de modo que a implementação de apenas um MCP pode ser usada em todos os três lugares.
Ah, e há uma desvantagem que todos compartilham. No final das contas, é o modelo que executa, então se o modelo que você está usando estiver em mau estado, ele pode não chamar a função, chamá-la de forma estranha, ou até mesmo realizar operações errôneas como um ataque de DOS no servidor MCP.
Host MCP em Go
mcphost do mark3lab
Em Go, há um mcphost que está sendo desenvolvido pela organização mark3lab.
O uso é muito simples.
1go install github.com/mark3labs/mcphost@latest
Após a instalação, crie o arquivo $HOME/.mcp.json
e escreva o seguinte:
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}
E execute com o modelo ollama da seguinte forma:
Claro, antes disso, se necessário, baixe o modelo com ollama pull mistral-small
.
Embora claude ou qwen2.5 sejam geralmente recomendados, eu, por enquanto, recomendo mistral-small.
1mcphost -m ollama:mistral-small
No entanto, ao executar dessa forma, só pode ser usado no ambiente CLI em um formato de pergunta e resposta.
Portanto, vamos modificar o código deste mcphost
para que ele possa operar de forma mais programável.
Fork do mcphost
Como já verificado, o mcphost
inclui a funcionalidade de extrair metadados usando MCP e chamar funções. Portanto, são necessárias as partes que chamam o LLM, que lidam com o servidor MCP e que gerenciam o histórico de mensagens.
O Runner
do pacote a seguir é a parte correspondente que foi trazida.
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}
Não vamos ver a declaração interna desta parte separadamente. No entanto, é quase o que o nome indica.
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}
Para mcpClients
e tools
a serem usados aqui, por favor, verifique este arquivo.
Para provider
, usaremos o do ollama, então por favor, verifique este arquivo.
O prato principal é o método Run
.
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}
O próprio código foi montado a partir de partes do arquivo correspondente.
O conteúdo é aproximadamente o seguinte:
- Envia a lista de ferramentas junto com o prompt para solicitar a execução ou a geração de uma resposta.
- Se uma resposta for gerada, a recursão é interrompida e a resposta é retornada.
- Se o LLM solicitar a execução da ferramenta, o host chama o MCP Server.
- A resposta é adicionada ao histórico e o processo retorna ao passo 1.
Conclusão
Já acabou?
Na verdade, não há muito o que dizer. Este artigo foi escrito para ajudar a entender como o MCP Server funciona. Espero que este artigo tenha sido de alguma ajuda para você entender o funcionamento de um host MCP.