A frente de MCP: sessao, protocolo, transporte e empresa

O diagnostico e a implementacao da correcao do proxy, renderizados do markdown de origem.

← Voltar ao relatorio

Diagnostico do proxy de MCP


/workspace/.sbcache/wf-mcp/diagnostico.md


Diagnóstico: proxy de MCP do AgentPack

2026-10-13 · sandbox agent-package-audit · código de origin/main 26268c0 (é o que roda em produção: agentpack-backend-ws está em 26268c0) · nenhuma linha do repo foi alterada.

Em números

0chamadas a tool proxied em produção nas últimas 45 h262 tool calls no gateway, 100% built-in (kind: builtin)
4falhas do proxy em produção, todas no REAEGRESS_TIMEOUT em 10 s (testar conexão) e 15 s (descobrir tools), 12/10 10:21–10:24
54agentes chegando no /mcp do AgentPack com o protocolo 2026-07-2854 de 54 recusados com 400 e caíram para 2025 com sucesso: sem impacto hoje
36passos do proxy atual contra 8 servidores de laboratório3 de 3 falham no servidor só 2026-07-28 · 3 de 3 no SSE legado · a sessão se perde em 3 situações
18passos do cliente oficial v2 (o do Motor) nos mesmos servidoresconecta 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")

Como o proxy funciona (e onde a sessão nasce e morre)

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óteses: veredito

HipóteseVereditoConfiançaProva
H1MCP com estado: gateway stateless não amarra sessãoConfirmada 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 deployaltalab sessao, ocioso, timeout; mcp-proxy.ts:169, 687-700, proxy-call.ts:54-57
H2Sessão expirada (404) sem novo initializeRefutada. O proxy refaz o initialize uma vez e a chamada passa. Resto: o estado volta do zero sem aviso ao agentealtalab expira (count volta a 1); mcp-proxy.ts:575-578, 695
H3Versão de protocolo: só 2026-07-28Confirmada. 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 initializealtalab versao; package.json:53; motor mcp-client.test.ts:4-7 (MCP do painel recusava o v1 com -32022)
H4Transporte: SSE legado e stdio por ponteConfirmada. 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
H5Pool chaveado só por URL e headers vaza entre empresasRefutada 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 juntoalta (código)lab sessao passo B; schema.prisma model McpServer sem organizationId; mcp.service.ts:183-200, 514-536

Causas raiz, em ordem de impacto para o dono

C1. O cliente do proxy não fala 2026-07-28 (H3) · confiança alta

C2. A sessão do destino não acompanha a conversa do agente (H1) · confiança alta

C3. Transportes que o proxy não fala (H4) · C3a SSE legado: confiança alta · C3b ponte do REA: confiança média-alta

C4. Catálogo de MCP é global, sem empresa (H5) · confiança alta (código, sem reprodução: exige banco)

C5. Erro opaco e catálogo que não se atualiza · confiança alta

Evidência de produção (só leitura)

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.

UTCRotaResultadoDestinoErro
12/10 10:21:47POST /api/mcps/test-connection-preview504 em 10.004 mssb-rea-mcp-proxy-p6402.mp.serendiped.comEGRESS_TIMEOUT 10000 ms
12/10 10:21:59POST /api/mcps/test-connection-preview504 em 10.001 msidemEGRESS_TIMEOUT 10000 ms
12/10 10:22:16POST /api/mcps/preview-tools504 em 15.002 msidemEGRESS_TIMEOUT 15000 ms
12/10 10:24:34POST /api/mcps/test-connection-preview504 em 10.001 msidemEGRESS_TIMEOUT 10000 ms
12/10 16:07:48POST /api/mcps/test-connection-preview200 (destino não logado)?sem erro registrado
11/10 a 13/10POST /mcp54 × 400gateway do próprio AgentPackUnsupported protocol version: 2026-07-28; o POST seguinte do mesmo cliente deu 200 nos 54 casos

Laboratório

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)SDKProxy 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çãonã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.

Mapa do código (origin/main 26268c0)

OndeO quê
backend/src/routes/mcps.ts:103-110cadastro (POST /api/mcps), só usuário
backend/src/routes/mcps.ts:183-235sync-tools, test-connection, preview-tools, test-connection-preview (discoverTools e probeConnection)
backend/src/routes/mcps.ts:250-294vínculo com projeto e filtros de tools
backend/src/services/mcp.service.ts:258, 473, 600, 624create, replaceTools (o que o gateway publica), listActiveServers, listActiveServersForProject
backend/src/services/mcp-headers.ts:336buildUpstreamConfig: 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-400cria Client v1 + StreamableHTTPClientTransport com o fetch do egress
backend/src/services/mcp-proxy.ts:268-296upstreamFetch: safeFetchStream, GET standalone = 405 local, captura Mcp-Session-Id
backend/src/services/mcp-proxy.ts:606-611chave do pool: sha256 de URL, headers (com segredos) e escopo
backend/src/mcp/proxy-call.ts:54-57 · mcp/tool-call.ts:45escopo 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-103LRU com TTL ocioso (5 min), teto (100), leases e dispose
backend/src/services/mcp-proxy.ts:740-748sessão nasce: openPooled → connect (initialize)
backend/src/services/mcp-proxy.ts:411-442, 692-696sessão morre: DELETE no dispose; evict em QUALQUER erro
backend/src/services/mcp-proxy.ts:575-578, 687-700404 → nova sessão, uma vez
backend/src/services/mcp-proxy.ts:161-166, 450-478prazos (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-101upstreamFailure (HTTP → MCP_UPSTREAM_ERROR) e logSafeError (corta o corpo)
backend/src/mcp/proxy-call.ts:27-47, 68-86callProxied e o isError "Proxy error for <tool> (<CODE>)"
backend/src/mcp/catalog.ts:274-305, 357-365, 409-451, 522-533tools proxied no gateway: <toolPrefix>_<tool>, lista, busca, escopo por projeto/chave
backend/src/mcp/toolsets.ts:40, 99-104toolset proxied, ligado por padrão
backend/src/routes/mcp.ts:102-146gateway stateless (sessionIdGenerator: undefined), GET/DELETE = 405
backend/src/schemas/mcp.ts:16-21transportes graváveis: http, streamable-http (SSE só leitura)
frontend/src/features/mcps/components/McpTransportSection.tsx:13-28opções da tela ("HTTP Legado" = mesmo transporte)
frontend/src/features/mcps/components/useMcpForm.ts:182-183, 232-234toast "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-756testes do pool, do 404 e da versão não suportada (hoje trava "connected: false")

O que corrigir (proposta para a fase de correção)

  1. C1 · cliente v2 no proxy (resolve o MCP do MyPanel): trocar o 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' }).
  2. C2 · sessão estável: não descartar a sessão em 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").
  3. C3 · transportes: falar SSE legado com 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.
  4. C5 · erro legível e sync: códigos MCP_PROTOCOL_UNSUPPORTED e MCP_TRANSPORT_UNSUPPORTED com texto pt-BR na tela; sincronizar tools ao salvar URL ou headers.
  5. C4 · catálogo por empresa: organizationId em McpServer vindo da Conta Nomad, filtro em lista, edição e vínculo. Frente própria (migration + regra entre apps).

Decisões do dono antes de codar

PerguntaPor quê
D1Sessã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
D2Prazos padrão: TTL ocioso (hoje 5 min) e chamada (hoje 60 s)trade-off entre memória/sessões penduradas e estado perdido
D3Catálogo de MCP por empresa agora ou depoisé mudança de modelo de dados e de regra entre apps
D4Onde roda a ponte do REAhoje morre a cada reinício do sandbox

Implementacao da correcao


/workspace/.sbcache/wf-mcp/IMPLEMENTACAO.md


Implementacao — correcao do proxy de MCP (sessao, era, SSE, empresa)

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)

1. O que mudou e por que

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).

C1 — o cliente nao falava 2026-07-28 (o que o dono sentiu: "nao funciona")

C2 — sessao de destino compartilhada e morta em silencio

C3a — SSE legado de 2024

C4 — catalogo global, sem empresa (vazamento de credencial)

C5 — erro opaco e sync so no create

Prazos de fabrica por env

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.

2. Como testar

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:

Provas novas (falham sem a mudanca)

3. Mudancas de producao

  1. MCP que so aceita 2026-07-28 volta a funcionar. Custo: uma sonda server/discover a mais na primeira conexao de cada sessao de destino contra servidores 2025 (mitigada pelo cache de era).
  2. Duas conversas nao dividem mais a janela de um MCP com sessao. Quem dependia de compartilhar perde isso de proposito.
  3. A sessao de destino sobrevive a prazo e a erro de tool (TTL 30 min, prazo de chamada 5 min, 200 clientes). Com 0 chamadas proxied em producao hoje, impacto nulo; com MCPs reais em uso, o numero de sessoes acompanha o de conversas ativas.
  4. SSE legado volta a funcionar e sse volta a ser selecionavel na tela.
  5. Catalogo por empresa: a credencial de um MCP deixa de ir para quem o vincular em outra empresa. O backfill de empresa unica mantem tudo funcionando; o que ficou ambiguo ou orfao e plataforma (visivel a todas) = o estado de hoje.
  6. Dois codigos de erro novos (502) no problem+json: 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.
  7. Sync de tools no PATCH so quando o transporte muda (antes: nunca). Escreve mcp_tools sem o usuario pedir explicitamente nesse caso.
  8. Migracao 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).
  9. Com mais de uma replica: a conversa precisa cair sempre na mesma enquanto a sessao de destino vive (sticky por Mcp-Session-Id na borda). Com 1 replica (hoje) nada muda. Registrado no ADR-015.

4. O que ficou de fora (de proposito)

5. Riscos e limitacoes conscientes

6. Fora do alcance desta frente

7. Rodada 1 da correcao (commit e13f3e2) — os 6 bloqueios

A 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.

#OndeSintoma confirmadoCorrecaoProva
B1mcp/proxy-call.ts escopocallerSessionId ?? "*", 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
B2mcp-proxy.ts connectretry 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 tentativaveredito legacy cacheado vs servidor so-2026 cai para 2026
B3mcp-proxy.ts callTool catchentrada com ready rejeitado ficava no pool; updateAgeOnGet renovava o TTL a cada tentativa → 30 min do mesmo erroflag connected; evict quando !connectedhandshake falha, upstream volta, a mesma chamada do mesmo escopo funciona
B4mcp-proxy.ts retrymesma causa de B2: o erro virava invalid_response, diagnostico erradoresolvido por B2 (conexao nova)com as 2 tentativas falhando o erro e o do upstream (http_status 500)
B5mcp-proxy.ts terminateSessionlia so transport.sessionId; quando o initialize falhava depois do upstream criar a sessao, o id nunca saia → janela aberta no upstreamstreamSessionId(conn) ?? conn.http.sessionId (id capturado pelo egress)initialize 500 com Mcp-Session-Id → upstream ve o DELETE (antes deletes=0)
B6routes/mcps.ts actorOrgrequest.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 empresasmcpOrgScopeOf() em mcp.service.ts, usada pelas rotas e por mcp/catalog.ts: so o bootstrap ve tudo, sem empresa ve so plataformaGET /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.

8. Rodada 2 da correcao — o ciclo de vida do SSE legado (commit 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.

B7 — a entrada SSE morta nunca sai do pool (o sintoma do dono)

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:

Prova (verde): 2 testes novos em tests/mcp-proxy-v2.test.ts, contra um upstream SSE legado REAL (v1 SSEServerTransport) com dropSessions():

  1. 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).
  2. 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.

B8 (revisto) — mesma raiz, segunda face

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.

Suite

Mudancas de producao desta rodada


Decisoes da implementacao


/workspace/.sbcache/wf-mcp/DECISOES.md


Decisoes da implementacao (frente MCP proxy) — 2026-10-13

D1 · Sessao por conversa: ENTRADA de Mcp-Session-Id, nunca emissao

A critica do plano (bloqueante) esta CONFIRMADA no fonte do SDK v1 1.30.1:

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.

D2 · Cache do catalogo: chave composta por organizacao

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.

D3 · Lockfile do backend gerado com o npm da imagem

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.

D4 · Retry de 404 so em chamada de leitura

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.

D5 · Teto de bytes do stream SSE legado: parametro explicito da chamada

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.

D6 · Teto por principal no pool

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.

D7 · Sync de tools no update: botao, nao automatico

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.

D8 · Backfill de empresa unica: regra escrita antes da migration

D9 · Entrega em 2 blocos na MESMA branch (o dono pediu "resolve tudo isso")

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.