Browser agent tools (WebMCP)
A Lakebed app can let a browser agent use it. Tag a query, mutation, or action with a tool option. The app page then registers that handler as a WebMCP tool. An agent in the user's browser can call the tool instead of clicking through the UI.
This feature is experimental. WebMCP is a draft web standard. Today a user must turn on chrome://flags/#enable-webmcp-testing in Chrome for an agent to see an app's tools. Other browsers ignore tools, and the app works as before.
Tag a handler
Pass { tool: { description, args } } as the second argument to query, mutation, or action:
import { boolean, capsule, id, mutation, query, string, table, userId } from "lakebed/server";
export default capsule({
schema: {
todos: table({ text: string(), done: boolean().default(false), ownerId: userId() }).index("by_owner", ["ownerId"])
},
queries: {
todos: query(
async (ctx) => {
const { userId } = ctx.auth.requireIdentity();
return ctx.db.todos.withIndex("by_owner", (q) => q.eq("ownerId", userId)).collect();
},
{ tool: { description: "List the current user's todos." } }
)
},
mutations: {
setTodoDone: mutation(
async (ctx, args) => {
const { userId } = ctx.auth.requireIdentity();
const todo = await ctx.db.todos.get(args.todoId);
if (!todo || todo.ownerId !== userId) {
throw new Error("Todo not found.");
}
await ctx.db.todos.update(args.todoId, { done: args.done });
},
{
tool: {
description: "Mark one of the current user's todos as done or not done. Get todo IDs from the todos tool.",
args: { todoId: id("todos"), done: boolean() }
}
}
)
}
});
The tool name is the handler name. Two tools cannot share a name, so a query and a mutation with the same name cannot both be tools.
Arguments
Build args from string(), number(), boolean(), and id("table"). Chain .optional() for an argument the agent can leave out. Tool arguments cannot use .default() or userId(). Read the caller from ctx.auth, never from an argument.
With args, the handler takes one object, typed from those fields. Client code calls it with the same object:
const setTodoDone = client.useMutation("setTodoDone");
await setTodoDone({ todoId: todo.id, done: true });
Without args, the tool takes no input and the handler takes no argument after ctx.
Read fields from the object, as in args.todoId. Optimistic updates cannot predict a mutation that destructures its argument, such as async (ctx, { todoId }) => ....
How a tool call runs
A tool call runs in the user's tab, through the same session as the app's UI. The server applies the same checks as any other call:
ctx.authis the user who has the tab open, guest or signed in.auth: { requireSignIn: true }rejects the call until the user signs in.- Your handler's ownership checks and validation still run.
Lakebed checks the agent's input against args before the call. It rejects unknown inputs, missing required inputs, and wrong types. Other callers, such as your client code, are not checked against args. Validate input in the handler as you do for any query or mutation.
A query tool reads the current result once and returns it. Lakebed marks query tools as read-only for the browser. Mutation and action tools return the handler's result.
Write descriptions for an agent
The agent picks tools from their descriptions. Say what the tool does, what it returns, and where its inputs come from. "Get todo IDs from the todos tool" tells the agent how to chain calls.
Tool names, descriptions, and arguments ship in the app's client bundle. Do not put secrets in them.
Treat tool results as data that an agent reads. If a tool returns text that other users wrote, that text can try to steer the agent. Return only the fields the agent needs.
Inspect tools
lakebed inspect lists the tools, with their kinds, arguments, and descriptions. Use --json for the full tools object. This works for lakebed dev and for hosted deploys.
lakebed inspect --port 3000
lakebed inspect <deploy-id-or-url>