Imperative tools · declarative forms · lifecycle safe

WebMCP

Publish app capabilities that compatible browser agents can discover and call through visible, user-controlled UI.
Documentation

Runs in this browser

Interactive playground

No hosted fallback

Live WebMCP workspace

Plan a Lisbon workday with visible, callable tools.

A browser agent can search the visible catalog, change the shortlist, and prepare the same visit form a person uses. Every call has typed input, cancellation, validation, and visible feedback.

Checking browser
01
Try these with a compatible browser agent

The prompts require discovery, a structured tool call, a page mutation, and a follow-up tool.

02

Workspace catalog

Real page state returned by search_lisbon_workspaces.

4 places
Alcântara

River Foundry

€22/day

A converted print workshop with focus booths and wide riverside tables.

  • Quiet booths
  • Monitor
  • Step-free
Príncipe Real

Lume Studio

€28/day

Small design-led studio with a shaded terrace and bookable project room.

  • Terrace
  • Meeting room
  • Monitor
Saldanha

Grid Saldanha

€19/day

Practical round-the-clock space close to the metro, built for focused days.

  • 24/7 access
  • Quiet booths
  • Meeting room
Baixa

Baixa Library

€24/day

Calm reading-room atmosphere with natural light and a hidden courtyard.

  • Step-free
  • Terrace
  • Quiet booths
04

Visit draft

A declarative tool made from ordinary, accessible HTML. It saves locally; it never books or pays.

Use it in your product

Start complete. Customize when you need to.

Components provide the fastest production path. Composables expose the same lifecycle for a UI that is entirely yours.

Imperative tools

Register typed tools with Vue lifecycle cleanup

Registration, execution state, discovery and unregistration stay reactive.
Imperative tools
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useWebMcp } from "@desource/browser-ai-vue";

const props = defineProps<{ total: number; currency: string }>();
const error = ref("");
// Tools are removed automatically when this component unmounts.
const { registerTool, registeredTools } = useWebMcp();

onMounted(() => {
  void registerTool({
    name: "get_cart_total",
    description: "Return the current visible cart total.",
    inputSchema: { type: "object", properties: {} },
    annotations: { readOnlyHint: true },
    execute: () => ({ total: props.total, currency: props.currency })
  }).catch(cause => {
    error.value = cause instanceof Error ? cause.message : String(cause);
  });
});
</script>

<template>
  <p v-if="error" role="alert">{{ error }}</p>
  <p v-else>{{ registeredTools.length }} cart tool registered</p>
</template>

Declarative tools

Make an existing form agent-accessible

Pass your authenticated ticket action. People and agents submit through the same form handler.
Declarative tools
<script setup lang="ts">
import { ref } from "vue";
import {
  createWebMcpFieldAttributes,
  createWebMcpFormAttributes
} from "@desource/browser-ai-vue";

const props = defineProps<{
  createTicket: (subject: string) => Promise<{ id: string }>;
}>();
const status = ref("");
const tool = createWebMcpFormAttributes({
  name: "create_support_ticket",
  description: "Create a support ticket from the visible form.",
  autoSubmit: true
});

const subject = createWebMcpFieldAttributes("Short ticket subject");

function submit(rawEvent: Event) {
  const event = rawEvent as SubmitEvent & {
    agentInvoked?: boolean;
    respondWith?: (response: Promise<unknown>) => void;
  };
  event.preventDefault();
  const form = event.currentTarget as HTMLFormElement;
  const value = String(new FormData(form).get("subject") ?? "").trim();
  const response = Promise.resolve().then(() => {
    if (!value) throw new Error("Enter a ticket subject.");
    return props.createTicket(value);
  });
  if (event.agentInvoked) event.respondWith?.(response);
  void response.then(
    ticket => { status.value = "Created ticket " + ticket.id; },
    error => { status.value = error instanceof Error ? error.message : String(error); }
  );
}
</script>

<template>
  <form v-bind="tool" @submit="submit">
    <input name="subject" v-bind="subject" required />
    <button type="submit">Create ticket</button>
  </form>
  <p role="status">{{ status }}</p>
</template>

One contract, native ergonomics

Use the same capability in every supported stack.

Vue composables, React hooks, Svelte stores, Angular signals, Nuxt auto-imports, and the TypeScript core all delegate browser behavior to the same tested runtime.

React

React hook

Register from an effect, read current cart values, and remove the tool when the component unmounts.
React
import { useEffect } from "react";
import { useWebMcp } from "@desource/browser-ai-react";

export function CartTools({ total, currency }: { total: number; currency: string }) {
  const { registerTool, registeredTools } = useWebMcp();

  useEffect(() => {
    const registration = new AbortController();
    void registerTool({
      name: "get_cart_total",
      description: "Return the current cart total.",
      inputSchema: { type: "object", properties: {} },
      annotations: { readOnlyHint: true },
      execute: () => ({ total, currency })
    }, { signal: registration.signal }).catch(error => {
      if (!registration.signal.aborted) console.error(error);
    });
    return () => registration.abort();
  }, [registerTool, total, currency]);

  return <p>{registeredTools.length} cart tool registered</p>;
}

Svelte

Svelte controller

Register on mount and dispose the controller with the owning Svelte component.
Svelte
<script lang="ts">
  import { onDestroy, onMount } from "svelte";
  import { createWebMcp } from "@desource/browser-ai-svelte";

  let { total, currency } = $props<{ total: number; currency: string }>();
  const tools = createWebMcp();
  const { state } = tools;
  onDestroy(tools.dispose);
  onMount(() => {
    void tools.registerTool({
      name: "get_cart_total",
      description: "Return the current cart total.",
      inputSchema: { type: "object", properties: {} },
      annotations: { readOnlyHint: true },
      execute: () => ({ total, currency })
    }).catch(console.error);
  });
</script>

<p>{$state.registeredTools.length} cart tool registered</p>

Angular

Angular signal controller

Pass the component DestroyRef to the headless factory so registrations share its lifetime.
Angular
import { Component, DestroyRef, inject, input, type OnInit } from "@angular/core";
import { createAngularWebMcp } from "@desource/browser-ai-angular/controllers";

@Component({
  selector: "app-cart-tools",
  standalone: true,
  template: `<p>{{ tools.current().registeredTools.length }} cart tool registered</p>`
})
export class CartToolsComponent implements OnInit {
  readonly total = input.required<number>();
  readonly currency = input.required<string>();
  readonly tools = createAngularWebMcp(inject(DestroyRef));

  ngOnInit() {
    void this.tools.registerTool({
      name: "get_cart_total",
      description: "Return the current cart total.",
      inputSchema: { type: "object", properties: {} },
      annotations: { readOnlyHint: true },
      execute: () => ({ total: this.total(), currency: this.currency() })
    }).catch(console.error);
  }
}

TypeScript core

No framework required

Read your application state inside the executor. Call the returned cleanup when the cart screen is removed.
TypeScript core
import { createWebMcp } from "@desource/browser-ai";

export function mountCartTools(readCart: () => { total: number; currency: string }) {
  const tools = createWebMcp();
  void tools.registerTool({
    name: "get_cart_total",
    description: "Return the current cart total.",
    inputSchema: { type: "object", properties: {} },
    annotations: { readOnlyHint: true },
    execute: () => readCart()
  }).catch(console.error);

  return () => tools.dispose();
}