WebMCP Kit

Developer start

Add one safe app action first.

Start with a narrow tool, give it a strict input schema, then verify it from tests or devtools before adding planner complexity.
01

Register 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)
    }
  })
)
02

Protect 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)
  })
})
03

Call 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.'
    }
  })
)
04

Upgrade 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 ?? '')
      })
    }
  })
}
05

Verify 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'
})
Before adding AIMake the tool contract boringly clear.
  • Tool name is stable and specific.
  • Schema is an object with required fields and no extra properties.
  • IDs come from current page context, not model guesses.
  • Mutating actions require confirmation.
  • Guards reject unavailable records or impossible quantities.
  • Tests cover the success path and at least one blocked path.
Going furtherHosted planner integration docs

Publishable keys, allowed origins, key rotation, and troubleshooting for the hosted planner are covered in the integration README. To see the contract in action first, start from the demo overview.