Hands on

Add the XataWorks Assistant to your web app

You want the people using your web app to have an assistant on the page that can do things in it. One script tag adds the XataWorks Assistant: they sign in with their own XataWorks account, chat on your page, and the Assistant uses the WebMCP tools your page registers.

What it is

The XataWorks Assistant is a chat panel that any web app adds with one script tag. The person using your app signs in with their own XataWorks account and chats with XataWorks AI without leaving your page. When your page registers WebMCP tools, the Assistant can call them — inside turns the person starts, and under rules the person controls.

It is not the XataWorks desktop app, and there is nothing to install. The rest of this section describes the desktop app, which has its own browser and its own permission questions; this article is the Assistant’s.

Add the snippet

Put this in your page’s <head>:

<script src="https://widget.xataworks.ca/embed.js"></script>

Click the snippet to select it.

Place it before your page’s own scripts, and without async. The loader captures WebMCP registrations on navigator.modelContext as they happen, from the moment it runs; a tool registered before the loader ran can’t be seen. Loaded first and in order, it is in place before any of your scripts registers a tool.

Where the browser already has navigator.modelContext, the loader wraps it and then calls the browser’s own methods, so the browser’s own agent keeps working. Where it doesn’t, the loader defines it, so your registrations always have somewhere to go.

A page that never opens the Assistant fetches only embed.js. The panel itself loads on the first open.

What the person sees

  • A launcher. A round button in the bottom-right corner, labelled Open XataWorks Assistant for screen readers. It follows the person’s light or dark setting.
  • On a wide screen, the Assistant opens as a drawer down the right edge of the page.
  • On a phone (under 600 pixels wide), it opens as a bottom sheet over two-thirds of the screen, so your page stays in view above it. A handle at the top of the sheet expands it to full screen and collapses it again; dragging it down closes it. The sheet stays above the on-screen keyboard.
  • The first time, the panel asks them to sign in. Sign in with XataWorks opens a XataWorks window, where they sign in and are asked, for a site at app.example.com, Let XataWorks work with app.example.com?, with Allow, Don’t allow and Use another account. The window also tells them that the site can’t see their account, their chats or their saved works. Next time on the same site, it skips the question.
  • The chat, with a model picker whose first choice is Auto. Only text the person types starts a turn.
  • Tool calls in the chat. Each call shows as a line — Calling list_orders…, then Called list_orders or Couldn’t call list_orders — which opens to show what was sent and what came back.
  • Three views in the panel’s header: Tools lists the tools your page offers, each badged Reads only, Makes changes or Can delete; Permissions lists the rules the person has made for your site; Works lists what they have saved to their account.

Escape closes the panel, and focus returns to the launcher.

Give it your tools

The Assistant uses standard WebMCP: there is no XataWorks-specific API to write against. If your page already registers tools, they appear in the Assistant as they are. If it doesn’t, Hello, WebMCP walks through one tool from start to finish. Here are two, one that reads and one that deletes, in a script loaded after the snippet:

navigator.modelContext.registerTool({
  name: "list_orders",
  description: "List the orders in this account, newest first, " +
    "with each order's id, customer, date and status.",
  inputSchema: {
    type: "object",
    properties: {
      status: {
        type: "string",
        enum: ["open", "shipped", "cancelled"],
        description: "Only orders with this status"
      }
    }
  },
  annotations: { readOnlyHint: true },
  execute: async ({ status }) => {
    const query = status ? "?status=" + encodeURIComponent(status) : "";
    const response = await fetch("/api/orders" + query);
    return JSON.stringify(await response.json());
  }
});

navigator.modelContext.registerTool({
  name: "delete_order",
  description: "Delete one order by its id. This cannot be undone.",
  inputSchema: {
    type: "object",
    properties: {
      id: { type: "string", description: "The order's id" }
    },
    required: ["id"]
  },
  annotations: { destructiveHint: true },
  execute: async ({ id }) => {
    const response = await fetch("/api/orders/" + encodeURIComponent(id),
      { method: "DELETE" });
    if (!response.ok) throw new Error("Couldn't delete order " + id + ".");
    return "Deleted order " + id + ".";
  }
});

Register on navigator.modelContext: that is the registry the loader records. It accepts registerTool (with an optional {signal} whose abort unregisters the tool), provideContext({tools}), unregisterTool and clearContext, and it records up to 100 tools. Tools registered or removed later are picked up while the panel is open.

What happens when the Assistant calls one:

  • execute runs in your page, so it calls your own endpoints with the person’s own session on your site, and your existing checks apply.
  • Return a string, or MCP-shaped {content: […]}, whose text items are joined. Anything else is turned into JSON. Nothing, or null, reads as (no result).
  • When execute throws, the Assistant is told the call failed, with the error’s message, so make the message a sentence.
  • A call that hasn’t answered within 30 seconds is reported as The tool didn’t answer within 30 seconds.
  • The requestUserInteraction that execute receives always refuses in the Assistant. The person’s approval comes from the panel, before the call, as below.

Tool descriptions, schemas and results are your page’s data. The Assistant is told to treat them as data, never as instructions. Designing tools agents use well covers names, descriptions and results.

What the person is asked before a tool runs

Every call passes a gate in the panel, decided by the tool’s class and the person’s rules for your site. The Assistant reads two hints: destructiveHint: true makes a tool Can delete; otherwise readOnlyHint: true makes it Reads only; anything else, including a tool that declares no hints, is Makes changes. The class follows the tool’s current annotations.

ClassWhat happensThe choices
Reads onlyAsks, unless the person has already allowed it for this chat or always on your site, or has allowed read-only tools on your site for this chatAllow once, Allow for this chat, Allow read-only tools on your site for this chat, Always allow on your site, Deny, Always deny on your site
Makes changesAsks, unless the person has already allowed it for this chat or always on your siteAllow once, Allow for this chat, Always allow on your site, Deny, Always deny on your site
Can deleteAsks every time; no rule or earlier answer allows it in advanceAllow once, Deny

Until the person allows it, every tool asks, including tools marked read-only, the same as in the XataWorks app. Allowing read-only tools for a chat never covers a tool that makes changes or deletes. An “always deny” rule is honoured for every class. “Allow for this chat” lasts until the person starts a new chat or reloads the page. A declined call never reaches your tool; the Assistant is told The person declined this action.

The question appears in the panel, at the call, one call at a time. Its buttons wait a moment before they work, and only a real click counts. The person sees and removes their rules in the Permissions view, which lists each one with a Remove button.

The hints are the same ones the desktop app reads, which has its own classes and questions; those are in safety classes.

Big results work

A result of up to 16,384 characters goes to the Assistant as it is. A longer one is kept aside in the chat’s workspace, and the Assistant is told its size and shape instead of reading it all. It then works over the value with code that runs in a sealed sandbox, a fresh one for each run, with no network access, no access to your page and a five-second limit. It can also pass a value from the workspace to one of your tools as its arguments, and that call is asked about exactly like any other.

So a tool that returns every order still works: the Assistant counts, filters and sorts the list without it filling the conversation. The workspace lasts as long as the chat, and a new chat or a reload clears it.

Works belong to the person

The Assistant can save results to the person’s own XataWorks account as works — documents, tables, web pages, images and data — and read them back later, from any site. Saving a work doesn’t ask, because it acts on the person’s account, not your site. The person opens, renames, downloads and deletes their works in the Works view.

Your page can’t read them. Moving a work’s content into your app always goes through one of your tools, under the same questions as any other call.

What your page can and cannot do

The loader gives your page one object, window.XataWorksCopilot. That is its name; the product people see is the XataWorks Assistant. It has three methods:

XataWorksCopilot.open()
Shows the panel, creating it the first time.
XataWorksCopilot.close()
Hides the panel.
XataWorksCopilot.toggle()
One or the other.

Your page can also hide or remove the launcher and the panel’s container. That is all. There is deliberately no call that sends text or presses anything in the panel, and the panel runs in a frame from another origin, so your page cannot:

  • type or send a message for the person, choose the model, or start a turn;
  • call a tool through the Assistant on its own — your tools run only inside turns the person starts;
  • answer an approval question;
  • read the conversation, the person’s account, their sign-in, the workspace or their works.

The only changes the loader makes to your page are its own elements and, while the sheet is full screen on a phone, hiding the page’s scrolling, which it puts back as it was.

Each site is separate

The Assistant takes your site from the browser — the page’s origin, its scheme, host and port — never from anything your page says. The person allows each site separately, a sign-in for one site is never used on another, and their rules belong to them on that site alone. In their XataWorks account they see every site they have allowed, and removing one signs the Assistant out there, deletes their rules for it, and asks again next time.

Where it runs

  • Pages served over https. Plain http works only on localhost and 127.0.0.1, for development. Anywhere else the panel says XataWorks only works on secure (https) sites.
  • Not inside another site’s frame. A page framed by a different site gets XataWorks can’t run inside another site’s frame.
  • Pop-ups allowed for the sign-in window. If the browser blocks it, the panel says so and the person can sign in again once pop-ups are allowed.

Try it

A demo app with sample data has the Assistant on it. Open this address in your browser:

https://widget.xataworks.ca/demo/

Sign in with your XataWorks account, then ask the Assistant about the orders on the page.

This article stops at the Assistant. For testing your tools in the XataWorks desktop app instead, see testing and debugging in XataWorks.