Browse the docs

Build

Agent tools (WebMCP)

Expose queries, mutations, and actions to browser agents as WebMCP tools with the tool option.

A fuast app can let an agent in the user's browser use it directly. Tag a query, mutation, or action with a tool option, and the app page registers it as a WebMCP tool. The agent calls the tool instead of clicking through the UI.

WebMCP is a draft standard. Today a user turns on chrome://flags/#enable-webmcp-testing in Chrome for agents to see tools. Other browsers ignore them, and the app works as before.

Tag a handler

Pass { tool: { description, args } } as the second argument:

import { boolean, capsule, id, mutation, query, string, table, userId } from "fuast/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)).order("desc").collect();
      },
      { tool: { description: "List the current user's todos, newest first. Each has id, text, and done." } }
    )
  },

  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 done or not done. Get todo ids from the todos tool.",
          args: { todoId: id("todos"), done: boolean() }
        }
      }
    )
  }
});

The tool name is the handler name, so it must be unique across queries, mutations, and actions.

Arguments

Build args from string(), number(), boolean(), and id("table"). Chain .optional() for inputs the agent may leave out. Tool arguments cannot use .default(), userId(), or json(). The caller always comes from ctx.auth, never from an argument.

With args, the handler takes one object typed from those fields, and your client calls it the same way:

const setTodoDone = client.useMutation("setTodoDone");
await setTodoDone({ todoId: todo.id, done: true });

Without args, the tool takes no input, and the handler takes nothing after ctx.

How a tool call runs

The call runs in the user's tab, over the app's own connection:

  • ctx.auth is whoever has the tab open.
  • Sharing rules and requireSignIn apply as for any call.
  • Your validation and ownership checks run as usual.

fuast checks the agent's input against args before calling: unknown keys, missing required keys, and wrong types are rejected. Calls from your own client code are not checked against args, so validate inside the handler as always.

A query tool reads the current result once and is marked read-only to the browser. Mutation and action tools return the handler's result.

Write descriptions for agents

The agent chooses tools by description. 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 argument shapes ship in the client bundle. Do not put secrets in them.

Tool results are read by an agent. If a result contains text other people wrote, that text can try to steer the agent. Return only the fields the agent needs.

Inspect tools

fuast inspect --port 3000
fuast inspect my-app --json      # full tools object

GET <app url>/__fuast/tools returns the same list, and the dashboard Tools tab shows it.