Learning center
how to 9 min readReviewed Aug 31, 2026

Build your first imperative WebMCP tool from an existing UI action

Expose one narrow action while keeping the visible interface, application services, and business rules intact.

The short version: A WebMCP handler should be a thin, validated entry point into behavior the application already trusts.

Pick one narrow action

Start with a read-only or reversible action that already works through the UI. Product search, availability lookup, or saving a non-final draft is safer to learn on than checkout, deletion, permission changes, or outbound messaging.

Name the action for what it actually does. A tool called search_products should search products, not silently personalize results, add items, or collect profile information.

Move shared behavior behind one application service

The button handler and tool handler should call the same domain function. That service owns input normalization, permissions, business rules, persistence, and structured results. This prevents the agent path from becoming a privileged second implementation that drifts from the UI.

async function searchProducts(input, actor) {
  authorize(actor, 'catalog:read');
  const filters = validateSearch(input);
  return catalog.search(filters);
}

Register only when the API is available

Feature detection keeps the application usable in browsers without WebMCP. Scope registration to the route or component where the action makes sense, and give that scope an AbortController so stale tools disappear when the page context changes.

if ('modelContext' in document) {
  const registration = new AbortController();
  await document.modelContext.registerTool({
    name: 'search_products',
    title: 'Search products',
    description: 'Filter the visible catalog without changing cart state.',
    inputSchema: {
      type: 'object',
      properties: { query: { type: 'string', maxLength: 120 } },
      required: ['query'],
      additionalProperties: false
    },
    annotations: { readOnlyHint: true },
    execute: (input, { signal }) => searchProducts(input, currentUser, signal)
  }, { signal: registration.signal });
  // Call registration.abort() when this route or component is disposed.
}

Validate structure and authority

The input schema helps an agent form a request, but it is not an authorization boundary. Reject unknown properties, bound strings and arrays, re-check identity, and enforce the same tenant and object permissions as the UI and backend API. Treat every call as untrusted input even when the browser performed schema checks.

Return structured errors that distinguish invalid input, missing state, denied access, conflict, and upstream failure without leaking sensitive details. The agent should know what can be corrected and what requires a person.

Return evidence and synchronize the UI

A useful result includes stable identifiers, the state that changed, and a compact verification summary. If the action affects the visible application, update the same state store the UI reads. The person should not have to guess whether the agent and page disagree.

  • Stable record or product identifiers
  • Previous and new state for mutations
  • A visible_state_updated flag only when true
  • Postcondition checks and bounded next actions
  • No raw secrets, tokens, or unnecessary personal data

Primary references

Read the sources

Put it to work

Use the guide on a real product surface.

Review the contract in the Workbench