Configuração do astro-webmcp: Security, Collections e Custom Tools

Visão geral da configuração

O astro-webmcp funciona com zero config — mas quando você precisa de controle granular sobre segurança, quais collections expor e tools customizadas, as opções estão ali.

// astro.config.mjs
import { defineConfig } from 'astro/config';
import webmcp from 'astro-webmcp';

export default defineConfig({
  integrations: [
    webmcp({
      collections: ['blog', 'docs'],
      customTools: [/* ... */],
      security: {
        exposedTo: [],
        maxOutputLength: 1500,
        sanitizeOutputs: true,
      },
    }),
  ],
});

Tipagem TypeScript completa

export interface WebMCPOptions {
  collections?: string[];
  customTools?: CustomTool[];
  security?: SecurityOptions;
}

export interface SecurityOptions {
  exposedTo?: string[];       // Origins cross-origin permitidas (default: [])
  maxOutputLength?: number;   // Max chars por output (default: 1500)
  sanitizeOutputs?: boolean;  // Strip injection patterns (default: true)
}

export interface CustomTool {
  name: string;
  description: string;
  inputSchema: Record<string, unknown>;
  executeBody: string;        // Corpo da função (executa no browser)
  annotations?: ToolAnnotations;
}

export interface ToolAnnotations {
  readOnlyHint?: boolean;
  untrustedContentHint?: boolean;
}

collections — Filtrando conteúdo

Por default, todas as content collections do Astro são incluídas no manifesto. Use collections para filtrar:

// Apenas blog e docs — exclui páginas avulsas, landing pages etc.
webmcp({
  collections: ['blog', 'docs'],
})

Quando filtrar

CenárioConfiguraçãoMotivo
Site com blog + docs + landing pagescollections: ['blog', 'docs']Landing pages não são conteúdo navegável
Apenas documentação técnicacollections: ['docs']Foco do agente
Tudo disponívelOmitir collectionsDefault — todas as collections
Site multilínguecollections: ['docs-pt', 'blog-pt']Filtrar por idioma

Verificando collections disponíveis

Após o build, o manifesto lista as collections incluídas:

{
  "collections": [
    { "name": "blog", "count": 42 },
    { "name": "docs", "count": 15 }
  ]
}

Se uma collection não aparece, verifique:

  1. O nome está correto no array collections?
  2. A collection tem pages geradas no build?
  3. As pages têm <title> no HTML compilado?

security — Opções de segurança

exposedTo

Controla quais origins cross-origin podem ver as tools registradas.

security: {
  // Default: [] — same-origin only (mais seguro)
  exposedTo: [],
  
  // Permitir partner específico
  exposedTo: ['https://partner-agent.com'],
}

Atenção: Na vasta maioria dos casos, mantenha exposedTo: []. A exposição cross-origin só faz sentido se outro site precisa consumir suas tools via iframe com allow="tools". Se você não tem esse caso, não abra.

maxOutputLength

Limite de caracteres por output de tool. O Chrome recomenda 1.500.

security: {
  maxOutputLength: 1500,  // Default — segue recomendação Chrome
  maxOutputLength: 3000,  // Para sites com conteúdo denso (use com cautela)
  maxOutputLength: 500,   // Extra restritivo
}

Como funciona internamente:

  • Se o output excede o limite, é truncado com indicador
  • O agente recebe "... [truncated, N chars omitted]" no final
  • Previne context window overflow no LLM

sanitizeOutputs

Quando true (default), strip de padrões conhecidos de prompt injection nos outputs:

security: {
  sanitizeOutputs: true,  // Default — recomendado
}

Padrões filtrados:

PatternExemploRisco
Ignore instructions”ignore previous instructions”Hijacking
Role-play injection”you are now a hacker”Role override
Fake delimiters”system:”, “assistant:“Confusion
XML instruction tags<system>, <instruction>Tag injection

Nota: Sanitização é camada de defesa complementar. Não substitui spotlighting no lado do agente. Veja Segurança para o modelo completo de defesa em profundidade.


customTools — Tools adicionais

Além das 4 tools automáticas, registre tools customizadas para lógica específica do seu site:

webmcp({
  customTools: [
    {
      name: 'get_related_posts',
      description: 'Get posts related to current page by tags. Returns up to 3 matches.',
      inputSchema: {
        type: 'object',
        properties: {
          limit: { 
            type: 'number', 
            description: 'Max related posts (default: 3)' 
          }
        }
      },
      executeBody: `
        const currentPath = window.location.pathname;
        const manifest = await fetch('/_webmcp/manifest.json').then(r => r.json());
        const current = manifest.entries.find(e => e.url === currentPath);
        if (!current?.tags?.length) return JSON.stringify([]);
        
        const related = manifest.entries
          .filter(e => e.url !== currentPath && e.tags?.some(t => current.tags.includes(t)))
          .slice(0, args.limit || 3)
          .map(e => ({ title: e.title, url: e.url }));
        
        return JSON.stringify(related);
      `,
      annotations: {
        readOnlyHint: true,
        untrustedContentHint: true
      }
    }
  ],
})

Estrutura de customTools

CampoObrigatórioDescrição
nameNome da tool (max 30 chars)
descriptionDescrição da ação (max 500 chars)
inputSchemaJSON Schema dos parâmetros
executeBodyCorpo da função (roda no browser)
annotationsreadOnlyHint, untrustedContentHint

Sobre executeBody

O executeBody é o corpo da função execute que roda no browser. Recebe args como variável disponível:

// executeBody tem acesso a:
// - args: objeto com parâmetros do inputSchema
// - window, document, fetch: APIs do browser
// - Manifesto via fetch('/_webmcp/manifest.json')

Atenção: executeBody é injetado como string no script client-side. Não inclua dados sensíveis, secrets ou lógica de autenticação. O código fica visível no source da página — qualquer um com F12 lê.

Exemplos de custom tools

Newsletter signup check

{
  name: 'check_newsletter_status',
  description: 'Check if visitor has already subscribed to newsletter',
  inputSchema: { type: 'object', properties: {} },
  executeBody: `
    const subscribed = localStorage.getItem('newsletter_subscribed');
    return JSON.stringify({ subscribed: subscribed === 'true' });
  `,
  annotations: { readOnlyHint: true }
}

Theme toggle

{
  name: 'toggle_theme',
  description: 'Switch between light and dark theme. Returns new theme.',
  inputSchema: {
    type: 'object',
    properties: {
      theme: { 
        type: 'string', 
        enum: ['light', 'dark', 'auto'],
        description: 'Target theme'
      }
    }
  },
  executeBody: `
    const target = args.theme || (document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark');
    document.documentElement.dataset.theme = target;
    localStorage.setItem('theme', target);
    return JSON.stringify({ theme: target });
  `,
  annotations: { readOnlyHint: false }
}

Table of contents

{
  name: 'get_table_of_contents',
  description: 'Get all headings (h2, h3) from current page as table of contents',
  inputSchema: { type: 'object', properties: {} },
  executeBody: `
    const headings = Array.from(document.querySelectorAll('h2, h3')).map(h => ({
      level: parseInt(h.tagName[1]),
      text: h.textContent.trim(),
      id: h.id || null
    }));
    return JSON.stringify(headings);
  `,
  annotations: { readOnlyHint: true, untrustedContentHint: true }
}

Configuração por ambiente

Para comportamento diferente entre dev e produção:

// astro.config.mjs
const isDev = import.meta.env.DEV;

export default defineConfig({
  integrations: [
    webmcp({
      collections: isDev ? undefined : ['blog', 'docs'],  // Em dev: tudo
      security: {
        sanitizeOutputs: true,
        maxOutputLength: isDev ? 5000 : 1500,  // Mais verboso em dev
      },
    }),
  ],
});

Compliance com character budgets

O astro-webmcp respeita todos os character budgets recomendados pelo Chrome:

ElementoLimite ChromeStatus astro-webmcp
Nome da tool30 chars✅ Todas as tools built-in respeitam
Descrição da tool500 chars✅ Descriptions são curtas e diretas
Descrição de parâmetro150 chars✅ Parâmetros com descriptions compactas
Output da tool1.500 chars✅ Enforced por maxOutputLength

Para custom tools, respeite esses limites nos campos name, description e nas descriptions dentro do inputSchema.


Validação da configuração

Erros comuns e como diagnosticar:

ConfiguraçãoErroSolução
collections: ['nonexistent']Manifesto vazioVerificar nome exato da collection
maxOutputLength: 0Outputs sempre truncadosUsar valor positivo (mínimo: 100)
exposedTo: ['http://site.com']Não funcionaUsar HTTPS (exceto localhost)
customTools com executeBody inválidoErro no consoleTestar o JS isoladamente

Configuração recomendada por tipo de site

Blog pessoal

webmcp({
  collections: ['blog'],
  security: {
    maxOutputLength: 1500,
    sanitizeOutputs: true,
  },
})

Simples — expõe apenas posts, com segurança padrão.

Documentação técnica

webmcp({
  collections: ['docs', 'api-reference', 'guides'],
  security: {
    maxOutputLength: 2000,  // Docs podem ser mais densos
    sanitizeOutputs: true,
  },
})

E-commerce

webmcp({
  collections: ['products', 'categories'],
  customTools: [{
    name: 'filter_by_price',
    description: 'Filter products by price range in BRL',
    inputSchema: {
      type: 'object',
      properties: {
        min: { type: 'number', description: 'Minimum price' },
        max: { type: 'number', description: 'Maximum price' }
      }
    },
    executeBody: `
      const manifest = await fetch('/_webmcp/manifest.json').then(r => r.json());
      // Lógica de filtro baseada em metadata
      return JSON.stringify(manifest.entries.filter(e => e.collection === 'products').slice(0, 10));
    `,
    annotations: { readOnlyHint: true, untrustedContentHint: true }
  }],
  security: {
    sanitizeOutputs: true,
  },
})

Site com área restrita

webmcp({
  collections: ['public-docs', 'blog'],  // NÃO incluir 'admin', 'internal'
  security: {
    exposedTo: [],  // Same-origin only
    sanitizeOutputs: true,
  },
})

Atenção: Nunca inclua collections com conteúdo restrito no collections array. O manifesto é um arquivo estático público — qualquer pessoa pode acessar /_webmcp/manifest.json direto no browser.


Próximos passos