GoSuda

Entendendo um pouco o host MCP

By snowmerak
views ...

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.

FunctionCalling

O fluxo geral é o seguinte:

  1. Define-se a função.
  2. Envia-se a definição da função para o Gemini junto com o prompt.
    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. O Gemini solicita a chamada da função se necessário.
    1. Se o Gemini precisar, o chamador recebe o nome e os parâmetros para a chamada da função.
    2. O chamador pode decidir se executa ou não.
      1. Se deve executar e retornar um valor válido.
      2. Se deve retornar dados como se tivesse sido chamado, sem realmente chamar.
      3. Se deve simplesmente ignorar.
  4. 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.
  5. 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:

  1. Envia a lista de ferramentas junto com o prompt para solicitar a execução ou a geração de uma resposta.
  2. Se uma resposta for gerada, a recursão é interrompida e a resposta é retornada.
  3. Se o LLM solicitar a execução da ferramenta, o host chama o MCP Server.
  4. 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.