agent-package — a pasta src/mcp/

O gateway MCP do motor de agentes: o adaptador que transforma as tools built-in da casa e os MCPs externos cadastrados num endpoint /mcp único, atrás do protocolo Model Context Protocol.

Caminho: agent-package/backend/src/mcp/ · norma: PADRAO-MCP.md (raiz do repo) · decisão: ADR-016 · catálogo gerado: docs/mcp/tools.md

16 arquivos .ts 3.399 linhas 236 tools built-in 41 arquivos de fora importam dela medido em 2026-10-26

1 · Em uma frase

Esta pasta é o coração do /mcp: recebe tools/list e tools/call do protocolo, decide quem pode ver/chamar o quê, roda a tool (ou encaminha para um servidor MCP externo), mede, audita e sela a resposta no envelope único da casa.

Ela não é o servidor HTTP (isso é src/mcp-server.ts + src/routes/mcp.ts), não é o registro das tools (isso mora em src/tools/** e entra por registry.ts), e não guarda estado: o gateway é stateless, cada request monta o seu servidor mcp-server.ts:136. Duas famílias de tool convivem aqui:

famíliao que écomo sai
builtin as 236 tools da casa (core + github + render), declaradas com defineTool/renderTool em src/tools/** sempre com o envelope { ok, data|error, meta }
proxied tools de MCPs externos cadastrados por projeto (o toolset proxied) intacta, sem envelope — como o upstream respondeu, salvo o aviso textual anexado quando a sessão do upstream reinicia no meio da chamada e o resultado de falha sintetizado proxy-call.ts:53

2 · Mapa dos arquivos

16 arquivos, 3.399 linhas. A leitura natural é por camada: quem declara, quem cataloga, quem despacha, quem observa, e as faces do protocolo (resources, prompts, instructions).

camadaarquivolinhasexportspapel
declaração + envelopetool-kit.ts43424 módulo-folha: defineTool, os helpers do envelope (jsonContent/errorContent/failureContent) e o sealEnvelope :283
declaração (toolsets)toolsets.ts35319 registro dos toolsets e a resolução por requisição (header/query/default, cortada pelo teto da chave)
registroregistry.ts532 array único das 236 built-ins com o toolset carimbado :44-48
catálogocatalog.ts59018 o que tools/list publica e o que tools/call acha: built-in + proxied, filtro por toolset, escopo e projeto
despachotool-call.ts1762 callCatalogTool: projeto default, instrumentação, selo do envelope, auditoria. O único ponto que carimba nome+versão no envelope selado (o catálogo também conhece os dois ao publicar o _meta, catalog.ts:181-192)
proxiedproxy-call.ts1281 callProxied: repassa ao servidor de origem pelo pool, devolve o resultado intacto
autorizaçãotool-access.ts14511 ToolCaller, escopos, e o contexto da chamada em curso via AsyncLocalStorage
envelope/errotool-envelope.ts861 toolError :42: toda exceção vira { ok: false, error } com código estável
observabilidadeinstrument.ts2447 duração por tool (mcp_tool_duration_seconds), a linha de log [MCP] tool call e o errorId das falhas internas
observabilidademetrics.ts1145 o nível de baixo: requisições JSON-RPC, duração HTTP, cache e pool (complementa o instrument.ts, não sobrepõe)
recursos MCPresources.ts45120 resources/list|read, templates de URI e completion/complete: skills, knowledge, memórias e prompts de projeto
prompts MCPprompts.ts2215 prompts/list|get: executar-task e handover, instruções em pt-BR com o id real embutido
instructionsinstructions.ts283 o texto curto (< 1200 chars) que o initialize devolve — o primeiro texto que o agente lê
meta-toolsmeta-tools.ts1836 agentpack_search_tools e agentpack_describe_tool: a progressive disclosure que mantém o tools/list curto
apoiojson-schema.ts571 zod → JSON Schema publicado, quebrando ciclos de $ref
apoiodefault-project.ts1368 projeto padrão: o projectId do header (ou da chave de um projeto só) preenche o argumento quando ele é obrigatório
Fora da pasta, mas parte do mesmo assunto: src/mcp-server.ts (250 linhas) é o entrypoint do protocolo — registra os handlers do SDK; src/tools/render/common.ts (136 linhas) declara a “segunda forma” de tool, o renderTool das 62 tools do Render.

3 · O fluxo de um tools/call

routes/mcp.ts ── createMcpServer() ──▶ mcp-server.ts:136
                                            │
                       handler de tools/call mcp-server.ts:159
                       acha a entrada no catálogo (:163)
                                            ▼
                          callCatalogTool  tool-call.ts:53
                                            │
          ┌─────────────────────────────────┴─────────────────────────────────┐
   entry.kind === "builtin"                                          entry.kind === "proxied"
          │                                                                       │
  instrumentToolCall (tool-call.ts:72)                                  instrumentToolCall (:76)
          │                                                                       │
   callBuiltin (:153) — roda DENTRO do callback                     callProxied (proxy-call.ts:53)
   ├─ checkToolAccess (:166) ─ negado → toolError (:167)               resultado INTACTO (sem selo;
   ├─ args.parse (:169)                                                  salvo aviso/falha)
   ├─ handler (:170)
   └─ catch → toolError (:174) ◀── tool-envelope.ts:42
          │
   seal (:97) → sealEnvelope (tool-kit.ts:283)
   meta = { tool: entry.name, version: entry.tool.version } (:98)
          │
   auditToolCall (:79) — pula se isError (:139) ou readOnlyHint (:140)
  1. O servidor é por request. routes/mcp.ts chama createMcpServer mcp-server.ts:136 — um Protocol do SDK serve um transporte por vez e o transporte é stateless (por isso tools/listChanged é false, mcp-server.ts:147-149). O caro e compartilhado é o catálogo pré-computado, não o servidor.
  2. O handler acha a entrada e delega. tools/call é registrado com um schema que só prende o método mcp-server.ts:90; a entrada sai do catálogo :163 e o despacho é callCatalogTool :168.
  3. No despacho tool-call.ts:53: preenche o projeto default :59-62 e bifurca em built-in ou proxied :67-78.
  4. Ordem real no caminho built-in: instrumentToolCall envolve o callback :72-74, e dentro dele correm checkToolAccess :166 (negado → toolError :167, antes de parsear argumentos), o parse do zod :169 e o handler :170. A ordem cronológica é instrumenta → checa → executa → sela.
  5. Exceção é capturada no despacho, não no handler. O try/catch de callBuiltin :168-175 converte tudo em toolError tool-envelope.ts:42, que classifica (ZodError → VALIDATION_ERROR, AppError → o próprio código, Prisma P2025/P2003, resto → INTERNAL_ERROR), avisa a instrumentação (de onde sai o errorId) e monta o corpo de falha.
  6. O selo. De volta ao despacho, a função privada seal tool-call.ts:97 chama sealEnvelope :101 (definida em tool-kit.ts:283) com meta = { tool, version } :98 — o único ponto que carimba as duas no envelope selado (o catálogo também as publica em _meta, mas não sela resposta).
  7. Auditoria. auditToolCall :79 grava linha em audit_events só em escrita bem-sucedida: pula se isError :139 ou se a built-in é readOnlyHint :140. Toda proxied conta como escrita, sem exceção — anotação de servidor externo não é garantia.
  8. Fora do toolError: erro de protocolo do JSON-RPC passa pelo withInternalErrorGuard mcp-server.ts:232-245 (McpError segue; o resto vira -32603 com errorId).

4 · O envelope (TB-22 / ADR-016)

Antes da TB-22 conviviam seis formas de resposta. Hoje toda built-in responde com uma só, montada num lugar só (o despacho) — decisão registrada no ADR-016 e normatizada no PADRAO-MCP.md §2.

sucesso

{
  "ok": true,
  "data": { … o que a tool produziu … },
  "meta": {
    "tool": "agentpack_project_list",
    "version": "1.2.0"
  }
}

falha

{
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid tool arguments",
    "retryable": false, "hint": "…", "errorId": "…"
  },
  "meta": { … }
}
// + isError: true no resultado
regraonde está no código
forma do envelope e do erro tool-kit.ts:73-78 (ToolEnvelope) · :55-62 (ToolErrorBody)
isError = isError-do-produtor || !ok — o selo nunca descarta um isError já marcado; sucesso omite a chave tool-kit.ts:301 e :306
texto ≡ structuredContent: o selo re-serializa o bloco de texto a partir do envelope; imagens ficam na frente, blocos de texto extras nunca somem tool-kit.ts:286-299
meta.tool = nome de wire (agentpack_…), não o nome interno do registro catalog.ts:82 (prefixo) · :182 (monta) · tool-call.ts:98 (selo)
meta.version = a da tool chamada; declarada sem default em toda tool tool-call.ts:98 · tool-kit.ts:338 e :363
no tools/list a versão vai em _meta.version, não numa chave version no topo — o ToolSchema do SDK 1.32.1 não tem version e o zod dele apaga chave desconhecida catalog.ts:191 (publica) · :106-118 (o porquê) · SDK types.d.ts:2381/2407/2419
legacyBody (resultado montado à mão em vez de helper) fica fora do wire: é pista de log, vai em SealOutcome separado tool-kit.ts:225-230 · log em tool-call.ts:102-104
proxied não é envelopada: o outputSchema de origem é validado pelo cliente SDK contra o structuredContent; envelopar quebraria. O conteúdo passa intacto, salvo o aviso de restart e a falha sintetizada proxy-call.ts:53 · :81-92 (aviso) · :55 (falha) · comentário em tool-call.ts:14-17
tools com teto de resposta descontam a reserva do envelope do próprio orçamento tool-kit.ts:255 (ENVELOPE_RESERVE_BYTES)
guarda do bump: o contrato do envelope entra no hash de cada tool — mudou a forma, sobe a versão de todas tool-kit.ts:99-114 · tests/__snapshots__/mcp-tools.json (236 tools)

5 · Quem fala com quem

O que a pasta importa de fora

tipopara ondepeso
pacotes npm / node zod (7) · @modelcontextprotocol/sdk/types (6) · prom-client (2) · node:crypto (2) · node:async_hooks (2) · zod-to-json-schema · lru-cache · fastify (1, só tipo) lista fechada — zero require() e zero import() dinâmico
services 19 imports em 7 arquivos (resources, prompts, tool-call, proxy-call, catalog, tool-access, metrics) sim, a pasta importa services — ver nota abaixo
middleware / utils / lib / tools middleware (8) · utils/errors (4) · lib (3) · tools (3, só registry.ts:19-21) tipos de escopo, AppError, métricas e o próprio registro das tools
Correção de um erro comum sobre esta pasta: diz-se que “mcp/ não importa service nenhum”. Isso é verdade só para tool-kit.ts, que é de propósito um módulo-folha — importa zod e dois tipos tool-kit.ts:29-31, para que tools/**, services e testes dependam dele sem fechar ciclo com mcp-server.ts. A pasta como todo importa 19 services: o despacho usa auditoria e proxy tool-call.ts:28-30, e resources/prompts usam os services de dados resources.ts:37-42.

Grafo interno (acíclico)

base (não importam nada de dentro):
  tool-kit · toolsets · instructions · json-schema · resources

camadas em cima:
  tool-access ─▶ tool-kit
  registry     ─▶ tool-kit, toolsets          default-project ─▶ tool-kit
  instrument   ─▶ tool-access
  catalog      ─▶ registry, tool-access, tool-kit, toolsets, default-project, json-schema
  tool-envelope─▶ instrument, tool-kit        meta-tools ─▶ tool-access, tool-kit, toolsets
  metrics      ─▶ catalog                     prompts ─▶ instructions, resources, toolsets
  proxy-call   ─▶ catalog, instrument, tool-envelope, tool-access, tool-kit
  tool-call     ─▶ catalog, proxy-call, instrument, tool-envelope, tool-access, default-project, tool-kit

sinks (nada dentro de mcp/ importa): tool-call · meta-tools · metrics · prompts

Quem importa desta pasta

41 arquivos fora de src/mcp/ importam dela: src/tools/ 30 (core 10, github 11, render 8, raiz 1) · src/services/ 7 · src/routes/ 2 (health.ts, mcp.ts) · src/middleware/ 1 (authz.ts) · src/mcp-server.ts. É por isso que a pasta é o centro de gravidade do app: quase tudo que é tool passa por aqui.

6 · Design vs. dívida

Regra da casa ao ler esta pasta: separe o que foi decidido do que está em aberto. A lista canônica das pendências é o PADRAO-MCP.md §8 “O que ainda está em aberto” PADRAO-MCP.md:384 — os itens 1–5, 8, 9 e 10 são dívida; o 6 é decisão registrada; o 7 é decisão de produto.

Design — decisões registradas

  • Envelope único + versão por tool (ADR-016, TB-22): uma forma de resposta, um lugar que a sela, versão declarada sem default.
  • Proxied sem envelope — não é pendência, é decisão (§8.6): envelopar quebraria a validação do outputSchema de origem. Envelopar é bug, não avanço.
  • Versão em _meta e não no topo — o zod do SDK apaga chave fora do schema (catalog.ts:106-118).
  • Servidor por request, stateless: nada de sessão no gateway; ativação de toolset é sempre por requisição.
  • Auditoria só em escrita bem-sucedida; proxied conta sempre como escrita (tool-call.ts:139-140).
  • Strictness divergente entre defineTool e renderTool é decisão de produto (§8.7), não refatoração pendente: unificar muda o contrato do que o agente pode mandar.

Dívida — em aberto (§8 do PADRAO-MCP.md)

  • 1 Sem contagem de queries: existe latência, não contagem. mcp_tool_duration_seconds não tem nada de query instrument.ts:105-112; o $on("query") do Prisma só loga e não conta por tool lib/prisma.ts:77-82.
  • 2 outputSchema publicado: 0 de 236. O descritor é montado sem ele catalog.ts:183-192 (o campo é só para proxied, :102-103). Sem isso o envelope é cumprimento interno, não contrato verificável pelo cliente.
  • 3 Fronteira de lint incompleta: a regra services-below-tools não lista src/mcp/ como alvo eslint.config.js:143 — 7 services importam mcp/ sem serem barrados. (src/mcp/** é coberto como sujeito pela regra do Prisma :129.)
  • 4 Duas funções de declarar: defineTool tool-kit.ts:379 (174 call sites) e renderTool tools/render/common.ts:107 (54 call sites → 62 tools). A diferença de comportamento que resta é a strictness: z.object(input) descarta campo a mais tool-kit.ts:381, .strict() recusa common.ts:108.
  • 5 Cast e revalidação dentro de handler: 9 casts + 1 revalidação de sub-schema medidos no §6.2 — violam “nada de args.x as string” tool-kit.ts:8-12.
  • 8 O selo está coberto por um recorte, não pelas 236: tests/mcp-envelope-seal.test.ts prova a forma em 5 casos; não varre o catálogo inteiro.
  • 9 LooseJson = any no leitor de corpo de teste tests/tool-envelope.ts:29 (custo de remover medido: 170 erros de tipo em 19 arquivos de teste).
  • 10 23 description prometem a forma velha (o objeto no topo, sem o nível data) em 7 arquivos de src/tools — grep -rn "Returns {" src/tools volta exatamente 23.
Nota de frescor das citações: as linhas citadas no PADRAO-MCP.md desdriftaram com as correções da TB-22 e foram sincronizadas com o código em 2026-10-26 — dois lotes, 33 citações no total (13e23b0 e d8e8c54); nesta data elas batem. Vale a regra geral do documento: confira a linha no código antes de citar, porque citação envelhece calada.

7 · Para mexer aqui

tarefao que fazer
criar uma tool defineTool em src/tools/core/<dominio>.ts — title, as 4 annotations (use HINTS) e version são obrigatórios, sem default tool-kit.ts:8-19
responder jsonContent / errorContent / failureContent — nunca corpo à mão: um ToolResult sem helper vira legacyBody + warn no log tool-call.ts:102-104
mudar o schema publicado sobe a version da tool e roda npm run test:tool-snapshot — o snapshot único reprova hash novo com versão velha tests/mcp-tool-versions.test.ts
documentar as tools npm run docs:gen gera docs/mcp/tools.md a partir do catálogo — não edite à mão (docs:check reprova divergência)
ler a norma PADRAO-MCP.md (a casa) · ADR-016 (a decisão do envelope) · §8 para o que está em aberto