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ília | o 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).
| camada | arquivo | linhas | exports | papel |
|---|---|---|---|---|
| declaração + envelope | tool-kit.ts | 434 | 24 | módulo-folha: defineTool, os helpers do envelope
(jsonContent/errorContent/failureContent) e o
sealEnvelope :283 |
| declaração (toolsets) | toolsets.ts | 353 | 19 | registro dos toolsets e a resolução por requisição (header/query/default, cortada pelo teto da chave) |
| registro | registry.ts | 53 | 2 | array único das 236 built-ins com o toolset carimbado :44-48 |
| catálogo | catalog.ts | 590 | 18 | o que tools/list publica e o que tools/call acha: built-in + proxied,
filtro por toolset, escopo e projeto |
| despacho | tool-call.ts | 176 | 2 | 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) |
| proxied | proxy-call.ts | 128 | 1 | callProxied: repassa ao servidor de origem pelo pool, devolve o resultado intacto |
| autorização | tool-access.ts | 145 | 11 | ToolCaller, escopos, e o contexto da chamada em curso via
AsyncLocalStorage |
| envelope/erro | tool-envelope.ts | 86 | 1 | toolError :42: toda exceção vira
{ ok: false, error } com código estável |
| observabilidade | instrument.ts | 244 | 7 | duração por tool (mcp_tool_duration_seconds), a linha de log
[MCP] tool call e o errorId das falhas internas |
| observabilidade | metrics.ts | 114 | 5 | o nível de baixo: requisições JSON-RPC, duração HTTP, cache e pool
(complementa o instrument.ts, não sobrepõe) |
| recursos MCP | resources.ts | 451 | 20 | resources/list|read, templates de URI e completion/complete:
skills, knowledge, memórias e prompts de projeto |
| prompts MCP | prompts.ts | 221 | 5 | prompts/list|get: executar-task e handover,
instruções em pt-BR com o id real embutido |
| instructions | instructions.ts | 28 | 3 | o texto curto (< 1200 chars) que o initialize devolve — o primeiro
texto que o agente lê |
| meta-tools | meta-tools.ts | 183 | 6 | agentpack_search_tools e agentpack_describe_tool: a
progressive disclosure que mantém o tools/list curto |
| apoio | json-schema.ts | 57 | 1 | zod → JSON Schema publicado, quebrando ciclos de $ref |
| apoio | default-project.ts | 136 | 8 | projeto padrão: o projectId do header (ou da chave de um projeto só)
preenche o argumento quando ele é obrigatório |
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)
- O servidor é por request.
routes/mcp.tschamacreateMcpServermcp-server.ts:136 — umProtocoldo SDK serve um transporte por vez e o transporte é stateless (por issotools/listChangedéfalse, mcp-server.ts:147-149). O caro e compartilhado é o catálogo pré-computado, não o servidor. - 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. - No despacho tool-call.ts:53: preenche o projeto default :59-62 e bifurca em built-in ou proxied :67-78.
- Ordem real no caminho built-in:
instrumentToolCallenvolve o callback :72-74, e dentro dele corremcheckToolAccess: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. - Exceção é capturada no despacho, não no handler. O
try/catchdecallBuiltin:168-175 converte tudo emtoolErrortool-envelope.ts:42, que classifica (ZodError →VALIDATION_ERROR, AppError → o próprio código, PrismaP2025/P2003, resto →INTERNAL_ERROR), avisa a instrumentação (de onde sai oerrorId) e monta o corpo de falha. - O selo. De volta ao despacho, a função privada
sealtool-call.ts:97 chamasealEnvelope:101 (definida em tool-kit.ts:283) commeta = { 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). - Auditoria.
auditToolCall:79 grava linha emaudit_eventssó em escrita bem-sucedida: pula seisError:139 ou se a built-in éreadOnlyHint:140. Toda proxied conta como escrita, sem exceção — anotação de servidor externo não é garantia. - Fora do
toolError: erro de protocolo do JSON-RPC passa pelowithInternalErrorGuardmcp-server.ts:232-245 (McpError segue; o resto vira-32603comerrorId).
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
| regra | onde 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
| tipo | para onde | peso |
|---|---|---|
| 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 |
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
outputSchemade origem. Envelopar é bug, não avanço. - Versão em
_metae 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
defineToolerenderToolé 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_secondsnã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
outputSchemapublicado: 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-toolsnão listasrc/mcp/como alvo eslint.config.js:143 — 7 services importammcp/sem serem barrados. (src/mcp/**é coberto como sujeito pela regra do Prisma :129.) - 4 Duas funções de declarar:
defineTooltool-kit.ts:379 (174 call sites) erenderTooltools/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.tsprova a forma em 5 casos; não varre o catálogo inteiro. - 9
LooseJson = anyno 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
descriptionprometem a forma velha (o objeto no topo, sem o níveldata) em 7 arquivos desrc/tools—grep -rn "Returns {" src/toolsvolta exatamente 23.
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
| tarefa | o 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 |