Hands on

Two apps, one bus

You want to see an agent combine two applications that know nothing about each other. Here are two, and one question neither can answer alone.

Try it

In XataWorks, open each of these in its own tab:

https://xataworks.ca/developers/examples/orders/

https://xataworks.ca/developers/examples/shipping/

Then ask: “Has order A-1002 shipped, and when will it arrive? Use the tools on the open Orders and Shipping pages.”

The two applications

Orders knows orders. For each one that has shipped, it knows a tracking number — and nothing else about the shipment.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Orders</title>
  <meta name="description" content="A WebMCP example: one of two apps on one bus.">
  <link rel="stylesheet" href="../example.css">
</head>
<body>
  <header>
    <h1>Orders</h1>
    <p>An example application for the XataWorks developer guide.
      <a href="../../two-apps-one-bus.html">How it works</a></p>
  </header>
  <main>
    <table>
      <thead>
        <tr><th>Order</th><th>Customer</th><th>Status</th></tr>
      </thead>
      <tbody id="orders"></tbody>
    </table>
  </main>
  <script>
    const orders = [
      { id: "A-1001", customer: "Priya", items: "2 × desk lamp",
        status: "Delivered", tracking: "TRK-55021" },
      { id: "A-1002", customer: "Tomás", items: "1 × standing desk",
        status: "Shipped", tracking: "TRK-55022" },
      { id: "A-1003", customer: "Mei", items: "4 × monitor arm",
        status: "Packing", tracking: null },
      { id: "A-1004", customer: "Oluwaseun", items: "1 × chair",
        status: "Awaiting payment", tracking: null }
    ];

    const rows = document.getElementById("orders");
    for (const order of orders) {
      const row = rows.insertRow();
      for (const value of [order.id, order.customer, order.status]) {
        row.insertCell().textContent = value;
      }
    }

    function register(registry) {
      registry.registerTool({
        name: "find_order",
        description: "Look up an order by its number, such as A-1002. " +
          "Returns its items, its status and, once shipped, " +
          "its tracking number.",
        inputSchema: {
          type: "object",
          properties: {
            order_id: { type: "string", description: "The order number" }
          },
          required: ["order_id"]
        },
        annotations: { readOnlyHint: true, openWorldHint: false },
        execute: async ({ order_id }) => {
          const order = orders.find((o) => o.id === order_id);
          const text = order
            ? JSON.stringify(order)
            : "No order " + order_id + ". Orders run A-1001 to A-1004.";
          return { isError: !order, content: [{ type: "text", text }] };
        }
      });
    }

    const registry = document.modelContext || navigator.modelContext;
    if (registry) register(registry);
  </script>
</body>
</html>

Shipping knows shipments by tracking number. It has never heard of an order.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Shipping</title>
  <meta name="description" content="A WebMCP example: one of two apps on one bus.">
  <link rel="stylesheet" href="../example.css">
</head>
<body>
  <header>
    <h1>Shipping</h1>
    <p>An example application for the XataWorks developer guide.
      <a href="../../two-apps-one-bus.html">How it works</a></p>
  </header>
  <main>
    <table>
      <thead>
        <tr><th>Tracking</th><th>Carrier</th><th>Status</th></tr>
      </thead>
      <tbody id="shipments"></tbody>
    </table>
  </main>
  <script>
    const shipments = [
      { tracking: "TRK-55021", carrier: "Northwind Post",
        status: "Delivered", delivered: "2026-09-28" },
      { tracking: "TRK-55022", carrier: "Northwind Post",
        status: "In transit", estimated: "2026-10-06" }
    ];

    const rows = document.getElementById("shipments");
    for (const shipment of shipments) {
      const row = rows.insertRow();
      for (const value of [shipment.tracking, shipment.carrier,
                           shipment.status]) {
        row.insertCell().textContent = value;
      }
    }

    function register(registry) {
      registry.registerTool({
        name: "track_shipment",
        description: "Track a shipment by its tracking number, " +
          "such as TRK-55022. Returns the carrier, the status and " +
          "the delivery date or estimate.",
        inputSchema: {
          type: "object",
          properties: {
            tracking_number: {
              type: "string", description: "The tracking number"
            }
          },
          required: ["tracking_number"]
        },
        annotations: { readOnlyHint: true, openWorldHint: false },
        execute: async ({ tracking_number }) => {
          const found = shipments.find(
            (s) => s.tracking === tracking_number);
          const text = found
            ? JSON.stringify(found)
            : "No shipment " + tracking_number + ".";
          return { isError: !found, content: [{ type: "text", text }] };
        }
      });
    }

    const registry = document.modelContext || navigator.modelContext;
    if (registry) register(registry);
  </script>
</body>
</html>

What happened

Orders and Shipping open in two tabs; the request; one site-permission question, answered with Allow read-only tools on this website; the agent calling find_order in one tab and track_shipment in the other; and the answer. No sound.
The XataWorks window after the run. The chat shows a line reading Calling Find Order via WebMCP, the agent noting that order A-1002 has shipped with tracking number TRK-55022, a line reading Calling Track Shipment via WebMCP, and a reply: order A-1002, Tomás's standing desk, has shipped, with a table giving status in transit, carrier Northwind Post, tracking TRK-55022 and estimated delivery 6 October 2026. The browser panel shows the Orders tab, with a Shipping tab beside it.
One answer, drawn from two applications: one call in each tab. Neither application was changed to make this possible.

The agent listed the open tabs, called find_order in the Orders tab, read the tracking number from its result, and called track_shipment in the Shipping tab with it. Each call names the tab it is for, which is how one chat reaches several applications.

Why the join belongs to the agent

Neither application had to integrate with the other. Each stayed small, owned its own data, and described its own operations well. The join — order to tracking number to shipment — happened on the bus, at the moment someone needed it.

That is the case for plugging your application in. You are not building an integration with each of the applications your users also use; you are making yours one more device on a bus that already reaches theirs.

One host, one question

Both examples are served from xataworks.ca, so one answer to the site-permission question covered both. On the open web, two applications are usually two sites and are asked about separately. When they share a host — tenants of one service, or projects under one domain — a rule can give each its own answers: permission scopes.