River Foundry
A converted print workshop with focus booths and wide riverside tables.
- Quiet booths
- Monitor
- Step-free
Imperative tools · declarative forms · lifecycle safe
Runs in this browser
Live WebMCP workspace
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.
The prompts require discovery, a structured tool call, a page mutation, and a follow-up tool.
Real page state returned by search_lisbon_workspaces.
A converted print workshop with focus booths and wide riverside tables.
Small design-led studio with a shaded terrace and bookable project room.
Practical round-the-clock space close to the metro, built for focused days.
Calm reading-room atmosphere with natural light and a hidden courtyard.
A declarative tool made from ordinary, accessible HTML. It saves locally; it never books or pays.
Use it in your product
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
<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
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
<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
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
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();
}