Ir para o conteúdo
Trilha
Capítulos

Referência

mcp

Cliente e servidor do Model Context Protocol (stdio e Streamable HTTP).

import "github.com/emersonjoe/trilha/ai/mcp" — JSON-RPC 2.0, revisão 2025-03-26, sem dependências externas. Cobre o recurso tools (listar e chamar).

trilha mcp — o projeto como ferramentas#

Agente com shell não precisa disto: trilha ctx --json e trilha check --json já são a resposta. Isto é para o agente sem shell — um cliente de chat, um editor que fala MCP e mais nada — que de outro jeito precisa adivinhar o que o seu projeto tem dentro.

cd meu-projeto
trilha mcp

Ele fala MCP por stdin/stdout e responde sobre o projeto em que foi iniciado. Aponte seu cliente para ele:

{
  "mcpServers": {
    "trilha": { "command": "trilha", "args": ["mcp"], "cwd": "/caminho/do/meu-projeto" }
  }
}

Esse formato vale no Claude Desktop, no Claude Code e em qualquer cliente que suba um servidor por stdio. O cwd é quem decide o projeto, e nada que o modelo mande muda isso.

As ferramentas#

FerramentaO que respondeO que está por trás
describe_projectrotas, API, tipos e setup, em JSONtrilha ctx --json
routesa tabela de rotas em ordem de precedênciatrilha routes
ui_describeo catálogo do ui: assinatura, para que serve, exemplo que compilao catálogo que vem no binário — dispensa projeto
checkgen, gofmt, vet, testes, auditoria e OpenAPI, em JSONtrilha check --json
get_contexto mapa do projeto recortado para um trabalho (pack), com preço em tokens estimados, dentro de um budgettrilha ctx --pack
search_codecódigo em janelas caminho:linha, nunca o arquivo inteiroos arquivos do projeto, só leitura
get_patternum padrão de tela inteiro: componentes, snippet, dados, acessibilidadetrilha ui patterns <nome>
generategrava página, rota, teste ou componentetrilha generate — só com --write

Cada uma é um invólucro: a ferramenta responde o que o comando responde, byte a byte, e há um teste que segura isso. Não existe uma segunda implementação para sair de sincronia.

O que ele não faz#

É somente leitura até você dizer o contrário. Sem --write, a ferramenta que grava não é registrada — ela não aparece no tools/list, então o modelo não tem como pedi-la e não há recusa para discutir. Suba com trilha mcp --write quando quiser que o agente gere código; a ferramenta diz Writes files na primeira linha da própria descrição, que é o que a pessoa que aprova a chamada lê.

Nunca usa shell. Todo comando roda como programa mais uma lista de argumentos. Valor vindo do modelo nunca é concatenado numa linha de comando.

Confere argumento contra lista do que é permitido, não contra lista do que é proibido. Endereço de rota tem que parecer endereço de rota; nome de componente tem que ser identificador; método tem que ser um dos seis. /../../etc/passwd, /x; rm -rf / e --force são recusados com o motivo, antes de qualquer coisa rodar.

Não sai do projeto. O diretório de trabalho é decidido quando o servidor sobe.

Um comando por vez, cada um com prazo (10 minutos para o check, que roda a sua suíte; um minuto para os outros) e teto de 1 MiB de saída, para que uma chamada de ferramenta não vire bomba de processos nem coma memória.

Não abre conexão de rede, e não escreve nada no stdout além do protocolo — toda linha de log vai para o stderr, onde você acompanha o que o agente pediu:

trilha mcp 0.43.0 · /caminho/do/meu-projeto · read-only unless --write; no shell; arguments allow-listed
tools: describe_project, check, routes, ui_describe, get_context, search_code, get_pattern
→ trilha ctx --json

trilha mcp --from-routes#

A outra coisa que a CLI responde é como a minha API ficaria como ferramentas: a lista que FromRoutes publicaria, derivada de app/ e do documento OpenAPI, impressa antes de existir servidor.

$ trilha mcp --from-routes
ferramentas que mcp.FromRoutes exporia (include: /api/*):
  deleteApiPostsId   DELETE /api/posts/{id}   DELETE removes a post.
  getApiPosts        GET /api/posts           GET lists posts.
  getApiPostsId      GET /api/posts/{id}      GET returns one post by slug.
  postApiPosts       POST /api/posts          POST creates a post from JSON.

--include aceita os mesmos padrões de FromRoutesOpts.Include. Uma rota deixada de fora — um upload multipart, por exemplo — sai com o motivo, a mesma linha que o servidor registra no log.

Servidor de documentação#

O aplicativo do site serve um endpoint MCP Streamable HTTP somente leitura em POST /mcp. Ele mantém a documentação fora do binário da CLI e lê exatamente o Markdown embutido pelo site:

FerramentaResposta
search_docspáginas encontradas por título, descrição e corpo; locale é en ou pt
get_pageuma página pelo caminho do site, como /pt/referencia/ui
get_recipeuma página de Receitas pelo slug, como paginacao
get_contexta fatia de documentação que uma receita toca — a página da receita e as páginas que ela liga — cada uma com o custo estimado (est., 4 caracteres por token)
search_codeas fontes Go das Receitas em janelas caminho:linha, a linha achada mais uma depois, poucas ocorrências de uma vez — nunca o arquivo inteiro
get_erroruma entrada do catálogo pelo código — causa, conserto, exemplo — a mesma tabela que o trilha check imprime ao lado de cada falha; um código de família como E_VULN_GO-2026-0001 responde a entrada da família
get_patternum dos padrões de tela do kit pelo nome — componentes, contrato de dados, notas de acessibilidade e um page.go completo de no máximo 60 linhas que compila; o mesmo dado do trilha ui patterns

get_recipe devolve o Markdown canônico da receita. O teste do site já confirma que os blocos Go aparecem caractere por caractere em examples/cookbook, então o MCP e a página publicada não podem divergir em silêncio do código que compila. Rode o aplicativo site num host que aceite POST; a exportação para GitHub Pages continua sendo o espelho estático da documentação e não tenta simular MCP com arquivos.

Cliente#

func Dial(ctx, dial Dialer) (*Client, error)     // abre o transporte e faz initialize
func Stdio(name string, args ...string) Dialer    // processo filho, JSON por linha
func HTTP(url string, headers map[string]string) Dialer  // Streamable HTTP (POST por mensagem)
func HTTPWith(url string, client *http.Client, headers map[string]string) Dialer  // o mesmo, sobre um cliente seu
MétodoPapel
ListTools(ctx) ([]ToolInfo, error)segue a paginação (nextCursor)
CallTool(ctx, name, args) (CallResult, error)CallResult.Text() junta os itens de texto
Tools(ctx) ([]*ai.Tool, error)ferramentas prontas para um ai.Agent; isError vira erro
Server.Name/Version/ProtocolVersionpreenchidos pelo initialize
Close()fecha o transporte e encerra o processo filho

O cliente HTTP guarda o Mcp-Session-Id recebido no initialize e o envia nas mensagens seguintes; aceita respostas JSON ou text/event-stream.

Servidor#

func NewServer(name, version string, tools ...*ai.Tool) *Server
func (s *Server) ServeHTTP(c *trilha.Ctx) error         // em app/.../route.go: POST
func (s *Server) Handler() http.Handler                  // fora do Trilha
func (s *Server) ServeStdio(ctx, r io.Reader, w io.Writer) error

Métodos atendidos: initialize, ping, tools/list, tools/call; notificações são aceitas sem resposta (202). Em HTTP, initialize emite Mcp-Session-Id; mensagens sem sessão válida recebem 404; sessões expiram após SessionTTL (1 h) sem uso. Só POST é aceito (405 com Allow: POST para o resto). Corpo limitado a 4 MiB.

Erros e pânicos de ferramentas viram resultado com isError: true, como manda o protocolo; ferramenta desconhecida é erro JSON-RPC -32602.

A sua API como ferramentas#

func FromRoutes(app *trilha.App, o FromRoutesOpts) *Server
func Preview(openAPI []byte, routes map[string][]string, o FromRoutesOpts) ([]ToolInfo, []string, error)

type FromRoutesOpts struct {
	Name, Version string   // o que o initialize responde
	Include       []string // padrões a expor; padrão /api/*
	Exclude       []string // padrões a esconder, conferidos depois de Include
	OpenAPI       []byte   // o documento que trilha openapi escreve; obrigatório
}

As ferramentas de contexto#

func ContextTools(opts ContextOpts) []*ai.Tool

type ContextOpts struct {
	Root    string // a pasta do projeto
	Module  string // o caminho do módulo; lido do go.mod quando vazio
	Version string // o que o mapa carimba
}

ContextTools devolve get_context, search_code e get_pattern, as ferramentas que um agente usa antes de escrever uma linha: o mapa do projeto fatiado por pack e com preço em tokens estimados (as mesmas fatias que o trilha ctx --pack responde), código em janelas caminho:linha em vez de arquivos inteiros, e um padrão de tela do kit inteiro — componentes, dados, acessibilidade e um page.go que compila (Padrões). Junte-as às ferramentas de domínio no mesmo servidor.

Uma API escrita em app/api/ já é o que um agente precisa: nome, descrição, esquema de argumentos, handler. FromRoutes a publica como ferramentas MCP sem uma segunda declaração — uma ferramenta por (método, rota), com o nome do operation id do documento (getApiPosts, postApiPosts), a descrição do comentário do próprio handler, e os parâmetros do caminho, a query e o corpo achatados num esquema de entrada só.

//go:embed mcp/openapi.json
var openAPI []byte

func Setup(a *trilha.App) error {
	trilha.Provide(a, mcp.FromRoutes(a, mcp.FromRoutesOpts{Name: "blog", Version: "1.0", OpenAPI: openAPI}))
	return nil
}
// app/mcp/route.go
func POST(c *trilha.Ctx) error { return trilha.Use[*mcp.Server](c).ServeHTTP(c) }

O documento é o que dá às ferramentas descrição e esquema — o runtime não os guarda —, por isso ele mora ao lado da rota, em app/mcp/openapi.json, escrito por trilha openapi -o app/mcp/openapi.json e embutido. O trilha check compara cada openapi.json dentro de app/ com as rotas: uma cópia que ficou para trás derruba o build, não o agente. Sem documento, FromRoutes entra em pânico: ferramenta sem descrição é ferramenta que o modelo usa errado.

Chamar é chamar a rota. A ferramenta monta a requisição — {id} no caminho, q na query, o resto como corpo JSON — e a passa pelo app.Handler(), cadeia incluída: o limite de taxa, a chave de API, a auditoria e o log veem a mesma requisição que um curl mandaria. O Authorization, o X-Forwarded-For, o X-Request-ID e o Accept-Language de quem chamou viajam junto; o ator do registro de auditoria diz Via: "mcp". Um 4xx/5xx vira resultado com isError: true e o corpo como texto, para o modelo ler o 422 que o handler escreveu.

O tools/list é por chamador. Antes de listar uma ferramenta o servidor sonda a rota com os cabeçalhos de quem chamou — a cadeia roda, o handler não — e uma rota que responderia 401 ou 403 não entra na lista. Uma chave com docs:read vê as leituras; as escritas não ficam escondidas atrás de uma recusa, elas não existem. A sonda não gasta token do limite de taxa nem conta uso (ver App.Probe).

O que fica de fora, com o motivo no log e no trilha mcp --from-routes: páginas (page.go), rotas fora do Include, OPTIONS/HEAD e handler cujo corpo é multipart — a ponte manda JSON. Include e Exclude são prefixos com * no fim ou caminhos exatos.

Preview é o mesmo plano sem app: entram o documento, as rotas e as opções; saem a lista e os avisos — o que a CLI imprime.

Transporte próprio#

Transport é uma interface (Send, Recv, Close). Pipe(r, w, closer) monta o transporte de linha sobre qualquer par leitor/escritor, o que os testes usam com io.Pipe.