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
find_order in one tab and track_shipment in the other; and the answer. No sound.
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.