01Register one tool
Expose one action the current page already owns.
import { defineTool, registerTool } from '@vampaz/webmcp-kit'
interface SelectItemsInput {
ids: string[]
}
registerTool(
defineTool<SelectItemsInput>({
name: 'select_items',
description: 'Select visible inventory rows by stable item IDs.',
inputSchema: {
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'string' }
}
},
required: ['ids'],
additionalProperties: false
},
execute(input: SelectItemsInput) {
return selectRows(input.ids)
}
})
)
02Protect mutations
Use confirmation and guards for anything that changes important state.
import {
defineTool,
registerTool,
setConfirmationHandler,
type ConfirmationTool
} from '@vampaz/webmcp-kit'
interface UpdateSelectedInvoiceStatusInput {
status: 'draft' | 'sent' | 'overdue' | 'paid' | 'void'
}
registerTool(
defineTool<UpdateSelectedInvoiceStatusInput>({
name: 'update_selected_invoice_status',
description: 'Update the status for selected invoice rows.',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['draft', 'sent', 'overdue', 'paid', 'void']
}
},
required: ['status'],
additionalProperties: false
},
confirmation: {
required: true,
reason: 'Changing invoice status mutates business records.'
},
guard() {
return selectedInvoices.length > 0 || 'No invoices are selected.'
},
execute(input: UpdateSelectedInvoiceStatusInput) {
return updateSelectedInvoices(input.status)
}
})
)
setConfirmationHandler(function confirmTool(
tool: ConfirmationTool,
input: unknown,
reason: string
) {
return showApprovalDialog({
title: `Run ${tool.name}?`,
body: reason,
preview: JSON.stringify(input, null, 2)
})
})
03Call the server for secrets
Keep private APIs, payments, email, and database writes behind an app endpoint.
import { defineServerTool, registerTool } from '@vampaz/webmcp-kit'
interface SendInvoiceInput {
invoiceId: string
}
registerTool(
defineServerTool<SendInvoiceInput>({
name: 'send_invoice',
description: 'Send an invoice email from the server.',
endpoint: '/api/tools/send-invoice',
inputSchema: {
type: 'object',
properties: {
invoiceId: { type: 'string' }
},
required: ['invoiceId'],
additionalProperties: false
},
confirmation: {
required: true,
reason: 'Sending an invoice emails a customer.'
}
})
)
04Upgrade an existing form
Turn a visible form into a tool without rebuilding the workflow.
import { registerFormTool, type FormInput } from '@vampaz/webmcp-kit'
const form = document.querySelector('form')
if (form) {
registerFormTool({
form,
name: 'create_support_ticket',
description: 'Create a support ticket from the visible form.',
execute(input: FormInput) {
return createTicket({
account: String(input.account ?? ''),
subject: String(input.subject ?? ''),
body: String(input.body ?? '')
})
}
})
}
05Verify from Playwright
Inspect the registered surface and invoke tools through the app boundary.
import {
invokeWebMCPTool,
waitForWebMCPTool
} from '@vampaz/webmcp-kit/testing/playwright'
await waitForWebMCPTool(page, 'select_items')
await invokeWebMCPTool(page, {
toolName: 'select_items',
input: { ids: ['item_4', 'item_7'] },
source: 'planner'
})