Post
Disponível em: English

Construindo um servidor MCP em Go do zero

E aí, pessoal!

Se você usa Claude, Cursor ou qualquer agente de IA no dia a dia, provavelmente já quis que ele pudesse acessar seu banco de dados, ler arquivos do seu projeto ou chamar uma API interna. O problema é que cada ferramenta tinha jeito diferente de integrar, e você acabava preso na interface que o agente oferecia.

O MCP resolve isso. E você vai construir um servidor em Go que funciona com Claude Desktop, Cursor e qualquer cliente compatível.


O que é MCP

MCP (Model Context Protocol) é um protocolo aberto criado pela Anthropic em novembro de 2024. A OpenAI adotou em março de 2025, e em dezembro de 2025 o protocolo foi doado para a Linux Foundation. Hoje é o padrão de fato para conectar agentes de IA a ferramentas externas.

A ideia central é simples: você cria um servidor que expõe capacidades, e qualquer cliente MCP compatível consegue usar essas capacidades sem que você precise integrar com cada agente separadamente.


Os três conceitos do protocolo

O MCP organiza o que um servidor pode oferecer em três tipos:

Tools são funções que o agente pode chamar. Recebem argumentos, executam alguma coisa e devolvem um resultado. São o caso mais comum.

Resources são dados que o agente pode ler. Arquivos, registros de banco, URLs, qualquer coisa que possa ser representada como conteúdo.

Prompts são templates de prompt que o servidor disponibiliza para o cliente reusar.

Neste post o foco é em Tools, que cobre a maioria dos casos práticos.


Configurando o projeto

O SDK oficial em Go ainda está em desenvolvimento. A biblioteca mais usada em produção é a mark3labs/mcp-go, que implementa a spec 2025-11-05.

1
2
3
mkdir mcp-servidor && cd mcp-servidor
go mod init github.com/seuusuario/mcp-servidor
go get github.com/mark3labs/mcp-go@latest

Estrutura do projeto:

1
2
3
4
5
6
7
8
mcp-servidor/
  main.go
  tools/
    fetch.go
    files.go
    search.go
  go.mod
  go.sum

O servidor mais simples possível

Antes de adicionar qualquer ferramenta, o servidor mínimo fica assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "fmt"
    "os"

    "github.com/mark3labs/mcp-go/server"
)

func main() {
    s := server.NewMCPServer(
        "meu-servidor",
        "1.0.0",
        server.WithToolCapabilities(false),
    )

    if err := server.ServeStdio(s); err != nil {
        fmt.Fprintf(os.Stderr, "erro: %v\n", err)
        os.Exit(1)
    }
}

O ServeStdio é o transporte padrão para integrar com Claude Desktop e Cursor. O servidor lê de stdin e escreve em stdout usando o protocolo JSON-RPC do MCP.


Adicionando ferramentas reais

Vamos criar três ferramentas que você usaria no dia a dia.

Ferramenta 1: buscar conteúdo de uma URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// tools/fetch.go
package tools

import (
    "context"
    "fmt"
    "io"
    "net/http"

    "github.com/mark3labs/mcp-go/mcp"
)

func FetchURLTool() mcp.Tool {
    return mcp.NewTool("fetch_url",
        mcp.WithDescription("Busca o conteúdo de uma URL via HTTP GET"),
        mcp.WithString("url",
            mcp.Required(),
            mcp.Description("A URL para buscar"),
        ),
    )
}

func FetchURLHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    url, ok := req.Params.Arguments["url"].(string)
    if !ok || url == "" {
        return mcp.NewToolResultError("parametro url obrigatorio"), nil
    }

    resp, err := http.Get(url)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("erro ao buscar url: %v", err)), nil
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("erro ao ler resposta: %v", err)), nil
    }

    return mcp.NewToolResultText(string(body)), nil
}

Ferramenta 2: ler arquivo local

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// tools/files.go
package tools

import (
    "context"
    "fmt"
    "os"

    "github.com/mark3labs/mcp-go/mcp"
)

func ReadFileTool() mcp.Tool {
    return mcp.NewTool("read_file",
        mcp.WithDescription("Le o conteudo de um arquivo local"),
        mcp.WithString("path",
            mcp.Required(),
            mcp.Description("Caminho absoluto ou relativo do arquivo"),
        ),
    )
}

func ReadFileHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    path, ok := req.Params.Arguments["path"].(string)
    if !ok || path == "" {
        return mcp.NewToolResultError("parametro path obrigatorio"), nil
    }

    content, err := os.ReadFile(path)
    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("erro ao ler arquivo: %v", err)), nil
    }

    return mcp.NewToolResultText(string(content)), nil
}

Ferramenta 3: buscar texto em arquivos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// tools/search.go
package tools

import (
    "context"
    "fmt"
    "os"
    "path/filepath"
    "strings"

    "github.com/mark3labs/mcp-go/mcp"
)

func SearchCodeTool() mcp.Tool {
    return mcp.NewTool("search_code",
        mcp.WithDescription("Busca um texto em todos os arquivos de um diretorio"),
        mcp.WithString("directory",
            mcp.Required(),
            mcp.Description("Diretorio onde buscar"),
        ),
        mcp.WithString("pattern",
            mcp.Required(),
            mcp.Description("Texto para buscar"),
        ),
    )
}

func SearchCodeHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
    dir, ok := req.Params.Arguments["directory"].(string)
    if !ok || dir == "" {
        return mcp.NewToolResultError("parametro directory obrigatorio"), nil
    }

    pattern, ok := req.Params.Arguments["pattern"].(string)
    if !ok || pattern == "" {
        return mcp.NewToolResultError("parametro pattern obrigatorio"), nil
    }

    var matches []string

    err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        if err != nil || info.IsDir() {
            return err
        }

        content, err := os.ReadFile(path)
        if err != nil {
            return nil
        }

        lines := strings.Split(string(content), "\n")
        for i, line := range lines {
            if strings.Contains(line, pattern) {
                matches = append(matches, fmt.Sprintf("%s:%d: %s", path, i+1, strings.TrimSpace(line)))
            }
        }

        return nil
    })

    if err != nil {
        return mcp.NewToolResultError(fmt.Sprintf("erro ao buscar: %v", err)), nil
    }

    if len(matches) == 0 {
        return mcp.NewToolResultText("nenhum resultado encontrado"), nil
    }

    return mcp.NewToolResultText(strings.Join(matches, "\n")), nil
}

Registrando tudo no servidor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// main.go
package main

import (
    "fmt"
    "os"

    "github.com/mark3labs/mcp-go/server"
    "github.com/seuusuario/mcp-servidor/tools"
)

func main() {
    s := server.NewMCPServer(
        "meu-servidor",
        "1.0.0",
        server.WithToolCapabilities(false),
    )

    s.AddTool(tools.FetchURLTool(), tools.FetchURLHandler)
    s.AddTool(tools.ReadFileTool(), tools.ReadFileHandler)
    s.AddTool(tools.SearchCodeTool(), tools.SearchCodeHandler)

    if err := server.ServeStdio(s); err != nil {
        fmt.Fprintf(os.Stderr, "erro: %v\n", err)
        os.Exit(1)
    }
}

Compilando o servidor

1
go build -o mcp-servidor .

O binario precisa estar acessivel para o Claude Desktop conseguir iniciar o processo.


Registrando no Claude Desktop

O Claude Desktop inicia o servidor como um processo filho, se comunicando via stdin/stdout. A configuracao fica em:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Adicione seu servidor:

1
2
3
4
5
6
7
8
{
  "mcpServers": {
    "meu-servidor": {
      "command": "/caminho/absoluto/mcp-servidor",
      "args": []
    }
  }
}

Reinicie o Claude Desktop. O icone de ferramentas no chat vai aparecer com as tres ferramentas registradas.


Testando de verdade

Com o servidor registrado, voce pode pedir ao Claude:

  • “Leia o arquivo /home/usuario/projeto/main.go e me explique o que ele faz”
  • “Busque todos os lugares onde a funcao ProcessOrder e chamada no diretorio /home/usuario/projeto”
  • “Busque o conteudo da URL https://api.github.com/repos/golang/go e me diga qual foi o ultimo commit”

O Claude vai chamar as ferramentas automaticamente quando entender que precisa delas. Voce ve as chamadas acontecendo em tempo real no chat.


Adicionando autenticacao entre o cliente e o servidor

Em ambientes reais voce pode querer restringir quais ferramentas ficam disponiveis dependendo do contexto. Uma forma simples e verificar uma variavel de ambiente no inicio:

1
2
3
4
5
6
7
8
9
10
11
func main() {
    token := os.Getenv("MCP_TOKEN")
    if token == "" {
        fmt.Fprintln(os.Stderr, "MCP_TOKEN nao definido")
        os.Exit(1)
    }

    s := server.NewMCPServer("meu-servidor", "1.0.0")
    // registra ferramentas...
    server.ServeStdio(s)
}

E no claude_desktop_config.json:

1
2
3
4
5
6
7
8
9
10
{
  "mcpServers": {
    "meu-servidor": {
      "command": "/caminho/mcp-servidor",
      "env": {
        "MCP_TOKEN": "seu-token-aqui"
      }
    }
  }
}

O que vem a seguir

Com essa base voce pode adicionar qualquer ferramenta:

  • consultas a banco de dados Postgres ou SQLite
  • chamadas a APIs internas com autenticacao
  • execucao de scripts e comandos do sistema
  • leitura de logs estruturados e agregacao de metricas

O protocolo tambem suporta transporte HTTP com SSE para servidores remotos, quando o servidor nao pode rodar localmente na maquina do usuario. A biblioteca mcp-go implementa os dois transportes.


Conclusao

MCP e simples de implementar e resolve um problema real: conectar agentes de IA a ferramentas que voce ja tem, sem depender de integracoes proprietarias. O SDK em Go funciona, o protocolo e estavel, e a adocao pelos principais clientes ja aconteceu.

O servidor que voce construiu neste post ja e funcional. A partir daqui e so adicionar as ferramentas que fazem sentido para o seu contexto.


Referencias