Arquitetura do astro-webmcp: Manifest, Injection e Design Decisions

Visão geral da arquitetura

O astro-webmcp opera em três camadas:

  1. Build time — Gera o manifesto JSON estático com metadata de todas as páginas
  2. Runtime (browser) — Injeta script leve que registra tools via WebMCP
  3. Agent — Descobre e consome tools registradas
┌─────────────────────────────────────────────────────────┐
│                      BUILD TIME                          │
│                                                         │
│  Astro pages ────→ Hook astro:build:done                │
│                         │                               │
│                         ▼                               │
│                   /_webmcp/manifest.json                 │
│                   (titles, slugs, descriptions)          │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│                    RUNTIME (Browser)                     │
│                                                         │
│  Injected script (injectScript) ~1KB gzipped            │
│       │                                                 │
│       ├─ Feature detection:                             │
│       │   document.modelContext ?? navigator.modelContext│
│       │                                                 │
│       ├─ fetch('/_webmcp/manifest.json')                │
│       │                                                 │
│       └─ mc.registerTool()                              │
│            ├── search_content                           │
│            ├── list_sections                            │
│            ├── go_to                                    │
│            └── get_page_info                            │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│                      AI AGENT                           │
│                                                         │
│  Chrome 149+ descobre tools via WebMCP protocol         │
│  Agent pode buscar, listar e navegar conteúdo do site   │
└─────────────────────────────────────────────────────────┘

Hooks Astro utilizados

A integração usa 3 hooks do Astro para operar em diferentes fases:

HookFaseFunção
astro:config:setupConfigInjeta script client-side via injectScript('page', ...)
astro:server:setupDevServe manifesto dinâmico durante astro dev
astro:build:doneBuildGera /_webmcp/manifest.json do HTML compilado

astro:config:setup

Registra o script que será injetado em toda página do site:

// Simplificação do que o hook faz:
addIntegration({
  name: 'astro-webmcp',
  hooks: {
    'astro:config:setup': ({ injectScript }) => {
      injectScript('page', generateClientScript(options));
    }
  }
});

O script injetado é minificado e pesa ~1KB gzipped. Contém:

  • Feature detection
  • Fetch do manifesto
  • Registro das 4 tools built-in
  • Custom tools (se configuradas)

astro:server:setup

Durante desenvolvimento (astro dev), serve o manifesto dinamicamente:

'astro:server:setup': ({ server }) => {
  server.middlewares.use('/_webmcp/manifest.json', (req, res) => {
    // Gera manifesto on-the-fly para dev
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify(buildManifest()));
  });
}

Isso permite que as tools funcionem em dev sem rodar astro build. Detalhe que faz diferença no dia a dia.

astro:build:done

Após o build, percorre o HTML compilado para extrair metadata:

'astro:build:done': ({ pages, dir }) => {
  const entries = [];
  
  for (const page of pages) {
    const html = readFileSync(join(dir, page.pathname, 'index.html'), 'utf-8');
    const title = extractTitle(html);        // <title>...</title>
    const description = extractMeta(html);   // <meta name="description">
    const headings = extractHeadings(html);  // h1, h2, h3
    
    entries.push({
      slug: page.pathname,
      url: `/${page.pathname}/`,
      title,
      description,
      collection: inferCollection(page.pathname),
      tags: extractTags(html)
    });
  }
  
  writeFileSync(
    join(dir, '_webmcp', 'manifest.json'),
    JSON.stringify({ generatedAt: new Date().toISOString(), entries })
  );
}

O manifesto JSON

Estrutura

{
  "generatedAt": "2026-06-16T14:30:00.000Z",
  "site": "https://prompt.api.br",
  "collections": [
    { "name": "blog", "count": 42 },
    { "name": "docs", "count": 15 },
    { "name": "webmcp", "count": 9 }
  ],
  "entries": [
    {
      "slug": "webmcp/introducao",
      "url": "/webmcp/introducao/",
      "title": "WebMCP: O Protocolo que Torna Sites Legíveis para Agentes de IA",
      "description": "Entenda o que é WebMCP, como funciona...",
      "collection": "webmcp",
      "tags": ["webmcp", "chrome", "ai-agents"]
    }
  ]
}

Características

AspectoDecisãoMotivo
FormatoJSON estáticoCDN-cacheable, sem server runtime
GeraçãoBuild timeSem custo runtime, funciona SSG e SSR
Localização/_webmcp/manifest.jsonConvenção, same-origin fetch
ConteúdoApenas metadata públicaEquivalente a sitemap.xml

O que o manifesto contém vs sitemap.xml

DadoManifestositemap.xml
URLs
Títulos
Descriptions
Collections
Tags
Lastmod
Priority

O manifesto é um sitemap enriquecido — contém dados semânticos que o agente usa para busca e navegação inteligente.


Client script injection

O que é injetado

O script injetado em toda página segue esta lógica:

// Versão simplificada do script injetado
(async () => {
  // 1. Feature detection
  const mc = document.modelContext ?? navigator.modelContext;
  if (!mc?.registerTool) return; // Sai silenciosamente
  
  // 2. Fetch manifesto
  const manifest = await fetch('/_webmcp/manifest.json').then(r => r.json());
  
  // 3. Helpers
  const truncate = (s, max) => s.length > max 
    ? s.slice(0, max - 30) + '\n... [truncated]' 
    : s;
  
  const sanitize = (s) => s.replace(/ignore\s+(previous|prior)\s+instructions/gi, '[FILTERED]');
  
  // 4. Registrar tools
  const options = { exposedTo: [] }; // Config de security
  
  mc.registerTool({
    name: 'search_content',
    description: 'Search articles and pages by keyword',
    inputSchema: { /* ... */ },
    execute: async ({ query, collection, limit = 5 }) => {
      const results = manifest.entries
        .filter(e => {
          const haystack = `${e.title} ${e.description || ''} ${(e.tags || []).join(' ')}`.toLowerCase();
          const match = haystack.includes(query.toLowerCase());
          return match && (!collection || e.collection === collection);
        })
        .slice(0, Math.min(limit, 20));
      
      return truncate(sanitize(JSON.stringify(results)), 1500);
    },
    annotations: { readOnlyHint: true, untrustedContentHint: true }
  }, options);
  
  // ... list_sections, go_to, get_page_info
})();

Tamanho e performance

MétricaValor
Tamanho do script (minificado)~1KB gzipped
Impacto no load timeNegligível (async, non-blocking)
Fetch do manifestoLazy (só quando tools são registradas)
CPU usageZero (sem polling, event-driven)

O script é non-blocking: se o browser não suporta WebMCP, sai na primeira linha sem custo nenhum. Quem usa Firefox nem percebe que existe.


Design decisions

Por que manifesto JSON estático (não virtual module)?

AlternativaProblema
Vite virtual moduleNão funciona com SSG puro; requer bundler runtime
API endpointPrecisa de server runtime; não funciona em CDN
Inline no HTMLAumenta tamanho de toda página; não cacheable
JSON estáticoFunciona SSG e SSR, CDN-cacheable, sem Vite plugin

Por que busca client-side (não server-side)?

AlternativaProblema
Server endpoint /api/searchRequer SSR; não funciona em deploy estático
External search serviceDependência adicional; latência; custo
Client-side filterZero infra; funcional para sites <1000 páginas

Para sites muito grandes (>1000 páginas), considere um search endpoint MCP em paralelo. Mas pra 99% dos sites Astro, client-side resolve.

Por que API Imperativa (não Declarativa)?

MotivoExplicação
Search não é formBusca precisa de lógica JS (filter, slice, format)
Navigation não é formgo_to causa window.location.href change
Controle de outputPrecisa truncar, sanitizar, formatar JSON
Lifecycle managementAbortController para cleanup

A API Declarativa é complementar — use para formulários reais (contato, newsletter, etc.).

Por que feature detection com fallback?

const mc = document.modelContext ?? navigator.modelContext;

O Chrome 149 usava navigator.modelContext. O Chrome 150+ mudou para document.modelContext (breaking change). O fallback garante compatibilidade:

ChromeInterfaceFallback funciona?
149navigator.modelContext
150+document.modelContext
Sem suporteAmbos undefined✅ (sai silenciosamente)

Segurança por design

Princípios implementados

PrincípioImplementação
Sem execução de código arbitrárioTools só lêem dados estáticos ou navegam
Sem innerHTMLOutput é JSON.stringify — zero vetor XSS
Sem requests externosManifesto fetched de same-origin only
Progressive enhancementBrowser sem suporte: script sai imediatamente
Origin-isolatedCross-origin iframes sem allow="tools" não acessam
Defense-in-depthMúltiplas camadas independentes

Superfície de ataque minimizada

O astro-webmcp não:

  • Executa código dinâmico baseado em input do agente
  • Faz requests para URLs fornecidas pelo agente
  • Expõe dados além do que já está no HTML público
  • Modifica o DOM baseado em output de tools
  • Armazena dados de sessão do agente

Fluxo de dados detalhado

┌─────────────────────────────────────────────────────────────┐
│  1. BUILD (astro build)                                      │
│                                                              │
│  src/content/blog/*.md  ──┐                                  │
│  src/content/docs/*.md  ──┼── Astro SSG ──→ dist/           │
│  src/pages/*.astro      ──┘                    │             │
│                                                │             │
│  astro:build:done hook ←───────────────────────┘             │
│       │                                                      │
│       ▼                                                      │
│  Percorre dist/**/*.html                                     │
│       │                                                      │
│       ▼                                                      │
│  Extrai: <title>, <meta description>, <h1>-<h3>, pathname   │
│       │                                                      │
│       ▼                                                      │
│  dist/_webmcp/manifest.json                                  │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  2. DEPLOY                                                   │
│                                                              │
│  dist/ → CDN/hosting (Vercel, Netlify, Cloudflare, etc.)    │
│  manifest.json é apenas mais um arquivo estático             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  3. RUNTIME (visitante abre página)                          │
│                                                              │
│  Browser carrega HTML ──→ Script injetado executa            │
│       │                                                      │
│       ├── mc = document.modelContext ?? navigator...          │
│       │   (se null → return — custo zero)                    │
│       │                                                      │
│       ├── fetch('/_webmcp/manifest.json')                    │
│       │   (cacheable — CDN serve; ~5-50KB)                   │
│       │                                                      │
│       └── mc.registerTool() x4 (ou mais com customTools)     │
│                                                              │
│  Tools agora visíveis para agentes via WebMCP                │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  4. AGENT (Chrome built-in ou extensão)                      │
│                                                              │
│  Descobre tools → Escolhe tool → Envia args → Recebe output │
│                                                              │
│  Exemplo:                                                    │
│  search_content({ query: "WebMCP" }) → JSON results          │
│  list_sections() → collections com contagem                  │
│  go_to({ slug: "webmcp/seguranca" }) → navegação             │
└─────────────────────────────────────────────────────────────┘

Extensibilidade

Adicionando tools sem modificar o plugin

Registre tools adicionais em qualquer página Astro — sem tocar na config do plugin:

---
// src/pages/products.astro
---
<html>
<body>
  <!-- Conteúdo da página -->
  
  <script>
    // Tool específica desta página
    const mc = document.modelContext ?? navigator.modelContext;
    if (mc?.registerTool) {
      mc.registerTool({
        name: 'filter_products',
        description: 'Filter products by category and price range',
        inputSchema: {
          type: 'object',
          properties: {
            category: { type: 'string', enum: ['electronics', 'books', 'clothing'] },
            maxPrice: { type: 'number', description: 'Maximum price in BRL' }
          }
        },
        execute: async ({ category, maxPrice }) => {
          // Lógica de filtro
          return JSON.stringify(filteredProducts);
        }
      });
    }
  </script>
</body>
</html>

Essas tools coexistem com as tools do astro-webmcp — o agente vê todas.

Integração com headless CMS

Se seu conteúdo vem de um CMS, o manifesto será gerado a partir do HTML compilado pelo Astro. Não importa a source:

  • Markdown local
  • Contentful
  • Sanity
  • Notion
  • Qualquer outro CMS

O hook astro:build:done opera sobre o HTML final, independente de onde o conteúdo nasceu.


Limitações da arquitetura

LimitaçãoMotivoWorkaround
Busca é full-scanClient-side filter no manifestoOK para <1000 páginas
Manifesto desatualizadoEstático (gerado no build)Rebuild para atualizar
Sem full-text searchManifesto contém apenas metadataAdicione tool custom com search API
Sem auth-awarePlugin é fully staticRegistre tools auth-aware separadamente
Sem streamingOutputs são JSON stringsLimitação da spec WebMCP (issue #82)

Próximos passos