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ário | Configuração | Motivo |
|---|---|---|
| Site com blog + docs + landing pages | collections: ['blog', 'docs'] | Landing pages não são conteúdo navegável |
| Apenas documentação técnica | collections: ['docs'] | Foco do agente |
| Tudo disponível | Omitir collections | Default — todas as collections |
| Site multilíngue | collections: ['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:
- O nome está correto no array
collections? - A collection tem pages geradas no build?
- 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 comallow="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:
| Pattern | Exemplo | Risco |
|---|---|---|
| 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
| Campo | Obrigatório | Descrição |
|---|---|---|
name | ✅ | Nome da tool (max 30 chars) |
description | ✅ | Descrição da ação (max 500 chars) |
inputSchema | ✅ | JSON Schema dos parâmetros |
executeBody | ✅ | Corpo da função (roda no browser) |
annotations | ❌ | readOnlyHint, 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:
| Elemento | Limite Chrome | Status astro-webmcp |
|---|---|---|
| Nome da tool | 30 chars | ✅ Todas as tools built-in respeitam |
| Descrição da tool | 500 chars | ✅ Descriptions são curtas e diretas |
| Descrição de parâmetro | 150 chars | ✅ Parâmetros com descriptions compactas |
| Output da tool | 1.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ção | Erro | Solução |
|---|---|---|
collections: ['nonexistent'] | Manifesto vazio | Verificar nome exato da collection |
maxOutputLength: 0 | Outputs sempre truncados | Usar valor positivo (mínimo: 100) |
exposedTo: ['http://site.com'] | Não funciona | Usar HTTPS (exceto localhost) |
customTools com executeBody inválido | Erro no console | Testar 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
collectionsarray. O manifesto é um arquivo estático público — qualquer pessoa pode acessar/_webmcp/manifest.jsondireto no browser.
Próximos passos
- astro-webmcp — Visão geral da integração
- astro-webmcp Arquitetura — Como o build-time manifest funciona
- Segurança — Modelo completo de defesa em profundidade