ADR-002 — MCP-First Surface (Open WebUI + MCP Server + MCP Apps)
ADR-002 — MCP-First Surface
Seção intitulada “ADR-002 — MCP-First Surface”| Campo | Valor |
|---|---|
| Status | Accepted + Smoke Validated (2026-06-08) (Gabriel pivot 2026-06-08) |
| Data | 2026-06-08 |
| Versão | v0.2.1+ |
| Substitui | Parcialmente ADR-001 na camada de interface. Camada de domínio + event-sourcing inalterada. |
| Substituído por | — |
| Premissas | Manager premise #26 (hard_rule), #27 (stack), #28 (stack) |
Contexto
Seção intitulada “Contexto”ADR-001 estabeleceu Domain Kernel + Emmett + Minimum Canonical Admin (Next.js) + MCP server como interfaces complementares. A intenção era: admin Next.js = canonical, MCP = porta agentic complementar.
Durante revisão de produto (2026-06-08), Gabriel re-priorizou:
-
Operação real esperada vive no chat. Associação de cannabis tem operação rotineira de dispensação que cabe muito bem em fluxo conversacional: “Maria pediu 10g do CBD-FS-200” → agente abre form com membro pré-selecionado, lote FIFO, quantidade → dispensador confirma → PendingAction → RT aprova no chat → 3 eventos atômicos. Construir esse mesmo fluxo em admin Next.js exige mais código sem entregar mais valor.
-
MCP Apps spec final (janeiro 2026) torna o caminho viável. ext-apps spec (SEP-1865) suporta UI HTML interativa renderizada inline em hosts compatíveis (Claude.ai web/desktop, ChatGPT, VS Code, Cursor, Open WebUI v0.6.31+). Forms, dashboards, timelines, approval flows nascem dentro da conversa.
-
Open WebUI v0.9.6+ tem suporte MCP nativo + OAuth 2.1. Self-host docker-compose pronto em uma sentada (~15min). Resolve chat UI + RAG + multi-modelo + auth gratuitamente. Não precisa construir.
-
Recursos de engenharia são finitos. Time pequeno. Cada hora gasta em Next.js admin é uma hora não gasta em MCP server + tools + apps. Como o objetivo de v0.2.1 é operação real piloto, o caminho MCP-first ship faster.
-
Admin Next.js como fallback “para o caso de” é trabalho especulativo. Se aparecer associação que não consegue operar via chat (hipótese ainda não validada), avaliar PWA leve naquele momento. Não pré-construir.
Decisão
Seção intitulada “Decisão”Para v0.2.1 até v1.0, o canna-oss não terá admin Next.js standalone. Toda interação humana com o sistema acontece através de:
-
MCP server TypeScript (
apps/mcp) — expõe Tools (Nível 1 read, 2 draft, 3 write-with-approval), Resources, Prompts, e MCP Apps via@modelcontextprotocol/ext-apps. -
MCP Apps inline (
packages/ui-apps) — components HTML interativos renderizados dentro do chat host:MemberQuotaCardApp(read-only card + recent dispensations)TraceabilityTimelineApp(plant → harvest → lot → dispensation chain)DispensationFormApp(form + member picker + lot picker FIFO + quantity → Tool Nível 3)PendingActionApprovalApp(diff + risk + approve/reject)InventoryLotPickerApp,MemberSearchApp,SngpcPendingApp,BackupStatusApp(v0.3)KpiDashboardApp,BspoReviewApp,RipdReviewApp,LgpdRequestsApp(v0.4)CultivationOverviewApp,LabResultsApp,FinanceDashboardApp(v1.0)
-
Open WebUI sidecar (docker-compose,
ghcr.io/open-webui/open-webui:v0.9.6) — primary product surface para associação. Self-hosted. Registersapps/mcpvia config file. OAuth 2.1 com scopes mapped para canna roles (DISPENSADOR/RT/DPO/Diretoria/Auditor/Federação). -
REST/OpenAPI (
apps/apiFastify) — system interface para integrações tradicionais (federation agents, contábil, jurídico). Open WebUI consome via MCP, não via OpenAPI direto.mcpobridge disponível para hosts OpenAPI-only mas não obrigatório.
O que NÃO está incluído
Seção intitulada “O que NÃO está incluído”- Admin Next.js standalone — fora do roadmap v0.2.1–v1.0. Move para Ideas Park.
- Apps mobile native — fora de escopo (PWA via Open WebUI mobile é alternativa).
- Workspace Tools (Python execution) no Open WebUI — desabilitado em produção. Toda lógica vive em
apps/mcp, não em scripts Python ad-hoc.
Critical commands continuam fora do MCP
Seção intitulada “Critical commands continuam fora do MCP”Per ADR-001 sync/async boundary + nível-4 risk tools:
execute_crypto_deletion(LGPD Art. 18 IV)change_user_role,disable_2fa,delete_or_rotate_keyssubmit_sngpc_productionchange_quota,recall_lot
Essas não são MCP tools. São operações no apps/api que exigem TOTP + DPO/Admin co-presença. Se v0.3+ produzir um LgpdRequestsApp, esse app inicia o fluxo (cria PendingAction) mas a execução final passa por confirmação fora do agente.
Boundary atualizado
Seção intitulada “Boundary atualizado”| Camada | Quem controla | Camadas dependentes |
|---|---|---|
Domain (@canna/domain) | TypeScript puro — sem deps externas | — |
Event Store (@canna/event-store) | Emmett 0.42.3 (in-memory + Postgres) | @canna/domain |
App Services (@canna/app-services) | Orchestration load → decide → append | @canna/domain, @canna/event-store |
Read Models (@canna/read-models) | Drizzle projections | @canna/app-services events stream |
MCP Server (apps/mcp) | @modelcontextprotocol/sdk + ext-apps; só chama app-services | @canna/app-services, @canna/read-models |
MCP Apps (packages/ui-apps) | ext-apps spec; HTML+CSS+JS inline; comunica via app.callServerTool | apps/mcp tool catalog |
REST API (apps/api) | Fastify thin; só chama app-services | @canna/app-services |
Workers (apps/worker) | BullMQ async (SNGPC, PDF, email) | @canna/app-services, @canna/read-models |
| Open WebUI | sidecar, self-host, consume MCP via config | apps/mcp |
Mental rule: if you find yourself sketching a Next.js admin page, stop. Render as MCP App inline in chat. If the workflow doesn’t fit chat conversation, that’s signal that the workflow needs redesign or that it belongs to the Nível-4 set (which lives at REST apps/api + TOTP).
Consequências
Seção intitulada “Consequências”Positivas
Seção intitulada “Positivas”- Ship faster: ~3-4 weeks of Next.js admin work cancelled. Same engineering hours go into MCP tools + apps that work in any compatible host.
- Agent-native default: associação opera com o agente que já usa (Claude/ChatGPT/Open WebUI/Cursor). “Build once, integrate everywhere” via MCP spec.
- Less UI code: ~70 telas tradicionais de ERP viram ~12 MCP Apps. Mesma cobertura operacional, menos surface area.
- Open WebUI free: chat UI + groups/users + RAG + multi-model — todos resolvidos pelo sidecar. Foco do time fica no domínio.
- MCP App reusability: cada
*Appcomponente renderiza em Claude, ChatGPT, Open WebUI, futuro Canna Copilot embedded. Um codebase, N hosts.
Negativas
Seção intitulada “Negativas”- Depende de host compatibility: Open WebUI v0.9.6+ tem MCP nativo + parcial MCP Apps (UI custom). Outros hosts (Claude.ai, ChatGPT) suportam full MCP Apps. Hosts antigos ou agente fora do ecossistema = sem fallback se a associação não usa nenhum. Mitigação: REST/OpenAPI para integrações; Open WebUI é o sidecar default obrigatório no compose.
- Sem fallback “admin standard”: se Open WebUI sair do ar, operação para. Mitigação: deploy redundante; emergency tool
apps/apiREST acessível via curl/Postman para Nível-4 critical commands. - License consideration: Open WebUI AGPL-3.0 + Commons Clause em enterprise. Não pode ser white-labeled como “canna-oss Admin”; usa como
ghcr.io/open-webui/open-webuicom branding visível. Aceito. - MCP Apps spec é jovem (jan 2026): hosts ainda implementando. Mitigação: começar com Tools (Nível 1 read) que funcionam em 100% dos hosts; gradualmente adicionar Apps conforme amadurecer.
- Multi-tenant isolation: Open WebUI v0.9.6 é single-tenant. Multi-tenant managed hosting v1.0+ exigirá schema isolation + 1 docker-compose per tenant ou Authentik front. Documentado em Interfaces como decisão diferida.
Riscos a evitar
Seção intitulada “Riscos a evitar”- Não fazer Workspace Tools (Python execution) no Open WebUI acessíveis a operadores. Doc oficial alerta = RCE vector. Desabilitar (
ENABLE_KB_EXEC=false) em produção. - Não embedar/forkar Open WebUI dentro do produto. Preservar branding via deploy sidecar.
- Não rodar regra de negócio no Open WebUI. Toda lógica em
apps/mcpchamando@canna/app-services. Open WebUI = chat UI + tool invocation only. - Não construir MCP Apps que pulem RBAC. Cada tool em
apps/mcpvalida OAuth scope antes de chamar app-service. PendingAction obrigatório para Tools Nível 3.
Spike validations executadas
Seção intitulada “Spike validations executadas”Antes de promover esta ADR para Accepted, validamos:
- ✅ Emmett 0.42.3 Postgres adapter funciona com testcontainers (6/6 specs PG verdes; ADR-001 spike gate cumprido)
- ✅ Cross-aggregate dispensation use case atomic 3-event append (50 vitest domain + 6 e2e app-services scenarios)
- ✅ Open WebUI v0.9.6 docker-compose pattern + MCP server registration researched (Agent B report)
- ✅ MCP Apps ext-apps spec + TypeScript SDK status confirmed (Agent C report) — Claude/ChatGPT/Cursor/VS Code full support; Open WebUI partial; ChatGPT launched Jan 2026
Próximos passos (v0.2.1 implementation)
Seção intitulada “Próximos passos (v0.2.1 implementation)”apps/apiFastify thin endpoints (commands proxy)@canna/read-modelsDrizzle projections (member-quota, inventory-summary, dispensation-history)apps/mcpMCP server scaffold (@modelcontextprotocol/sdk)- MCP Tools Nível 1 (read) — 6 tools
- MCP Tools Nível 2 (draft) — 3 tools
- MCP Tools Nível 3 (write w/ approval) — 3 tools + PendingAction infra
packages/ui-apps— 3 MCP Apps básicos (MemberQuotaCardApp,TraceabilityTimelineApp,DispensationFormApp)- Open WebUI sidecar docker-compose + MCP config wiring
- OAuth 2.1 + scope-to-role mapping
- Pilot deploy em 1 associação
Smoke Validation 2026-06-08
Seção intitulada “Smoke Validation 2026-06-08”Sub-agent G executou smoke end-to-end no commit 147009f (tag v0.2.1, branch feature/mcp-first-pivot). Verdict: PARTIAL PASS — stack boota verde, bundles renderizam, OWUI tool-server registration exige seed script (Lane I em flight).
Stack boot
Seção intitulada “Stack boot”docker compose up -demops/openwebui/→ 3 containers (OWUI v0.9.6 + Postgres 16 + Redis 7) healthy.- Cold start: ~44s (image pull cached). RAM steady ~3 GB combinado.
- OWUI responde em
127.0.0.1:8080(admin form loginENABLE_PASSWORD_FORM=trueem smoke override).
Bundle render
Seção intitulada “Bundle render”Os 3 MCP Apps prontos buildam para single-file HTML com inlining estático (script + style inline) via packages/ui-apps Vite SSG step. Servidos por um HTTP server local na porta 8081 durante smoke, todos retornam 200 e renderizam:
MemberQuotaCardApp(manifest) — empty state + populated state OKTraceabilityTimelineApp(manifest) — timeline renderiza com phases ordenadasDispensationFormApp(manifest) — form submit dispara postMessageui/tools/call
OWUI MCP registration reality
Seção intitulada “OWUI MCP registration reality”GOTCHA descoberto no smoke: ops/openwebui/mcp_config.json é template/seed, NÃO arquivo carregado pelo OWUI v0.9.6 no boot. OWUI persiste tool servers no Postgres na tabela tool_server_connection. Registro acontece em runtime via duas vias:
- Admin UI: Settings → Integrações → Servidores de Ferramentas → + (manual, 1× por servidor)
- API:
POST /api/v1/configs/tool_serverscom bearer token de admin (idempotente; seed script emops/openwebui/scripts/seed-tool-servers.ts— Lane I)
Docs operacionais atualizados: ops/openwebui/README.md + ops/openwebui/Kamal.deploy.notes.md seção “MCP server registration — RUNTIME”.
postMessage canonical contract
Seção intitulada “postMessage canonical contract”Host (OWUI / Claude.ai / ChatGPT) ↔ App (iframe) usa window.postMessage bidirecional. Schema canônico baseado em ext-apps spec:
// Host → App (push de payload inicial / refresh)window.postMessage({ type: "ui/notifications/tool-result", params: { content: [{ text: JSON.stringify(canonicalPayload) }] }}, "*")
// App → Host (Tool L2/L3 invocation a partir de form submit, etc.)window.parent.postMessage({ type: "ui/tools/call", params: { name: "request_record_dispensation", arguments: {...} }}, "*")App-side handlers (em main.ts de cada bundle) ouvem message events e fazem JSON.parse(content[0].text) defensivo — tolerância a payload já-objeto ou string (patch landed pós-smoke em member-quota-card/main.ts).
Canonical payload shapes
Seção intitulada “Canonical payload shapes”Cada App declara seu schema esperado no manifest index.ts. Resumo:
- MemberQuotaCardApp (read-only) — espera:
{memberId: string,status: "active" | "suspended" | "pending",consumedG: number,prescription: { monthlyQuotaG: number },recent: Array<{ date: string, quantityG: number, lotId: string }>}
- TraceabilityTimelineApp (read-only) — espera:
{dispensationId: string,timeline: Array<{ phase: string, date: string, data: Record<string, unknown> }>}
- DispensationFormApp — não recebe payload inicial (form em branco); emite Tool call
request_record_dispensationno submit com{ memberId, lotId, quantityG, dispensedAt }.
Evidence
Seção intitulada “Evidence”9 screenshots em ops/openwebui/smoke/:
| # | Frame | Estado |
|---|---|---|
| 01 | OWUI landing | sign-in pré-login |
| 02 | Logged in + MCP servers list | conta admin criada |
| 03 | Admin → Integrações | menu correto |
| 04 | Tool Servers empty | confirma seed pendente |
| 05 | MemberQuotaCardApp empty | bundle carrega antes do payload |
| 06 | MemberQuotaCardApp rendered | postMessage payload aplicado |
| 07 | DispensationFormApp | inputs + submit handler |
| 08 | TraceabilityTimelineApp | timeline phases ordenadas |
| 09 | Smoke summary | run sintético |
Verdict
Seção intitulada “Verdict”PARTIAL PASS. Stack boota verde. Bundles renderizam com postMessage contract. OWUI tool-server registration depende de seed script (Lane I em flight). Próximo gate: seed automático + Kamal deploy canna.fonsecagabriel.com.br (Lane H).
Patches landed pós-smoke:
ops/openwebui/canna-mcp/.gitkeep— garante mount path do composepackages/ui-apps/src/member-quota-card/main.ts— tolerância postMessage (string|object)ops/openwebui/README.md+Kamal.deploy.notes.md— clarifica quemcp_config.jsoné seed, não auto-load
Referências
Seção intitulada “Referências”- MCP Apps ext-apps spec
- MCP Apps blog post (Jan 2026)
- Open WebUI v0.9.6+
- mcpo bridge
- ADR-001 — Domain Kernel + Emmett
- Interfaces — UI · MCP · REST
- Roadmap v0.2.1+
- Manager premises: #26 (hard_rule MCP-first surface), #27 (stack MCP Apps substitui admin), #28 (stack Open WebUI self-host)