API Imperativa do WebMCP: document.modelContext em Detalhe
Visão geral da API Imperativa
A API Imperativa é o coração do WebMCP. Com ela, você registra tools via JavaScript puro usando document.modelContext — a interface que conecta sua página a agentes de IA.
Quando faz sentido usar a API Imperativa:
- Lógica programática (buscas, cálculos, transformações)
- Operações assíncronas (chamadas a APIs, banco de dados)
- Controle fino sobre registro/desregistro de tools
- Interação cross-origin entre documentos
Atenção: No Chrome 150+, a API migrou de
navigator.modelContextparadocument.modelContext. Use o pattern de feature detection abaixo para funcionar nas duas versões.
const mc = document.modelContext ?? navigator.modelContext;
if (!mc?.registerTool) return; // Browser não suporta WebMCP
document.modelContext — A interface principal
document.modelContext é o ponto de entrada para tudo que é imperativo no WebMCP. Três métodos e um evento:
| Método/Evento | Descrição |
|---|---|
registerTool(definition, options?) | Registra uma tool no contexto |
getTools(options?) | Descobre tools disponíveis |
executeTool(tool, args, options?) | Executa uma tool manualmente |
addEventListener('toolchange', handler) | Escuta mudanças na lista de tools |
registerTool() — Registrando tools
Assinatura
document.modelContext.registerTool(
toolDefinition: ToolDefinition,
options?: RegisterToolOptions
): void
ToolDefinition — Estrutura completa
interface ToolDefinition {
name: string; // Identificador (max 30 chars recomendado)
description: string; // O que a tool faz (max 500 chars)
inputSchema: JSONSchema; // Schema dos parâmetros de entrada
execute: (args: object) => Promise<any>; // Callback executado
annotations?: {
readOnlyHint?: boolean; // true = não altera estado
untrustedContentHint?: boolean; // true = output pode conter UGC
};
}
interface RegisterToolOptions {
signal?: AbortSignal; // Para desregistrar via abort
exposedTo?: string[]; // Origins cross-origin permitidas
}
Exemplo básico — Tool de busca
const mc = document.modelContext ?? navigator.modelContext;
mc.registerTool({
name: 'search_articles',
description: 'Search published articles by keyword. Returns title, URL and excerpt.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search term (case-insensitive)'
},
limit: {
type: 'number',
description: 'Max results (default: 5, max: 20)'
}
},
required: ['query']
},
execute: async ({ query, limit = 5 }) => {
const results = await fetchArticles(query, Math.min(limit, 20));
return JSON.stringify(results.map(a => ({
title: a.title,
url: a.url,
excerpt: a.excerpt.slice(0, 200)
})));
},
annotations: {
readOnlyHint: true,
untrustedContentHint: true
}
});
Exemplo com enum e oneOf
Quando os parâmetros têm valores fixos, enum combinado com oneOf dá contexto semântico ao agente:
mc.registerTool({
name: 'get_order_status',
description: 'Search orders in a given timeframe. Returns order number, shipping status and location.',
inputSchema: {
type: 'object',
properties: {
timeframe: {
type: 'string',
oneOf: [
{ type: 'string', const: 'today', title: 'Today' },
{ type: 'string', const: 'yesterday', title: 'Yesterday' },
{ type: 'string', const: 'last_7_days', title: 'Last 7 Days' },
{ type: 'string', const: 'last_30_days', title: 'Last 30 Days' },
{ type: 'string', const: 'last_6_months', title: 'Last 6 Months' }
],
enum: ['today', 'yesterday', 'last_7_days', 'last_30_days', 'last_6_months'],
description: 'Timeframe for the order lookup.'
}
},
required: ['timeframe']
},
execute: async ({ timeframe }) => {
const orders = await fetchOrders(timeframe);
return JSON.stringify(orders);
}
});
Dica: O
titledentro deoneOfdá contexto humano para cada opção. Se o usuário diz “pedidos recentes”, o agente entende quelast_7_daysé a melhor escolha — justamente por causa do title.
Exemplo completo — Pizza Maker (demo oficial)
O padrão usado na demo oficial do Google Chrome Labs:
const controller = new AbortController();
mc.registerTool({
name: 'toggle_layer',
description: 'Control pizza layers (sauce, cheese). Use "add", "remove", or "toggle".',
inputSchema: {
type: 'object',
properties: {
layer: {
type: 'string',
enum: ['sauce-layer', 'cheese-layer'],
description: 'Which layer to modify'
},
action: {
type: 'string',
enum: ['add', 'remove', 'toggle'],
description: 'Action to perform on the layer'
}
},
required: ['layer']
},
execute: async ({ layer, action = 'toggle' }) => {
await toggleLayer(layer, action);
return `Performed ${action} on layer: ${layer}`;
},
annotations: {
readOnlyHint: false,
untrustedContentHint: false
}
}, { signal: controller.signal });
AbortSignal — Desregistrando tools
O jeito recomendado de desregistrar tools é via AbortController. Chamou controller.abort()? A tool some do registro. Simples assim.
Registro condicional baseado em estado
class CartManager {
#controller = null;
onCartUpdated(items) {
// Desregistra tool anterior
this.#controller?.abort();
if (items.length > 0) {
// Registra tool de checkout apenas quando há itens
this.#controller = new AbortController();
mc.registerTool({
name: 'checkout',
description: `Proceed to checkout with ${items.length} items. Total: R$${this.getTotal()}.`,
inputSchema: {
type: 'object',
properties: {
coupon: {
type: 'string',
description: 'Optional discount coupon code'
}
}
},
execute: async ({ coupon }) => {
const result = await processCheckout(coupon);
return JSON.stringify(result);
}
}, { signal: this.#controller.signal });
}
}
}
Desregistro por navegação SPA
Em SPAs, registre/desregistre tools conforme o usuário navega:
// React example
function ProductPage({ productId }) {
useEffect(() => {
const controller = new AbortController();
mc.registerTool({
name: 'add_to_cart',
description: `Add current product (ID: ${productId}) to shopping cart`,
inputSchema: {
type: 'object',
properties: {
quantity: { type: 'number', description: 'Quantity (default: 1)' }
}
},
execute: async ({ quantity = 1 }) => {
await addToCart(productId, quantity);
return `Added ${quantity}x product ${productId} to cart`;
}
}, { signal: controller.signal });
// Cleanup: desregistra quando componente desmonta
return () => controller.abort();
}, [productId]);
}
Dica: Em React, Vue ou Svelte, vincule o ciclo de vida da tool ao ciclo de vida do componente. O
AbortControllerencaixa naturalmente nesse padrão — quem já trabalha com fetch já conhece a mecânica.
getTools() — Descobrindo tools disponíveis
Assinatura
document.modelContext.getTools(
options?: GetToolsOptions
): Promise<ToolDescriptor[]>
interface GetToolsOptions {
fromOrigins?: string[]; // Origins cross-origin para buscar
}
interface ToolDescriptor {
name: string;
description: string;
inputSchema: string; // JSON stringificado
origin: string; // Origin que registrou
window: Window; // Window do documento
annotations?: {
readOnlyHint?: boolean;
untrustedContentHint?: boolean;
};
}
Listando tools same-origin
const tools = await mc.getTools();
tools.forEach(tool => {
console.log(`${tool.name}: ${tool.description}`);
console.log(` Schema: ${tool.inputSchema}`);
console.log(` Origin: ${tool.origin}`);
});
Listando tools cross-origin
Para acessar tools de outros origins, passe fromOrigins:
const allTools = await mc.getTools({
fromOrigins: ['https://partner-widget.com', 'https://payments.example.com']
});
Nota: Uma tool cross-origin só aparece se duas condições forem verdadeiras: (1) a tool foi registrada com
exposedToincluindo seu origin, e (2) você listou o origin da tool emfromOrigins. Tem que bater dos dois lados — é um handshake explícito.
executeTool() — Execução manual de tools
Assinatura
document.modelContext.executeTool(
tool: ToolDescriptor,
argsJSON: string,
options?: { signal?: AbortSignal }
): Promise<string | null>
O retorno é null quando a execução causa navegação (mudança de página).
Exemplo: Orquestração de tools
// Obter tools disponíveis
const tools = await mc.getTools();
const searchTool = tools.find(t => t.name === 'search_content');
if (searchTool) {
// Executar com argumentos JSON
const result = await mc.executeTool(
searchTool,
JSON.stringify({ query: 'WebMCP', limit: 3 })
);
console.log('Resultado:', result);
}
Execução com timeout via AbortSignal
const controller = new AbortController();
// Timeout de 10 segundos
const timeout = setTimeout(() => controller.abort(), 10000);
try {
const result = await mc.executeTool(
tool,
JSON.stringify({ query: 'heavy search' }),
{ signal: controller.signal }
);
clearTimeout(timeout);
return result;
} catch (error) {
if (error.name === 'AbortError') {
console.warn('Tool execution timed out');
return null;
}
throw error;
}
Evento toolchange — Reagindo a mudanças
O evento toolchange dispara sempre que a lista de tools muda — registro, desregistro, ou mudança em tools de iframes.
mc.addEventListener('toolchange', async () => {
// Re-descobrir tools disponíveis
const updatedTools = await mc.getTools();
console.log(`Tools atualizadas: ${updatedTools.length} disponíveis`);
// Atualizar UI se necessário
updateToolsPanel(updatedTools);
});
Caso de uso: Page Agent
Um padrão comum é construir um “page agent” que monitora tools e apresenta ao usuário:
class PageAgent {
#tools = [];
constructor() {
mc.addEventListener('toolchange', () => this.refresh());
this.refresh();
}
async refresh() {
this.#tools = await mc.getTools({
fromOrigins: ['https://trusted-widget.com']
});
this.render();
}
render() {
const panel = document.getElementById('agent-panel');
panel.innerHTML = this.#tools.map(tool => `
<div class="tool-card">
<h3>${tool.name}</h3>
<p>${tool.description}</p>
<code>${tool.inputSchema}</code>
</div>
`).join('');
}
}
Cross-origin — Compartilhando tools entre domínios
Cenário
Um e-commerce (shop.example.com) embarca um widget de pagamento (payments.provider.com) via iframe. O widget quer expor uma tool process_payment para o site pai.
Passo 1: Widget registra tool com exposedTo
// Rodando em payments.provider.com (dentro do iframe)
mc.registerTool({
name: 'process_payment',
description: 'Process a payment with the given amount and method',
inputSchema: {
type: 'object',
properties: {
amount: { type: 'number', description: 'Amount in cents' },
method: { type: 'string', enum: ['credit', 'debit', 'pix'] }
},
required: ['amount', 'method']
},
execute: async ({ amount, method }) => {
const result = await processPayment(amount, method);
return JSON.stringify({ transactionId: result.id, status: result.status });
}
}, {
exposedTo: ['https://shop.example.com'] // Apenas este origin pode ver
});
Passo 2: Site pai habilita Permissions Policy no iframe
<!-- Em shop.example.com -->
<iframe
src="https://payments.provider.com/widget"
allow="tools"
></iframe>
Passo 3: Site pai consulta tools cross-origin
// Rodando em shop.example.com
const tools = await mc.getTools({
fromOrigins: ['https://payments.provider.com']
});
const paymentTool = tools.find(t => t.name === 'process_payment');
// paymentTool agora está disponível para o agente
Regras de visibilidade cross-origin
| Condição | Resultado |
|---|---|
Tool sem exposedTo | Visível apenas para same-origin e agentes built-in |
Tool com exposedTo: ['https://A.com'] | Visível para A.com se A.com usar fromOrigins |
fromOrigins sem match em exposedTo | Tool não aparece |
iframe sem allow="tools" | Tool não pode ser registrada |
Atenção: Apenas secure origins (HTTPS) são aceitas em
exposedToefromOrigins. HTTP localhost é a exceção para desenvolvimento.
Patterns avançados
Tool com validação e retry amigável
mc.registerTool({
name: 'create_event',
description: 'Create a calendar event. Date must be in the future.',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Event title' },
date: { type: 'string', description: 'Date in ISO 8601 format (YYYY-MM-DD)' },
time: { type: 'string', description: 'Time as provided by user (e.g. "3pm", "15:00")' }
},
required: ['title', 'date']
},
execute: async ({ title, date, time }) => {
const eventDate = new Date(date);
if (isNaN(eventDate.getTime())) {
return JSON.stringify({
error: 'Invalid date format. Please use YYYY-MM-DD.',
example: '2026-07-15'
});
}
if (eventDate < new Date()) {
return JSON.stringify({
error: 'Date must be in the future.',
today: new Date().toISOString().split('T')[0]
});
}
const event = await calendar.createEvent({ title, date, time });
return JSON.stringify({
success: true,
eventId: event.id,
message: `Event "${title}" created for ${date}`
});
}
});
Dica: Retorne erros descritivos em JSON — o agente usa essa informação para corrigir o input e tentar de novo. Exceções genéricas não ajudam ninguém.
Tool com rate limiting graceful
const rateLimiter = {
calls: 0,
resetAt: Date.now() + 60000,
maxCalls: 10,
canCall() {
if (Date.now() > this.resetAt) {
this.calls = 0;
this.resetAt = Date.now() + 60000;
}
return this.calls < this.maxCalls;
}
};
mc.registerTool({
name: 'search_products',
description: 'Search product catalog by keyword.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search term' }
},
required: ['query']
},
execute: async ({ query }) => {
if (!rateLimiter.canCall()) {
return JSON.stringify({
error: 'Rate limit reached. Try again in 1 minute.',
suggestion: 'User can search manually using the search bar above.'
});
}
rateLimiter.calls++;
const results = await searchProducts(query);
return JSON.stringify(results.slice(0, 5));
},
annotations: { readOnlyHint: true, untrustedContentHint: true }
});
Registro dinâmico baseado em autenticação
async function registerAuthenticatedTools() {
const controller = new AbortController();
const user = await getCurrentUser();
if (!user) {
// Usuário não autenticado: apenas tools públicas
mc.registerTool({
name: 'login',
description: 'Navigate to login page',
inputSchema: { type: 'object', properties: {} },
execute: async () => {
window.location.href = '/login';
return null; // navegação
}
}, { signal: controller.signal });
return controller;
}
// Usuário autenticado: tools completas
mc.registerTool({
name: 'get_my_orders',
description: 'List my recent orders with status and tracking',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['all', 'pending', 'shipped', 'delivered'],
description: 'Filter by order status'
}
}
},
execute: async ({ status = 'all' }) => {
const orders = await fetchUserOrders(user.id, status);
return JSON.stringify(orders.slice(0, 10));
},
annotations: { readOnlyHint: true }
}, { signal: controller.signal });
mc.registerTool({
name: 'update_profile',
description: 'Update user profile information',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Full name' },
email: { type: 'string', description: 'Email address' },
phone: { type: 'string', description: 'Phone number' }
}
},
execute: async (updates) => {
const result = await updateProfile(user.id, updates);
return JSON.stringify({ success: true, updated: Object.keys(updates) });
},
annotations: { readOnlyHint: false }
}, { signal: controller.signal });
return controller;
}
Angular: Suporte experimental
Angular tem suporte experimental para WebMCP com integração ao ciclo de vida do dependency injection:
- Tools vinculadas ao lifecycle do DI — desregistram automaticamente quando o componente é destruído
- Signal Forms transformadas em WebMCP tools automaticamente
- Integração nativa com o sistema de reatividade do Angular
Character budgets
O Chrome recomenda limites para que tools não sobrecarreguem a context window do agente:
| Elemento | Limite recomendado |
|---|---|
| Nome da tool | 30 caracteres |
| Descrição da tool | 500 caracteres |
| Nome de parâmetro | 30 caracteres |
| Descrição de parâmetro | 150 caracteres |
| Output da tool | 1.500 caracteres |
Nota: São recomendações, não enforcement obrigatório (ainda). Mas respeite — garante compatibilidade com qualquer agente que consumir suas tools.
Checklist de implementação
Antes de publicar suas tools em produção:
- Feature detection com fallback (
document.modelContext ?? navigator.modelContext) - Nomes claros e descritivos (verbos de ação)
- Schemas com
requiredapenas para campos obrigatórios -
annotations.readOnlyHintdefinido corretamente -
annotations.untrustedContentHintpara outputs com UGC - Output truncado dentro do character budget (1.500 chars)
- Erros descritivos em JSON (não exceções genéricas)
- AbortController para cleanup em SPAs
- Cross-origin com
exposedToapenas para origins confiáveis - Testado com Model Context Tool Inspector
Próximos passos
- API Declarativa — Transforme formulários em tools sem JavaScript
- Segurança — Proteja tools contra prompt injection
- Best Practices — Estratégias para tools eficazes