MCP permission prompt
Consent gate UI before invoking MCP tools with side effects. Implements the SHOULD-level requirement from MCP spec (Tools Security): "Clients SHOULD prompt for user confirmation on sensitive operations." Required to ship any MCP-aware Koder client safely.
Spec — MCP permission prompt
MCP normative source: https://modelcontextprotocol.io/specification/2025-11-25/server/tools §Security. Histórico: Claude Code bug #28580 — permission lookup acoplado ao tool schema load causou false-denies. Lição: separar lookup.
Princípios
- *ntrusted by default*— todo MCP server é untrusted até o user dar consent explícito.
- *-grain control*— Allow once / Allow always / Deny once / Deny always; binário (sim/não) viola UX state of the art.
- *ecoupled lookup*— permission resolution SEPARADA do schema load. Tool catálogo pode estar incompleto; permission cache pode estar fresh.
- *udit everything*— toda decisão persistida em audit log; user pode revisar histórico.
R1 — Anatomia
Bottom sheet (mobile, compact width) OU modal centered (desktop, expanded width):
┌──────────────────────────────────────────────┐
│ [🔧] tool_name │
│ From <server_origin_chip> [risk_badge] │
├──────────────────────────────────────────────┤
│ This tool will: │
│ • <annotation.title> │ ← annotations do MCP tool
│ • <annotation.readOnlyHint?> │
│ • <annotation.destructiveHint?> │
│ │
│ Arguments preview: │
│ { "query": "...", "limit": 10 } │
├──────────────────────────────────────────────┤
│ [ Deny always ] [ Deny once ] │ ← actions
│ [ Allow once ] [ Allow always ] │
└──────────────────────────────────────────────┘Slots:
| Slot | Conteúdo |
|---|---|
| Tool icon + name | Como em mcp-tool-invocation.kmd R1 |
| Server origin chip | Slug + trust indicator (cross |
| Risk badge | per R2 — Low / Medium / High |
| Annotations | Render de tools/list[].annotations (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint) |
| Arguments preview | JSON truncated com Show-more |
| Actions | 4 botões per R3 |
R2 — Risk derivation
Risk badge derivado de tools/list[].annotations + heuristics:
| Risk | Critérios | Color (color-roles) |
|---|---|---|
| *ow* | readOnlyHint: true AND server trusted |
success-container |
| *edium* | readOnlyHint: true AND server untrusted, OR idempotentHint: true with side effects |
warning-container |
| *igh* | destructiveHint: true OR openWorldHint: true OR no annotations (treat as worst case) |
error-container |
Quando server fornece custom risk metadata via _meta.koder.risk, isso sobrescreve o derivation acima (server self-describes).
R3 — Actions (4-grain control)
| Action | Behavior |
|---|---|
| *llow once* | Executa este invoke; próximo invoke do mesmo tool re-prompts. |
| *llow always* | Executa este invoke; cria entry em permission store: (server_id, tool_name, user_id, workspace_id) → ALLOW. Re-prompts NÃO acontecem. |
| *eny once* | Cancela este invoke; próximo invoke do mesmo tool re-prompts. |
| *eny always* | Cancela este invoke; entry permission store: → DENY. AI client receive error result com explanation. |
Default focus button: *llow once*(princípio do menor compromisso — user precisa confirmar, mas não auto-trust).
Tools com destructiveHint: true: default focus muda pra *eny once*(reduce accident risk).
R4 — Persistência
Permission store schema (kdbkv table per `rfc001 kdbasunifieddataplane`):
key: mcp_permission:<koder_user_id>:<workspace_id>:<server_id>:<tool_name>
value: {
decision: "ALLOW" | "DENY",
granted_at: ISO8601,
expires_at: ISO8601 | null,
granted_by: <koder_user_id>,
args_hash: optional sha256(canonical_json(args)) // se per-args, não per-tool
}- Lookup é *(1)*per tool call.
- Cache em memory client-side; sync com kdb on session start.
- Cross
tenant lookup retorna nil (não erro), per `multitenantbydefault.kmd`.
R4.1 — Lookup decouple (lição Claude Code #28580)
Permission lookup MUST NOT bloquear ou atrasar tool schema load.
- Schema load:
tools/listrequest → cache schemas. Não consulta permission store. - Permission lookup: chamado SÓ no momento do
tools/call, após user click "invoke" (ou agent autonomous decide invoke). Lookup roda em background; UI mostra "Checking permissions…" se >100ms. - Race condition: se permission store está vazio E user já clicou invoke, fall back para R1 prompt (consent UI). NUNCA assume default
allow ou defaultdeny silenciosamente.
R5 — Auto-revoke
Allow always SHOULD ter expires_at automático (mitigation):
| Risk | Default expiry |
|---|---|
| Low | 90 dias |
| Medium | 30 dias |
| High | 7 dias (Allow always proibido pra destructiveHint: true; user MUST escolher Allow once) |
User pode override via settings (extender ou desabilitar expiry). Expired permissions disparam re-prompt na próxima invocação.
R6 — Audit log
Toda decisão de permission MUST emit audit event pra services/foundation/audit/ schema:
{
event_type: "mcp.permission.decision",
decision: "ALLOW_ONCE" | "ALLOW_ALWAYS" | "DENY_ONCE" | "DENY_ALWAYS",
koder_user_id: ...,
workspace_id: ...,
server_id: ...,
tool_name: ...,
args_hash: sha256(canonical_json(args)),
risk_tier: "low" | "medium" | "high",
timestamp: ISO8601,
origin: "user_prompt" | "cache_hit" | "auto_revoke_renewal"
}Audit log respeita policies/identity-data-retention.kmd (R2 auth_events retention windows; mcp.permission.* falls under same retention).
R7 — Surface bindings
| Surface | API |
|---|---|
| Flutter | KoderMCPPermissionSheet em engines/sdk/koder_kit/lib/src/ai/mcp_permission_sheet.dart |
| Web | <koder-mcp-permission-sheet> |
| Compose Android | KoderMCPPermissionSheet em koder-design-compose (futuro) |
| SwiftUI iOS | KoderMCPPermissionSheet em koder-design-swift (futuro) |
| CLI / TUI | Plain prompt: header + actions numeradas (123/4) |
API consistent: show(toolCall, onDecision: callback).
R8 — Acessibilidade
- Sheet é
role="dialog" aria-modal="true" aria-labelledby="tool-name". - Focus trap: Tab cycle entre 4 botões + close.
- ESC = Deny once (não Deny always — escape é cautious).
- Screen reader announce: tool name + risk tier + annotations.
- Reduced
motion: sheet aparece sem slidein. - Touch target: cada button ≥48dp.
R9 — i18n
Copy ratificada em koder_kit/l10n:
| Key | en-US | pt-BR |
|---|---|---|
mcp.permission.title |
"Allow this tool to run?" | "Permitir execução desta ferramenta?" |
mcp.permission.from |
"From {server}" | "Do servidor {server}" |
mcp.permission.action.allow_once |
"Allow once" | "Permitir uma vez" |
mcp.permission.action.allow_always |
"Allow always" | "Permitir sempre" |
mcp.permission.action.deny_once |
"Deny once" | "Negar uma vez" |
mcp.permission.action.deny_always |
"Deny always" | "Negar sempre" |
mcp.permission.risk.low |
"Low risk · read-only" | "Risco baixo · somente leitura" |
mcp.permission.risk.medium |
"Medium risk" | "Risco médio" |
mcp.permission.risk.high |
"High risk · may modify data" | "Risco alto · pode modificar dados" |
Per feedback_kds_owner_curated_content: editorial copy NOT editable by AI autonomously; changes require owner review.
T-suite
- *1*Prompt shows: novo tool call sem entry no store → sheet aparece.
- *2*Allow once: tool executa; segundo call do mesmo tool → re-prompt.
- *3*Allow always: tool executa; segundo call → sem prompt (cache hit). Audit log emite "cache_hit".
- *4*Deny once: tool NÃO executa; AI client recebe error result.
- *5*Deny always: tool NÃO executa; entry persiste; segundo call também denied sem re-prompt.
- *6*Decouple regression (lição #28580): tools/list demora 5s; user clica invoke imediatamente após render do tool card → permission lookup roda independentemente; sheet aparece sem aguardar schema reload.
- *7*Auto
revoke: Allow always lowrisk; avançar clock 91 dias; próximo call → re-prompt (expired). - *8*Multi
tenant: user A allow always em workspace 1; user B no workspace 2 invoca mesmo tool → reprompt (scoping correto). - *1*Race condition negativo: invoke disparado antes do permission store sync → R4.1 fallback (prompt).
- *2*Audit log emit on every decision (T1-T5).
- *3*Destructive hint Allow always proibido: tool com
destructiveHint: true→ Allow always button disabled, tooltip explica.
Cross-link
- Companion:
mcp-tool-invocation.kmd(consumer da decision),mcp-server-state.kmd(server trust state) - Policies:
multi-tenant-by-default.kmd(R4 storage),identity-data-retention.kmd(R6 audit retention) - Backend:
services/ai/mcp/,services/foundation/audit/ - Historical incident: https://github.com/anthropics/claude-code/issues/28580 (decouple lesson)
- MCP normative: https://modelcontextprotocol.io/specification/2025-11-25/server/tools