O diagnostico e a implementacao da correcao do proxy, renderizados do markdown de origem.
2026-10-13 · sandbox
agent-package-audit· código deorigin/main26268c0 (é o que roda em produção:agentpack-backend-wsestá em 26268c0) · nenhuma linha do repo foi alterada.
| 0 | chamadas a tool proxied em produção nas últimas 45 h | 262 tool calls no gateway, 100% built-in (kind: builtin) |
| 4 | falhas do proxy em produção, todas no REA | EGRESS_TIMEOUT em 10 s (testar conexão) e 15 s (descobrir tools), 12/10 10:21–10:24 |
| 54 | agentes chegando no /mcp do AgentPack com o protocolo 2026-07-28 | 54 de 54 recusados com 400 e caíram para 2025 com sucesso: sem impacto hoje |
| 36 | passos do proxy atual contra 8 servidores de laboratório | 3 de 3 falham no servidor só 2026-07-28 · 3 de 3 no SSE legado · a sessão se perde em 3 situações |
| 18 | passos do cliente oficial v2 (o do Motor) nos mesmos servidores | conecta em 7 de 8 cenários (o 8º é o SSE legado pelo transporte errado, de propósito); só não refaz a sessão sozinho no 404 |
O QUE QUEBRA HOJE (proxy atual, SDK v1 1.30.1) IMPACTO
────────────────────────────────────────────────────── ───────────────────────────────
MCP que só fala 2026-07-28 (MCP do MyPanel) ██████████ MCP inteiro: 0 tools, toda chamada falha
MCP com sessão (browsers) ███████░░░ funciona até: 5 min parado, 1 chamada > 60 s,
1 erro HTTP, deploy · e mistura conversas
MCP SSE legado (/sse) ██████████ MCP inteiro: não conecta
MCP só de stdio (REA) via ponte █████░░░░░ depende da ponte: hoje fora do ar (503)
Empresa A x empresa B ███░░░░░░░ catálogo de MCP é global (segurança, não "não funciona")
diagrama · fonte mermaid
flowchart LR
A[Agente<br/>Claude Code, Motor] -->|POST /mcp<br/>sem Mcp-Session-Id| G[Gateway /mcp<br/>STATELESS<br/>routes/mcp.ts:102]
G --> S[createMcpServer<br/>um por requisição]
S -->|tools/call nome com prefixo| C[catalog.ts findTool<br/>toolset proxied]
C --> P[proxy-call.ts callProxied<br/>escopo = principal + projeto]
P --> K{Pool LRU<br/>chave = sha256 de URL,<br/>headers com segredos, escopo<br/>TTL ocioso 5 min, teto 100}
K -->|não existe| N[openPooled<br/>Client SDK v1 1.30.1<br/>initialize 2025-11-25]
K -->|existe| R[reusa a sessão<br/>Mcp-Session-Id do destino]
N --> U[(MCP de destino)]
R --> U
K -. ocioso 5 min, teto, qualquer erro, deploy .-> D[DELETE da sessão<br/>a janela do browser some]
diagrama · fonte mermaid
sequenceDiagram
participant Ag as Agente (chave A, projeto P1)
participant GW as Gateway /mcp
participant PX as Pool do proxy
participant BR as MCP com sessão (browsers)
Ag->>GW: tools/call create_automation_session
GW->>PX: escopo agent:A|project:P1
PX->>BR: initialize (sessão S1) + tools/call
BR-->>Ag: janela criada na sessão S1
Note over Ag,BR: outra conversa da MESMA chave e projeto cai na MESMA S1 (janela compartilhada)
Ag->>GW: tools/call navigate (6 min depois)
Note over PX: S1 ficou 5 min parada: DELETE S1
PX->>BR: initialize (sessão S2) + navigate
BR-->>Ag: isError "sem sessão de automação" (estado perdido, sem aviso)
| Hipótese | Veredito | Confiança | Prova | |
|---|---|---|---|---|
| H1 | MCP com estado: gateway stateless não amarra sessão | Confirmada em parte. Não abre sessão nova a cada chamada: o pool guarda uma sessão por (chave, projeto). Mas a sessão (a) é compartilhada por todas as conversas da mesma chave e projeto, (b) morre em silêncio com 5 min parada, com qualquer chamada acima de 60 s, com erro HTTP e a cada deploy | alta | lab sessao, ocioso, timeout; mcp-proxy.ts:169, 687-700, proxy-call.ts:54-57 |
| H2 | Sessão expirada (404) sem novo initialize | Refutada. O proxy refaz o initialize uma vez e a chamada passa. Resto: o estado volta do zero sem aviso ao agente | alta | lab expira (count volta a 1); mcp-proxy.ts:575-578, 695 |
| H3 | Versão de protocolo: só 2026-07-28 | Confirmada. O proxy usa o SDK v1 1.30.1, que só conhece até 2025-11-25 (nem o v1 mais novo, 1.32.1, fala 2026-07-28). Servidor só 2026-07-28 responde 400 com JSON-RPC -32022 no initialize | alta | lab versao; package.json:53; motor mcp-client.test.ts:4-7 (MCP do painel recusava o v1 com -32022) |
| H4 | Transporte: SSE legado e stdio por ponte | Confirmada. SSE legado não é falado (404/400 no initialize). Ponte do REA: as 4 falhas de produção têm a assinatura exata da ponte sem reescrita de id; hoje a ponte está fora do ar (503) | alta (SSE) · média-alta (REA) | lab sse, ponte; logs de produção 12/10 10:21–10:24; schemas/mcp.ts:16-21 |
| H5 | Pool chaveado só por URL e headers vaza entre empresas | Refutada no pool, confirmada no catálogo. A chave do pool inclui o principal e o projeto (chave B recebeu sessão própria). Mas McpServer não tem empresa: qualquer usuário lista, edita e vincula qualquer MCP, e as credenciais dele vão junto | alta (código) | lab sessao passo B; schema.prisma model McpServer sem organizationId; mcp.service.ts:183-200, 514-536 |
backend/package.json:53 fixa @modelcontextprotocol/sdk 1.30.1; SUPPORTED_PROTOCOL_VERSIONS = 2025-11-25 … 2024-10-07. O v1 mais novo (1.32.1, 05/10) também não tem 2026-07-28: o caminho é o pacote v2 @modelcontextprotocol/client (2.3.1) com versionNegotiation: { mode: 'auto' }, como o Motor fez.services/mcp-proxy.ts:384-400 cria Client + StreamableHTTPClientTransport do v1; o connect manda initialize 2025-11-25.HTTP 400 {"code":-32022,"message":"Unsupported protocol version: 2025-11-25","data":{"supported":["2026-07-28"]}}.logSafeError corta o corpo do upstream de propósito.fetch sobre o safeFetchStream real) e falou com os 4 tipos de servidor (run-v2-safefetch.jsonl).routes/mcp.ts:123-126): não existe sessão de quem chama. O proxy aproxima com o escopo principal|projeto (mcp/proxy-call.ts:54-57, mcp/tool-call.ts:45).services/mcp-proxy.ts):PROXY_POOL_IDLE_TTL_MS (169) → closeConnection → DELETE (411-442).this.pool.evict(key, lease) em todo catch (692-696), inclusive EGRESS_TIMEOUT de chamada acima de 60 s (callMs, 161-166) e resultado fora do schema.mcpProxy.close() no onClose (routes/mcp.ts:96-99) manda DELETE em todas.create_session → navigate funciona no mesmo escopo; depois de ocioso ou de um timeout, navigate volta "No automation session"; a "segunda conversa" recebeu "reused window" da primeira.GET /sse + POST /messages): "not spoken" (mcp-proxy.ts:68-71); sse deixou de ser gravável (schemas/mcp.ts:16-21). A tela oferece "Streamable HTTP" e "HTTP (Legado)" (McpTransportSection.tsx:13-28), que são o MESMO transporte: o rótulo engana. O v2 ainda tem SSEClientTransport (lab: conecta)./workspace/rea-bridge.mjs (sandbox rea-mcp-proxy, modificada às 10:31 de 12/10) diz no topo que sem reescrever o id "os clientes esperam um id que nunca chega". As 4 falhas de produção (10:21–10:24) são exatamente isso: lab sem reescrita = EGRESS_TIMEOUT em 10.003 ms e 15.003 ms, produção = 10.001–10.004 ms e 15.002 ms. Com a reescrita, a ponte funciona no lab.https://sb-rea-mcp-proxy-p6402.mp.serendiped.com/health = 503. E a ponte divide UM processo REA entre todos (lab: contador da chave B continuou o da chave A).prisma/schema.prisma model McpServer: sem organizationId. mcpService.list (183-200) e linkToProject (514-536) não filtram empresa. Um usuário da empresa B vê o MCP da empresa A, vincula ao projeto dele e os agentes de B chamam o destino com a credencial de A.useMcpForm.ts:232-234: no criar, só sincroniza as tools se o preview achou alguma; no editar (URL, headers) não sincroniza. MCP salvo com 0 tools não publica nada no gateway.https e só endereço público (lib/safe-fetch.ts): http:// ou nome interno da rede Docker viram EGRESS_BLOCKED.Fonte: services_logs do agentpack-backend-ws, 13.319 linhas = todo o container atual, de 11/10 08:27 a 13/10 05:16 UTC.
| UTC | Rota | Resultado | Destino | Erro |
|---|---|---|---|---|
| 12/10 10:21:47 | POST /api/mcps/test-connection-preview | 504 em 10.004 ms | sb-rea-mcp-proxy-p6402.mp.serendiped.com | EGRESS_TIMEOUT 10000 ms |
| 12/10 10:21:59 | POST /api/mcps/test-connection-preview | 504 em 10.001 ms | idem | EGRESS_TIMEOUT 10000 ms |
| 12/10 10:22:16 | POST /api/mcps/preview-tools | 504 em 15.002 ms | idem | EGRESS_TIMEOUT 15000 ms |
| 12/10 10:24:34 | POST /api/mcps/test-connection-preview | 504 em 10.001 ms | idem | EGRESS_TIMEOUT 10000 ms |
| 12/10 16:07:48 | POST /api/mcps/test-connection-preview | 200 (destino não logado) | ? | sem erro registrado |
| 11/10 a 13/10 | POST /mcp | 54 × 400 | gateway do próprio AgentPack | Unsupported protocol version: 2026-07-28; o POST seguinte do mesmo cliente deu 200 nos 54 casos |
[MCP] tool call: 262 linhas, 0 com kind: proxied. Nenhum POST /api/mcps (cadastro) e nenhum sync-tools no período.Pasta /workspace/.sbcache/wf-mcp/lab (fora do repo). Servidores com o SDK oficial em [ip-interno]; o proxy é o código de origin/main (worktree wf-baseline), com a política REAL do safeFetch e DNS falso (como tests/mcp-egress.ts).
| Servidor (porta) | SDK | Proxy atual (v1) | Cliente v2 auto |
|---|---|---|---|
| stateless (7105) | v1 | ✅ probe, discover, call | ✅ 2025-11-25 |
| com sessão, imita browsers (7101) | v1 | ✅ no mesmo escopo · ❌ estado perdido após ocioso e após timeout · ⚠️ 2ª conversa da mesma chave reusa a janela | ✅ sessão mantida no mesmo cliente |
| sessão que expira, 404 (7102) | v1 | ⚠️ passa, mas contador volta a 1 (estado zerado sem aviso) | ❌ lança SdkHttpError 404 (o proxy precisa manter o próprio retry) |
| só 2026-07-28 (7103) | v2 legacy: 'reject' | ❌ 3/3: 400, -32022 | ✅ 2026-07-28 |
| v2 duas eras (7107) | v2 padrão | ✅ cai para 2025-11-25 | ✅ 2026-07-28 |
| SSE legado (7104) | v1 SSEServerTransport | ❌ 3/3: 404 em /sse, 400 em /messages | ✅ só com SSEClientTransport |
| ponte REA com id reescrito (7106) | cópia da ponte + stdio v2 | ✅ (estado único para todas as chaves) | ✅ |
| ponte REA sem reescrever id (7108) | idem | ❌ EGRESS_TIMEOUT 10.003 / 15.003 ms = produção | não testado |
Prova da correção (C1): run-v2-safefetch.ts liga o cliente v2 com versionNegotiation auto a um adaptador fetch sobre o safeFetchStream real (GET standalone respondido com 405 localmente, como o proxy faz): 4 de 4 conectaram e chamaram tool (2026-07-28 no só-moderno, 2025-11-25 nos demais, com sessão no 7101). Custo: um POST a mais (server/discover) por conexão nova em servidor 2025.
Rodar de novo (servidores primeiro, depois os clientes):
cd /workspace/.sbcache/wf-mcp/lab && PATH=/workspace/.node/bin:$PATH LAB_TTL_MS=600000 node servers.mjs &
PATH=/workspace/.node/bin:$PATH BRIDGE_PORT=7106 node rea-bridge-lab.mjs & BRIDGE_PORT=7108 BRIDGE_REWRITE=0 node rea-bridge-lab.mjs &
cd /workspace/.worktrees/wf-baseline/backend && NODE_ENV=development DATABASE_URL=postgresql://[redigido] \
node_modules/.bin/tsx /workspace/.sbcache/wf-mcp/lab/run-proxy.ts stateless,sessao,ocioso,timeout,expira,versao,sse,ponte
cd /workspace/.sbcache/wf-mcp/lab && node run-v2.mjs
Saídas: proxy-v1-todos.jsonl (36 passos), run-v2.jsonl (18), run-v2-safefetch.jsonl (4), servers.log e bridge.log (cada requisição HTTP com rpc, sessão abreviada e versão). Todos os processos do lab foram encerrados no fim.
| Onde | O quê |
|---|---|
backend/src/routes/mcps.ts:103-110 | cadastro (POST /api/mcps), só usuário |
backend/src/routes/mcps.ts:183-235 | sync-tools, test-connection, preview-tools, test-connection-preview (discoverTools e probeConnection) |
backend/src/routes/mcps.ts:250-294 | vínculo com projeto e filtros de tools |
backend/src/services/mcp.service.ts:258, 473, 600, 624 | create, replaceTools (o que o gateway publica), listActiveServers, listActiveServersForProject |
backend/src/services/mcp-headers.ts:336 | buildUpstreamConfig: config.headers + segredos decifrados + override do projeto |
backend/package.json:53 | @modelcontextprotocol/sdk 1.30.1 (cliente E servidor) |
backend/src/services/mcp-proxy.ts:384-400 | cria Client v1 + StreamableHTTPClientTransport com o fetch do egress |
backend/src/services/mcp-proxy.ts:268-296 | upstreamFetch: safeFetchStream, GET standalone = 405 local, captura Mcp-Session-Id |
backend/src/services/mcp-proxy.ts:606-611 | chave do pool: sha256 de URL, headers (com segredos) e escopo |
backend/src/mcp/proxy-call.ts:54-57 · mcp/tool-call.ts:45 | escopo da sessão: tipo:id do principal + project:<id do header, ou o projeto padrão da chave, ou *> |
backend/src/services/mcp-proxy-pool.ts:38-103 | LRU com TTL ocioso (5 min), teto (100), leases e dispose |
backend/src/services/mcp-proxy.ts:740-748 | sessão nasce: openPooled → connect (initialize) |
backend/src/services/mcp-proxy.ts:411-442, 692-696 | sessão morre: DELETE no dispose; evict em QUALQUER erro |
backend/src/services/mcp-proxy.ts:575-578, 687-700 | 404 → nova sessão, uma vez |
backend/src/services/mcp-proxy.ts:161-166, 450-478 | prazos (15 s handshake/página, 60 s call, 10 s probe, 5 s DELETE) e withDeadline |
backend/src/services/mcp-proxy-errors.ts:52-62, 80-101 | upstreamFailure (HTTP → MCP_UPSTREAM_ERROR) e logSafeError (corta o corpo) |
backend/src/mcp/proxy-call.ts:27-47, 68-86 | callProxied e o isError "Proxy error for <tool> (<CODE>)" |
backend/src/mcp/catalog.ts:274-305, 357-365, 409-451, 522-533 | tools proxied no gateway: <toolPrefix>_<tool>, lista, busca, escopo por projeto/chave |
backend/src/mcp/toolsets.ts:40, 99-104 | toolset proxied, ligado por padrão |
backend/src/routes/mcp.ts:102-146 | gateway stateless (sessionIdGenerator: undefined), GET/DELETE = 405 |
backend/src/schemas/mcp.ts:16-21 | transportes graváveis: http, streamable-http (SSE só leitura) |
frontend/src/features/mcps/components/McpTransportSection.tsx:13-28 | opções da tela ("HTTP Legado" = mesmo transporte) |
frontend/src/features/mcps/components/useMcpForm.ts:182-183, 232-234 | toast "Falha na conexão (status N)"; sync só no criar e só se o preview achou tools |
backend/tests/mcp-proxy.test.ts:655-735, 742-756 | testes do pool, do 404 e da versão não suportada (hoje trava "connected: false") |
Client/transport de services/mcp-proxy.ts por @modelcontextprotocol/client 2.3.1 com versionNegotiation: { mode: 'auto' }, mantendo o upstreamFetch (o transport v2 aceita fetch), o withDeadline e o pool. Mapear SdkHttpError.status (404 para o retry, demais para MCP_UPSTREAM_ERROR) e -32022 para um código próprio com mensagem clara. Guardar a era descoberta por URL (opção prior do v2) para não sondar a cada conexão. O servidor do gateway pode ficar no v1 (os clientes novos caem para 2025 sem erro). Teste novo: upstream createMcpHandler(..., { legacy: 'reject' }).EGRESS_TIMEOUT (cancelar a requisição e manter a sessão) nem em resultado fora do schema; prazo e TTL ocioso configuráveis por MCP (o browser precisa de mais de 5 min e de chamadas perto de 60 s); avisar o agente quando a sessão foi refeita (bloco de texto ou _meta: "a sessão do MCP foi reiniciada; o estado anterior, como a janela do browser, se perdeu").SSEClientTransport do v2 para linhas sse (e voltar a opção na tela, com rótulo certo); a ponte do REA precisa rodar como serviço supervisionado, não como processo solto num sandbox.MCP_PROTOCOL_UNSUPPORTED e MCP_TRANSPORT_UNSUPPORTED com texto pt-BR na tela; sincronizar tools ao salvar URL ou headers.organizationId em McpServer vindo da Conta Nomad, filtro em lista, edição e vínculo. Frente própria (migration + regra entre apps).| Pergunta | Por quê | |
|---|---|---|
| D1 | Sessão por conversa? O gateway passaria a devolver um Mcp-Session-Id opaco no initialize (sem guardar nada no servidor) e o proxy usaria esse id no escopo | é o único jeito de duas conversas da mesma chave não dividirem a janela do browser; mexe na letra do ADR-011 ("sem Mcp-Session-Id"). Clientes 2026-07-28 não têm sessão e continuariam no escopo chave+projeto |
| D2 | Prazos padrão: TTL ocioso (hoje 5 min) e chamada (hoje 60 s) | trade-off entre memória/sessões penduradas e estado perdido |
| D3 | Catálogo de MCP por empresa agora ou depois | é mudança de modelo de dados e de regra entre apps |
| D4 | Onde roda a ponte do REA | hoje morre a cada reinício do sandbox |
Frente: MCP-proxy · Sandbox agent-package-audit · Worktree /workspace/.worktrees/fix-mcp-proxy-sessoes Branch fix/mcp-proxy-sessoes a partir de origin/main (26268c0) · HEAD e13f3e2 · 8 commits (ver secao 7: a revisao do round 1 achou 6 bloqueios, todos corrigidos no commit e13f3e2)
O proxy de MCP do AgentPack falhava por 5 causas (diagnostico em /workspace/.sbcache/wf-mcp/diagnostico.json). As 5 foram corrigidas, com as ajustes que a critica do plano mandou (em /workspace/.sbcache/wf-mcp/critica-plano.json).
@modelcontextprotocol/sdk 1.30.1 (so 2025). Um MCP que so aceita 2026-07-28 (como o do painel) falhava INTEIRO no initialize com HTTP 400 / -32022 e o MCP ficava sem tools.@modelcontextprotocol/client 2.3.1 (unico que fala 2026-07-28) com versionNegotiation: { mode: "auto" }: sonda server/discover, cai para o initialize de 2025 quando o servidor e dessa era. Veredito de era em cache por upstream (12 h, prior), e veredito velho que falha e sondado de novo. O gateway /mcp (lado servidor) continua no v1 de proposito (ADR-011).tools/call e validado pelo schema do v2 no era moderno e do v1 no legacy. Chamadas continuam por client.request() explicito com schema (o listTools() do v2 agrega com teto, o callTool() valida outputSchema — o repo nao quer nenhum dos dois).readOnlyHint); em escrita NAO repete (repetir pode aplicar o efeito duas vezes). Nos dois casos o resultado carrega o aviso pt-BR de sessao reiniciada.sessionIdGenerator nem estado no servidor: e so um header de resposta, um id por conversa; GET/DELETE continuam 405 e qualquer replica responde qualquer chamada. Aceita o header que o cliente mandar tambem; malformado cai no escopo sem sessao.sse nao era gravavel e o proxy tentava Streamable HTTP com todo cadastro sse (toda chamada falhava).sse volta a MCP_WRITABLE_TRANSPORTS; o proxy disca SSEClientTransport (GET /sse + POSTs no endpoint do evento endpoint), com prazo de 6 h e teto de 64 MiB so do stream (SSE_STREAM_MAX_BYTES, explicito — o DEFAULT de 5 MiB do safeFetch continua valendo para o POST). Estourar o teto encerra o stream e a sessao (contrato documentado).McpServer sem organizationId. Qualquer empresa listava, lia, editava e vinculava qualquer MCP; a credencial de uma ia para o agente de outra. O cache do catalogo usava a chave '*' para todas as empresas.McpServer.organizationId (nullable, FK Restrict, indice) + migration com backfill de empresa unica (regra D8: vinculo em projeto vivo de uma empresa so herda; duas empresas / sem vinculo / so lixeira viram plataforma NULL). Filtro em list/getById/update/delete/linkToProject/ listActiveServers/listActiveServersForProject. A chave do cache agora leva a empresa (org:<uuid>|…) — sem isso a B lia a entrada populada pela A.MCP_PROTOCOL_UNSUPPORTED (com a versao pedida e as suportadas que o upstream declarou) e MCP_TRANSPORT_UNSUPPORTED (502), traduzidos para pt-BR na tela (mcpErrors.ts). O "Testar conexão" devolve code. Sync de tools no update so quando o transporte muda (D7: nao em toda edicao de URL/header, que invalidaria o catalogo de todos os projetos por um discover nao pedido).MCP_PROXY_IDLE_TTL_MS (30 min, era 5), MCP_PROXY_CALL_TIMEOUT_MS (5 min, era 60 s), MCP_PROXY_MAX_CLIENTS (200, era 100). Gerados no .env.example.
bash
# worktree
cd /workspace/.worktrees/fix-mcp-proxy-sessoes/backend
export PATH=/workspace/.node/bin:$PATH
# banco local
bash /workspace/.sbcache/wf/pg-up.sh agentpack_wf_mcp_test
PG_WF_DB=agentpack_wf_mcp_test bash -c 'source /workspace/.sbcache/wf/pg-env.sh && npm run test:db:migrate'
# gates (tudo via heavy.sh: uma vaga, Node 24, NODE_OPTIONS=1536MB)
/workspace/.sbcache/wf/heavy.sh node node_modules/typescript/bin/tsc --noEmit
/workspace/.sbcache/wf/heavy.sh npm run lint:ratchet
/workspace/.sbcache/wf/heavy.sh npm run cycles
/workspace/.sbcache/wf/heavy.sh npm run knip
/workspace/.sbcache/wf/heavy.sh npm run build
/workspace/.sbcache/wf/heavy.sh npm run docs:check
# testes novos e tocados
PG_WF_DB=agentpack_wf_mcp_test bash -c 'source /workspace/.sbcache/wf/pg-env.sh && \
/workspace/.sbcache/wf/heavy.sh npx jest tests/mcp-proxy.test.ts --runInBand'
PG_WF_DB=agentpack_wf_mcp_test bash -c 'source /workspace/.sbcache/wf/pg-env.sh && \
/workspace/.sbcache/wf/heavy.sh npx jest tests/mcp-proxy-v2.test.ts --runInBand'
PG_WF_DB=agentpack_wf_mcp_test bash -c 'source /workspace/.sbcache/wf/pg-env.sh && \
/workspace/.sbcache/wf/heavy.sh npx jest tests/mcps.test.ts tests/mcp-ssrf.test.ts tests/mcp-transport.test.ts --runInBand'
# suite inteira, em shards (--runInBand)
for i in 1 2 3 4 5 6 7 8; do
PG_WF_DB=agentpack_wf_mcp_test bash -c 'source /workspace/.sbcache/wf/pg-env.sh && \
/workspace/.sbcache/wf/heavy.sh npx jest --runInBand --shard='$i'/8'
done
# frontend
cd ../frontend
/workspace/.sbcache/wf/heavy.sh npm run typecheck
/workspace/.sbcache/wf/heavy.sh npx vitest run src/test/components/mcps/
Resultados desta frente:
shutdown.test.ts no shard 4, exatamente as do baseline (/workspace/.sbcache/wf/baseline.json, backend:jest:shard-4/8). Nenhuma falha nova.tests/mcp-proxy-v2.test.ts (NOVO, 5 testes): upstream so-2026-07-28 via createMcpHandler({ legacy: "reject" }) — discovery e call funcionam; dual-era cai para 2025 sem erro; SSE legado v1 fala com transport: "sse"; Streamable contra destino so-SSE = HTTP 404 honesto; SSE contra destino so-Streamable = MCP_TRANSPORT_UNSUPPORTED. Os upstreams sao handles de node:http dentro do proprio arquivo, encerrados no afterAll.tests/mcp-proxy.test.ts (3 novos em "session per conversation"): duas conversas da mesma chave nao dividem a janela; 404 em leitura e repetido com aviso pt-BR; 404 em escrita NAO e repetido, com aviso. Mais: prazo nao derruba a sessao (poolSize 1 + reuso), teto do stream nao derruba a sessao, logSafeError com os tipos do v2.tests/mcp-transport.test.ts (2 novos): o header Mcp-Session-Id e so entrada (initialize nao o devolve, duas conversas chamam); valor malformado e ignorado.tests/mcps.test.ts (6 novos em "MCPs por empresa"): isolamento CRUD entre empresas (404, nunca 403), vinculo com projeto de outra empresa recusado, MCP de plataforma visivel as duas, 422 sem empresa, o cache do catalogo nao vaza (populado pela A, lido pela B), e o filtro do service em vinculo legado.server/discover a mais na primeira conexao de cada sessao de destino contra servidores 2025 (mitigada pelo cache de era).sse volta a ser selecionavel na tela.MCP_PROTOCOL_UNSUPPORTED, MCP_TRANSPORT_UNSUPPORTED. Quem casa por string de code precisa conhece-los. O teste de conexao devolve code junto com connected: false.mcp_tools sem o usuario pedir explicitamente nesse caso.20261013090000_mcp_server_organization: coluna nullable, FK Restrict, indice, backfill. Verificada sem drift (migrate diff vazio contra o schema; prisma migrate deploy limpo em banco novo).Mcp-Session-Id na borda). Com 1 replica (hoje) nada muda. Registrado no ADR-015./mcp para o v2 — muda negotiation, toolsets e semantica de sessao do lado servidor. So depois de o cliente estar em 2026-07-28 de verdade, ou junto, se houver prova de que o ganho e nulo.docs/ops/troubleshooting.md diz como religar; levar para um servico de verdade no painel, com health check e reinicio, e tarefa de infra do dono. Importante para o dono: o MCP do REA esta com /health = 503 agora (processo nao supervisionado sumiu no reinicio do sandbox). Nada deste merge o religa.Mcp-Session-Id pelo gateway nao existe (D1). Um cliente que mande o id de outra conversa cai no escopo dela (compartilhado de proposito); ator e projeto continuam vindo do token e do header, entao nao ha escalada. Se incomodar, a variante e validar o id contra um store, que quebra o stateless.no-unsafe-call, +14 no-unsafe-member-access, -1 require-await): sao os .body.* dos testes novos, no padrao ja usado por todos os testes existentes do repo. Erros: 0.npm run api:check nao foi commitado — o openapi.json gerado tinha so drift de outra frente (comentarios do agent-key), revertido. As rotas de MCP nao tem schemas zod no OpenAPI entao a API publica nao mudou.routes/render, mcp/tools/render e docs/adr: o merge vai precisar de rebase nos dois lados. Os 2 commits de feat/admin-agent-key no clone principal nao sao desta frente.e13f3e2) — os 6 bloqueiosA revisao adversarial do PR achou 6 bloqueios. Todos os 6 foram confirmados no fonte (nenhum era falso positivo) e corrigidos, cada um com a prova pedida.
| # | Onde | Sintoma confirmado | Correcao | Prova |
|---|---|---|---|---|
| B1 | mcp/proxy-call.ts escopo | callerSessionId ?? "*", mas o gateway nunca emitia Mcp-Session-Id e o SDK so ecoa um id devolvido no handshake (client/dist/index.mjs:5716 apaga o header do handshake, :5738 grava o da resposta) → todo mundo em session:* | routes/mcp.ts emite um id opaco no initialize (sendWebResponse) | 2 Client do SDK reais, sem header manual, ids diferentes e janelas distintas |
| B2 | mcp-proxy.ts connect | retry de era rodava client.connect() no mesmo transport → SDK v2 joga "already started!" (provado em lab-connect-retry.mjs) | reopen() troca Client+Transport antes da 2a tentativa | veredito legacy cacheado vs servidor so-2026 cai para 2026 |
| B3 | mcp-proxy.ts callTool catch | entrada com ready rejeitado ficava no pool; updateAgeOnGet renovava o TTL a cada tentativa → 30 min do mesmo erro | flag connected; evict quando !connected | handshake falha, upstream volta, a mesma chamada do mesmo escopo funciona |
| B4 | mcp-proxy.ts retry | mesma causa de B2: o erro virava invalid_response, diagnostico errado | resolvido por B2 (conexao nova) | com as 2 tentativas falhando o erro e o do upstream (http_status 500) |
| B5 | mcp-proxy.ts terminateSession | lia so transport.sessionId; quando o initialize falhava depois do upstream criar a sessao, o id nunca saia → janela aberta no upstream | streamSessionId(conn) ?? conn.http.sessionId (id capturado pelo egress) | initialize 500 com Mcp-Session-Id → upstream ve o DELETE (antes deletes=0) |
| B6 | routes/mcps.ts actorOrg | request.actor?.orgId dava undefined para qualquer principal sem empresa (= escopo do bootstrap): um apv_* (preview) ou apk_* com organization_id NULL lia o catalogo de MCP de todas as empresas | mcpOrgScopeOf() em mcp.service.ts, usada pelas rotas e por mcp/catalog.ts: so o bootstrap ve tudo, sem empresa ve so plataforma | GET /api/mcps com apv_* e com apk_* NULL devolve so as linhas de plataforma |
Decisao que mudou: B1 reverte a D1/ADR-015 ("nunca emitir"). O mecanismo nao exige sessionIdGenerator nem estado no servidor, e o gateway segue stateless (GET/DELETE 405, qualquer replica responde). E uma mudanca de contrato de fio do /mcp: os testes que travavam "initialize nao emite" foram atualizados. Vale o aceite do dono antes do deploy.
Nao feito de proposito: a defesa em profundidade que a revisao sugeriu no B6 (orgId no actor do token de preview + NOT NULL/backfill em agent_keys.organization_id). A correcao primaria ja fecha o vazamento exigido pelo teste, e um token de preview global (pid nulo) e legitimamente sem empresa.
1a67c15)A revisao adversarial da rodada 2 apontou 2 bloqueios, ambos da mesma classe que o B3 da rodada 1 (entrada morta envenenando o pool), e ambos foram confirmados no fonte antes de mexer. Os dois descrevem o mesmo defeito pelo seu angulo: o sse legado, reabilitado pela C3a desta branch, nunca tirava a entrada morta do pool.
isSessionExpired (mcp-proxy.ts:887) exige streamSessionId(conn) !== undefined, mas streamSessionId so le StreamableHTTPClientTransport.sessionId — o SSEClientTransport de 2024 nao tem Mcp-Session-Id, entao o predicado e inalcancavel nesse transporte. E isDeadTransport (mcp-proxy.ts:916) devolve false para QUALQUER SdkHttpError, que e o que o SDK levanta quando o POST em /messages e recusado. Resultado: quando o destino esquece a sessao (deploy, restart), toda chamada seguinte do MESMO escopo repete o mesmo erro ate MCP_PROXY_IDLE_TTL_MS (30 min) — e o updateAgeOnGet: true do LeasePool renova esse TTL a cada tentativa, entao so se cura para quem ficar quieto.
O erro tambem era mentiroso: o SSEClientTransport responde a um POST para o endpoint morto com um Error simples ("Error POSTing to endpoint (HTTP 404): ...", client/dist/index.mjs:5265), nao um SdkHttpError. Logo upstreamFailure caia no ramo final e devolvia MCP_UPSTREAM_ERROR / invalid_response ("The MCP server sent an invalid JSON-RPC response to tools/call") para o que e um HTTP 404 do /messages.
Prova do defeito: lab/r2-lifecycle-3.jsonl (K6 e K7) — 1a chamada ok (inits=1, poolSize=1), o destino esquece a sessao e as 3 chamadas seguintes falham com o MESMO erro, inits parado em 1 e poolSize parado em 1. O controle K8 (mesmo cenario no Streamable HTTP) se cura: inits=2.
Correcao:
mcp-proxy-errors.ts: ssePostRefusalStatus(err) extrai o status HTTP do Error do transporte SSE legado (o SdkHttpError nao existe nesse caminho).mcp-proxy.ts: sseSessionGone(err, conn) — so no sse, e so para os status 400/404/410 ("a sessao acabou"). 401/403 ficam de fora (credencial errada nao e sessao morta: repetir o handshake nao conserta).callTool: expired = connected && (isSessionExpired(...) || sseSessionGone(...)). O SSE ganha o mesmo tratamento do Streamable HTTP: evict + retry em leitura com o aviso pt-BR de sessao reiniciada (onSessionRestarted), e em escrita so o aviso (o efeito pode ter ocorrido).upstreamFailure: esse Error passa a classificar como http_status com o status registrado, em vez do invalid_response enganoso.Prova (verde): 2 testes novos em tests/mcp-proxy-v2.test.ts, contra um upstream SSE legado REAL (v1 SSEServerTransport) com dropSessions():
a dropped legacy SSE session reconnects on the next call instead of poisoning the pool — chamada 1 ok (inits=1); dropSessions(); a chamada 2 do MESMO escopo re-faz o handshake (inits=2), responde echo: b e o onSessionRestarted disparou com { retried: true }; a chamada 3 reusa a sessao nova (inits continua 2). Hoje ela falha (o pool fica envenenado e inits nunca sai de 1).a refused legacy SSE POST is an HTTP status, not 'invalid JSON-RPC response' — em escrita (readOnly: false) o erro e McpUpstreamError com { stage: "tools/call", problem: "http_status", status: 400 } e a mensagem cita HTTP 400; o aviso disparou com { retried: false } (escrita nao repete). Hoje ela falha com problem: "invalid_response".Red-check executado: desligando so o ssePostRefusalStatus (para forcar o comportamento pre-fix), os 2 testes novos falham exatamente no ponto certo (problem: invalid_response em vez de http_status, inits parado em 1) e os 6 outros do arquivo continuam verdes — a correcao e a responsavel pelo comportamento, nao o teste.
O bloqueador B8 e a mesma evidencia relatada por dois angulos (o 404 do POST e o 400 "No transport found"); nao ha uma segunda correcao a fazer — e o unico codigo que faltava era o predicado de evict e a classificacao do erro, ambos resolvidos em B7. Os pontos citados pelo revisor foram honors: o comentario do cabecalho do arquivo (que prometia "a 404 on a request that carried a session id" como unico sinal) e o comentario de isSessionExpired foram atualizados para nomear tambem o sse.
tests/mcp-proxy-v2.test.ts: 9/9 (7 anteriores + 2 novos).tests/mcp-proxy.test.ts: 44/44. tests/mcp-transport.test.ts, tests/mcps.test.ts, tests/origin.test.ts: 103/103 juntos.--runInBand, memguard): igual ao baseline — so o shard-4 (shutdown.test.ts) fecha rc=1, com as 3 mesmas falhas pre-existentes de baseline.json. Nenhuma falha nova. O shard-8 primeiro apontou docs-gen.test.ts (o llms-full.txt gerado ficou para tras das minhas edicoes de doc): rodei npm run docs:gen, commitei o llms-full.txt e re-rodei o shard 8 — 16 suites / 331 testes verdes.npm run typecheck (backend e frontend) limpo; eslint dos arquivos tocados sem aviso; npm run lint:ratchet: 4831 avisos (igual ao baseline), 0 erros — meus arquivos nao acrescentaram aviso.GET /sse novo) em vez de repetir o mesmo erro por 30 min. Em leitura a chamada e repetida; em escrita nao (o efeito pode ter ocorrido), e o resultado leva o aviso pt-BR de sessao reiniciada, como no Streamable HTTP.http_status + codigo), em vez de "resposta JSON-RPC invalida".401/403 no SSE continuam NAO sendo sessao morta: o cliente sobrevive a eles, entao uma credencial trocada no cadastro nao derruba a janela.A critica do plano (bloqueante) esta CONFIRMADA no fonte do SDK v1 1.30.1:
this.sessionId !== undefined;this.sessionId = this.sessionIdGenerator?.();sessionIdGenerator: undefined (o que routes/mcp.ts usa hoje), o header NUNCA sai.E o caminho alternativo (passar sessionIdGenerator) e o oposto de stateless: validateSession (737-758) passa a exigir o header e a devolver 404, e o handler cria um server.connect por POST, logo o transporte morre no fim da requisicao.
DECISAO: o gateway continua STATELESS e NAO emite id. Aceita Mcp-Session-Id como ENTRADA OPCIONAL e o usa so como chave de correlacao do escopo do pool de destino. Sem o header, o escopo e o de hoje (principal + projeto) — sem regressao. Consequencia: mcp-transport.test.ts:102-106 ("initialize sem Mcp-Session-Id") continua valendo sem mudanca; o teste 128-133 vira a prova do comportamento novo. Ator e projeto continuam vindo do token e do header, entao nao ha escalada.
A critica (bloqueante) esta CONFIRMADA: getProxiedCatalog usa projectId ou a constante ALL_SERVERS = '' (catalog.ts:192,215-218), e o '' e alcancado por loadProxiedCatalog quando projectId === null (274-278) e por proxiedCatalogFor quando o ator nao tem projectIds (532). Depois da migration por empresa, as duas empresas leriao o MESMO objeto cacheado. O filtro do banco nao fecharia isso. DECISAO: a chave passa a ser org:<uuid>|project:<uuid>, org:<uuid>|all e org:<uuid>|projects:<ids>; '*' fica reservado para quando nao ha organizacao (token de bootstrap). Exigido: teste que popula o cache com a empresa A e so entao consulta a B, sem revisao no meio.
npm 11.19.0 do node 24.21.0 = o npm do Dockerfile (FROM node:24.21.0-alpine3.24). Gerado e conferido com npm ci --dry-run na propria sandbox; sem override, sem --legacy-peer-deps. Prova: zod 3.25.76 no topo e zod 4.6.7 aninhado coexistem.
A critica (importante) esta CONFIRMADA: mcp-proxy.ts:687-700 repete invokeTool com os MESMOS argumentos depois do evict, e o HTTP nao distingue chamada processada de nao processada. Com o prazo indo de 60 s para 5 min a janela de corrida cresce 5x. DECISAO: o retry so acontece quando a tool e de leitura (annotations.readOnlyHint === true, ja exposto no catalogo como readOnly). Em escrita nao se repete: devolve-se o aviso pt-BR de sessao reiniciada, dizendo que o efeito pode ter ocorrido. O aviso em leitura tambem avisa do reenvio.
safe-fetch.ts:55 DEFAULT_MAX_BYTES = 5 MiB e por requisicao; nao ha teto de 64 MiB no codigo. DECISAO: o teto do POST continua 5 MiB (o de hoje) e o stream SSE legado leva maxBytes explicito, justificado pelo pior caso real (janela de browser por 30 min). Estourar o teto FECHA o stream e encerra a sessao: isso vai no doc, porque o contrario seria um stream sem teto.
Sessao de destino por sessao de conversa cresce o pool. Alem do teto global, um teto por principal impede que um agente ocupe todos os slots. Teste de cesto: N sessoes simultaneas acima do TTL, poolSize nao passa do teto e as mais antigas saem.
A critica (menor): catalogChanged() invalida o catalogo inteiro e um update de URL custaria um discover completo. DECISAO: sync-tools no create como hoje; no update, so quando o TRANSPORTE muda (que hoje quebra a tool de qualquer jeito) — nao quando muda so header ou descricao.
Entrega 1 (o que o dono sentiu): cliente v2 (C1), evict so em sessao morta, erro legivel (C5), sync de tools, sessao por conversa na ENTRADA (C2). Entrega 2 (seguranca e recurso): SSE legado (C3a), catalogo por empresa (C4), prazos de fabrica. Commits separados e coerentes, para que o rebase com a frente do Render seja por pedaco.