Criando um MCP Server usando Go
Em Novembro de 2024 a Anthropic publicou um post em seu blog anunciando o que talvez seja sua maior contribuição para o ecossistema de AI até o momento: o Model Context Protocol
Segundo a definição oficial no site:
O MCP é um protocolo aberto que padroniza como os aplicativos fornecem contexto ao LLMS. Pense no MCP como uma porta USB-C para aplicativos de IA. Assim como o USB-C fornece uma maneira padronizada de conectar seus dispositivos a vários periféricos e acessórios, o MCP fornece uma maneira padronizada de conectar modelos de IA a diferentes fontes e ferramentas de dados.
Rapidamente outros players começaram a anunciar o suporte a este novo protocolo:
Não vou entrar em detalhes sobre toda a arquitetura definida no MCP para não tornar este post muito extenso, mas vou deixar alguns links no final para quem quiser se aprofundar mais.
Mesmo assim, alguns componentes básicos são necessários para o entendimento:
- MCP Hosts: Programas como Claude Desktop, IDEs ou ferramentas de AI que desejam acessar dados através do MCP
- MCP Clients: Clientes de protocolo que mantêm conexões 1: 1 com servidores
- MCP Servers: Programas leves que expõem recursos específicos através do MCP
Neste exemplo eu vou criar um Server, uma aplicação Go, e vou usar alguns Hosts para testar o acesso a ele.
Além das definições sobre o protocolo, no site oficial do projeto estão disponíveis SDKs para algumas linguagens como Python, TypeScript, Java, Kotlin e C#. Apesar de existir uma discussão sobre o suporte a Go no repositório oficial, a comunidade já começou a criar algumas implementações do protocolo. Para a prova de conceito apresentada neste post eu usei um destes projetos, o mcp-golang.
O primeiro passo foi a criação de um novo projeto Go:
mkdir mcp-server
go mod init github.com/eminetto/mcp-server
O código do main.go
ficou desta forma:
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
mcp_golang "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
"github.com/ryanuber/go-filecache"
)
const cacheTime = 500
type MyFunctionsArguments struct {
ZipCode string `json:"zip_code" jsonschema:"required,description=The zip code to be searched"`
}
// Cep is the brazilian postal code and address information
type Cep struct {
Cep string `json:"cep"`
Logradouro string `json:"logradouro"`
Complemento string `json:"complemento"`
Bairro string `json:"bairro"`
Localidade string `json:"localidade"`
Uf string `json:"uf"`
Unidade string `json:"unidade"`
Ibge string `json:"ibge"`
Gia string `json:"gia"`
}
func main() {
done := make(chan struct{})
server := mcp_golang.NewServer(stdio.NewStdioServerTransport())
err := server.RegisterTool("zipcode", "Find an address by his zip code", func(arguments MyFunctionsArguments) (*mcp_golang.ToolResponse, error) {
address, err := getCep(arguments.ZipCode)
if err != nil {
return nil, err
}
return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Your address is %s!", address))), nil
})
if err != nil {
panic(err)
}
err = server.Serve()
if err != nil {
panic(err)
}
<-done
}
func getCep(id string) (string, error) {
cached := getFromCache(id)
if cached != "" {
return cached, nil
}
req, err := http.Get(fmt.Sprintf("http://viacep.com.br/ws/%s/json/", id))
if err != nil {
return "", err
}
var c Cep
err = json.NewDecoder(req.Body).Decode(&c)
if err != nil {
return "", err
}
res, err := json.Marshal(c)
if err != nil {
return "", err
}
return saveOnCache(id, string(res)), nil
}
func getFromCache(id string) string {
updater := func(path string) error {
return errors.New("expired")
}
fc := filecache.New(getCacheFilename(id), cacheTime*time.Second, updater)
fh, err := fc.Get()
if err != nil {
return ""
}
content, err := io.ReadAll(fh)
if err != nil {
return ""
}
return string(content)
}
func saveOnCache(id string, content string) string {
updater := func(path string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write([]byte(content))
return err
}
fc := filecache.New(getCacheFilename(id), cacheTime*time.Second, updater)
_, err := fc.Get()
if err != nil {
return ""
}
return content
}
func getCacheFilename(id string) string {
return os.TempDir() + "/cep" + strings.Replace(id, "-", "", -1)
}
Vou destacar a seguir os trechos mais importantes.
Definição dos parâmetros da função que vai estar disponível:
type MyFunctionsArguments struct {
ZipCode string `json:"zip_code" jsonschema:"required,description=The zip code to be searched"`
}
Definição da ferramenta que vai estar disponível para o client e host:
server := mcp_golang.NewServer(stdio.NewStdioServerTransport())
err := server.RegisterTool("zipcode", "Find an address by his zip code", func(arguments MyFunctionsArguments) (*mcp_golang.ToolResponse, error) {
address, err := getCep(arguments.ZipCode)
if err != nil {
return nil, err
}
return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Your address is %s!", address))), nil
})
if err != nil {
panic(err)
}
err = server.Serve()
O protocolo define que um Server pode disponibilizar três “capabilities”:
- Resources: Dados semelhantes a arquivos que podem ser lidos por clientes (como respostas de API ou conteúdo de arquivo)
- Tools: Funções que podem ser chamadas pelo LLM (com aprovação do usuário)
- Prompts: Modelos pré-escritos que ajudam os usuários a realizar tarefas específicas
Como podemos ver no código acima, nesta prova de conceito estou disponibilizando apenas uma tool. Os outros itens eu deixei de fora para simplificar o exemplo.
O restante do código é responsável pela lógica da ferramenta, que neste caso é acessar uma API externa e retornar dados de acordo com o que foi pesquisado. Outros exemplos poderiam ser o acesso um banco de dados, um recurso de uma nuvem privada, etc. No site do projeto constam alguns outros exemplos.
O próximo passo é gerar um binário, usando o go build
.
Agora que temos nosso Server gerado precisamos configurar algum Host para fazer uso da lógica que estamos disponibilizando.
Como a Anthropic foi a criadora do protocolo nada mais justo do que a sua ferramenta, o Claude seja o melhor exemplo para começarmos. Com o aplicativo instalado no macOS basta acessarmos a área de configuração, clicar em “Developer”, e em “Edit Config”. Somos levados à localização do arquivo claude_desktop_config.json
que editei e salvei com o seguinte conteúdo:
{
"mcpServers": {
"golang-mcp-server": {
"command": "/Users/eminetto/Developer/mcp-server/mcp-server",
"args": [],
"env": {}
}
}
}
Existem mais configurações possíveis, mas estas foram suficientes para este cenário.
Podemos agora fazer uma pergunta ao Claude, como: Qual é o endereço completo do cep 88034102?
. Ele vai avisar que para responder isso é necessário acessar uma ferramenta externa:
Após a permissão ser dada a resposta é mostrada com sucesso:
A mesma pergunta pode ser feita em inglês e o resultado é o esperado:
Como o MCP se propõe, podemos usar o mesmo Server com outros Hosts. Para testar isso usei o Cursor, famosa IDE baseada em IA. Para configurá-la também é bem simples, bastando clicar no ícone de configurações (a universal engrenagem). Na tela de configurações existe uma opção “MCP” e dentro dela a “Add new global MCP Server”. A IDE vai abrir um arquivo mcp.json
onde vamos preencher com o mesmo conteúdo que usamos para configurar o Claude. Podemos agora usar o chat da IDE para fazer a mesma pergunta que fizemos anteriormente e temos o mesmo resultado:
O MCP abre caminho para um alcance ainda maior das ferramentas de IA existentes, principalemente com mais empresas adotando o padrão. Confesso que demorei um pouco para me empolgar com as possibilidades das IAs/GenAI, para poder separar o hype da realidade, mas finalmente essa empolgação me atingiu e vejo grandes possibilidades para aplicações dentro das empresas.
Quando o Gemini lançar o suporte ao MCP (talvez no Google I/O deste ano? Vou estar lá, espero trazer novidades) eu pretendo fazer outro post sobre o assunto. O mesmo no momento do lançamento da SDK oficial de Go.
E qual sua opinião sobre isso, nobre leitor(a)? Acredita que é mais uma onda de hype ou esta padronização realmente abre caminho para novas aplicações?