Phase 13 - Lesson 14

MCP Apps — Recursos de UI Interativos via `ui://`

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

A saída de ferramenta apenas em texto limita o que os agentes podem exibir. O MCP Apps (SEP-1724, oficial em 26/01/2026) permite que uma ferramenta retorne HTML interativo em sandbox renderizado inline no Claude Desktop, ChatGPT, Cursor, Goose e VS Code. Painéis, formulários, mapas, cenas 3D, tudo através de uma única extensão. Esta lição percorre o esquema de recurso ui://, o MIME text/html;profile=mcp-app, o protocolo postMessage do sandbox de iframe e a superfície de segurança decorrente de permitir que um servidor renderize HTML.

Tipo: Build Idiomas: Python (stdlib, emissor de recursos de UI), HTML (aplicativo de exemplo) Pré-requisitos: Fase 13 · 07 (servidor MCP), Fase 13 · 10 (recursos) Tempo: ~75 minutos

Objetivos de Aprendizado

  • Retornar um recurso ui:// a partir de uma chamada de ferramenta e definir o MIME e os metadados corretos.
  • Declarar a UI associada de uma ferramenta com _meta.ui.resourceUri, _meta.ui.csp e _meta.ui.permissions.
  • Implementar o postMessage JSON-RPC do sandbox de iframe para comunicação entre UI e host.
  • Aplicar padrões de CSP e política de permissões (permissions-policy) que defendam contra ataques originados da UI.

O Problema

Uma ferramenta visualize_timeline da era de 2025 pode retornar "Aqui estão 14 notas organizadas cronologicamente: ...". Isso é um parágrafo. Na verdade, os usuários querem a linha do tempo interativa. Antes do MCP Apps, as opções eram: APIs de widgets específicas do cliente (Claude Artifacts, HTML de GPTs personalizados da OpenAI) ou nenhuma UI de forma alguma.

O MCP Apps (SEP-1724, lançado em 26 de janeiro de 2026) padroniza o contrato. O resultado de uma ferramenta contém um resource cuja URI é ui://... e cujo MIME é text/html;profile=mcp-app. O host o renderiza em um iframe em sandbox com uma CSP limitada e sem acesso à rede, a menos que explicitamente concedido. A UI dentro do iframe envia mensagens para o host por meio de um pequeno dialeto de postMessage JSON-RPC.

Cada cliente compatível (Claude Desktop, ChatGPT, Goose, VS Code) renderiza o mesmo recurso ui:// da mesma forma. Um servidor, um pacote HTML, UI universal.

O Conceito

O esquema de recurso ui://

Uma ferramenta retorna:

{
  "content": [
    {"type": "text", "text": "Here is your notes timeline:"},
    {"type": "ui_resource", "uri": "ui://notes/timeline"}
  ],
  "_meta": {
    "ui": {
      "resourceUri": "ui://notes/timeline",
      "csp": {
        "defaultSrc": "'self'",
        "scriptSrc": "'self' 'unsafe-inline'",
        "connectSrc": "'self'"
      },
      "permissions": []
    }
  }
}

O host então chama resources/read na URI ui://notes/timeline e obtém de volta:

{
  "contents": [{
    "uri": "ui://notes/timeline",
    "mimeType": "text/html;profile=mcp-app",
    "text": "<!doctype html>..."
  }]
}

Sandbox de iframe

O host renderiza o HTML dentro de um <iframe> em sandbox com:

  • sandbox="allow-scripts allow-same-origin" (ou mais restrito conforme declaração do servidor)
  • CSP declarada pelo servidor aplicada através de cabeçalhos de resposta.
  • Sem cookies, sem localStorage da origem do host.
  • Acesso à rede limitado a connectSrc na CSP.

Protocolo postMessage

O iframe comunica-se com o host através de window.postMessage. Um dialeto simplificado de JSON-RPC 2.0:

Sempre fixe (pin) targetOrigin na origem exata do par e, no lado receptor, valide event.origin contra uma lista de permissões antes de processar qualquer payload. Nunca use "*" em nenhum dos lados deste canal — o corpo transporta chamadas de ferramentas e leituras de recursos.

// iframe to host  (pin to host origin)
window.parent.postMessage({
  jsonrpc: "2.0",
  id: 1,
  method: "host.callTool",
  params: { name: "notes_update", arguments: { id: "note-14", title: "..." } }
}, "https://host.example.com");

// host to iframe  (pin to iframe origin)
iframe.contentWindow.postMessage({
  jsonrpc: "2.0",
  id: 1,
  result: { content: [...] }
}, "https://iframe.example.com");

// receiver on both sides
window.addEventListener("message", (event) => {
  if (event.origin !== "https://expected-peer.example.com") return;
  // safe to process event.data
});

Métodos disponíveis no lado do host que a UI pode chamar:

  • host.callTool(name, arguments) — invoca uma ferramenta do servidor.
  • host.readResource(uri) — lê um recurso MCP.
  • host.getPrompt(name, arguments) — busca um modelo de prompt.
  • host.close() — fecha a UI.

Cada chamada ainda passa pelo protocolo MCP e herda as permissões do servidor.

Permissões

A lista _meta.ui.permissions solicita recursos adicionais:

  • camera — acesso à câmera do usuário (usado para UIs de digitalização de documentos).
  • microphone — entrada de voz.
  • geolocation — localização.
  • network:* — acesso à rede mais amplo do que o permitido apenas por connectSrc.

Cada permissão gera um aviso que o usuário vê antes de a UI ser renderizada.

Riscos de segurança

HTML em um iframe ainda é HTML. Nova superfície de ataque:

  • Injeção de prompt via UI. Uma UI de servidor maliciosa pode exibir um texto que pareça uma mensagem do sistema e enganar o usuário. A renderização do host deve distinguir visualmente a UI do servidor da UI do host.
  • Exfiltração via connectSrc. Se a CSP permitir connect-src: *, a UI poderá enviar dados para qualquer lugar. O padrão deve ser estrito.
  • Clickjacking. A UI se sobrepõe ao chrome do host. Os hosts devem impedir a manipulação de z-index e aplicar regras de opacidade.
  • Roubo de foco. A UI assume o foco do teclado e captura a próxima mensagem. Os hosts devem interceptar isso.

A Fase 13 · 15 aborda estes pontos em detalhes como parte da segurança do MCP; esta lição os introduz.

Handshake de ui/initialize

Depois de carregar o iframe, ele envia ui/initialize por postMessage:

{"jsonrpc": "2.0", "id": 0, "method": "ui/initialize",
 "params": {"theme": "dark", "locale": "en-US", "sessionId": "..."}}

O host responde com as capacidades e um token de sessão. A UI usa o token de sessão em cada chamada subsequente ao host.

Primitivas do SDK AppRenderer / AppFrame

O SDK ext-apps expõe duas primitivas utilitárias:

  • AppRenderer (lado do servidor) — empacota um componente React / Vue / Solid e emite um recurso ui:// com o MIME e metadados corretos.
  • AppFrame (lado do cliente) — recebe o recurso, monta o iframe e media o postMessage.

Você pode usar estas primitivas ou criar manualmente o HTML e o JSON-RPC.

Status do ecossistema

O MCP Apps foi lançado em 26 de janeiro de 2026. Suporte do cliente a partir de abril de 2026:

  • Claude Desktop. Suporte completo desde janeiro de 2026.
  • ChatGPT. Suporte completo através do SDK do Apps (mesmo protocolo MCP Apps subjacente).
  • Cursor. Beta; habilite através das configurações.
  • VS Code. Apenas compilações do Insider (Insider builds).
  • Goose. Suporte completo.
  • Zed, Windsurf. Planejado no roadmap.

Servidores em produção: painéis, visualizações de mapa, tabelas de dados, construtores de gráficos, pré-visualizações de IDE em sandbox.

Use

O code/main.py estende o servidor de notas com uma ferramenta visualize_timeline que retorna um recurso ui://notes/timeline, além de um tratador para resources/read nessa URI que retorna um pacote HTML pequeno, mas completo, com uma linha do tempo em SVG. O HTML usa templates da stdlib — sem sistema de build. O postMessage é esboçado em comentários JS, já que a stdlib não pode controlar um navegador.

O que observar:

  • _meta.ui na resposta da ferramenta carrega resourceUri, CSP, permissões.
  • O HTML é renderizado sem acesso à rede; todos os dados são inseridos inline.
  • O JS chama host.callTool via window.parent.postMessage (documentado, mas inerte nesta demonstração com stdlib).

Entregue

Esta lição produz outputs/skill-mcp-apps-spec.md. Dada uma ferramenta que se beneficiaria de uma UI interativa, a habilidade produz o contrato completo do MCP Apps: URI ui://, CSP, permissões, pontos de entrada postMessage e uma lista de verificação de segurança.

Exercícios

  1. Execute code/main.py e inspecione o HTML emitido. Abra o HTML diretamente em um navegador; verifique se o SVG é renderizado. Em seguida, esboce o contrato postMessage que a UI usaria para chamar host.callTool("notes_update", ...).
  2. Restrinja a CSP: remova 'unsafe-inline' e use uma política de script baseada em nonce. O que muda no código de geração do HTML?
  3. Adicione um segundo recurso de UI ui://notes/editor com um formulário para editar uma nota diretamente no local. Quando o usuário enviar, o iframe chamará host.callTool("notes_update", ...).
  4. Faça uma auditoria na superfície de ataque da UI. Onde um servidor malicioso poderia injetar conteúdo? Contra o que o sandbox de iframe defende e contra o que ele não defende?
  5. Leia a especificação SEP-1724 e identifique uma capacidade no SDK do MCP Apps que esta implementação simples não utiliza. (Dica: sincronização de estado no nível do componente.)

Termos-Chave

Termo O que as pessoas dizem O que realmente significa
MCP Apps "Recursos de UI interativos" Extensão SEP-1724 lançada em 26/01/2026
ui:// "Esquema de URI do App" Esquema de recurso para pacotes de UI
text/html;profile=mcp-app "O MIME" Tipo de conteúdo para o HTML de MCP Apps
Sandbox de iframe "Contêiner de renderização" Sandbox do navegador para a UI com CSP e permissões
postMessage JSON-RPC "Conexão UI-para-host" Pequeno dialeto JSON-RPC sobre postMessage para chamadas ao host
_meta.ui "Associação Tool-UI" Metadados que vinculam o resultado de uma ferramenta a um recurso de UI
CSP "Política de Segurança de Conteúdo" Declara origens permitidas para scripts, rede e estilos
AppRenderer "Primitiva do SDK do servidor" Converte um componente de framework em um recurso ui://
AppFrame "Primitiva do SDK do cliente" Auxiliar de montagem do iframe que media o postMessage
ui/initialize "Aperto de mão (Handshake)" Primeiro postMessage da UI para o host

Leituras Adicionais

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).