Developers
MCP is the open standard for connecting AI to your systems: register your server, choose who may use each tool, publish. Ship the assistant on web, iOS, Android and desktop, and manage every workspace from any MCP client.
claude mcp add --transport http busymate-ai https://busymate.ai/mcpWays in
One script tag on any page you allow — the launcher and guest chat work right away.
Install the versioned Swift package for account identity, chat and tap-triggered microphone permission.
Install the versioned Android library for account identity, chat and tap-triggered microphone permission.
Your own MCP server becomes the assistant's actions, on each customer's behalf.
Drive the platform from Claude Code, Cursor or any MCP client over OAuth.
WebMCP: your pages list their own actions as tools — book, order, check a status — with confirmation built in.
01Quickstart
Your app stays the source of truth. The assistant receives a short-lived signed token that says who is present, and calls your tools as a separate, scoped actor.
01
Keeps identity and account data; signs a two-minute token that says who is signed in.
02
Applies your workspace's settings: content, models, tool access levels, confirmation steps, handoff.
03
Answers tools for one user at a time, taking the user from the signed-in bearer token — never from tool arguments.
7 ways to connect, one assistant: Web embed · Signed-in customers · iOS · Android · Desktop · REST API · Your MCP server
02Signed-in customers
So the assistant can trust who is asking, one authenticated endpoint on your backend signs a short-lived token (an ES256 JWT) with your stable customer id and a few safe display fields.
The token lives at most 120 seconds, carries a one-time nonce and jti, and names your workspace. The private key and your product session never reach the browser or the chat.
import { SignJWT, importJWK } from "jose";
// POST https://YOUR-PRODUCT-DOMAIN/api/bmai/identity — requires YOUR OWN logged-in product session.
app.post("/api/bmai/identity", requireSession, async (req, res) => {
// Fresh on EVERY mint. Never persist the launch token/nonce in localStorage,
// sessionStorage, cookies, React state, or module state: every assistant
// launch consumes this pair exactly once.
const nonce = typeof req.body?.nonce === "string" ? req.body.nonce : "";
if (!/^[A-Za-z0-9_-]{32,200}$/.test(nonce)) return res.status(400).json({ error: "invalid_nonce" });
const key = await importJWK(JSON.parse(process.env.AI_LAUNCH_PRIVATE_JWK), "ES256");
const token = await new SignJWT({
tenant_id: process.env.BMAI_TENANT_ID, // the tenant id shown in Console → Identity
nonce, // equals the sibling field below
name: req.user.displayName, // optional low-sensitivity display claim
})
.setProtectedHeader({ alg: "ES256", kid: process.env.AI_LAUNCH_KEY_ID })
.setIssuer(process.env.BMAI_ISSUER) // the Issuer you registered in Console → Identity
.setAudience("busymate-ai")
.setSubject(req.user.id) // IMMUTABLE internal account id; never email/phone/session id
.setJti(crypto.randomUUID()) // one-time (replay-protected)
.setIssuedAt()
.setExpirationTime("120s") // <= registered max age (120s)
.sign(key);
res.set("Cache-Control", "no-store");
res.status(201).json({ token, nonce, expiresIn: 120 });
});The token says who is there, not what they may do. Keep balances, devices, settings and every change behind tools that answer for one user only.
03Web and full page
The SDK asks your backend for identity when it needs one, works for guests, refreshes after login and logout, opens external links outside the frame, and needs no third-party cookies.
<!-- Floating "mate" launcher for Your product.
Anonymous chat works immediately on the allowed origins. -->
<script>
function newLaunchNonce() {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
window.BusymateAI = {
getIdentity: async () => {
const accessToken = await getProductAccessToken(); // YOUR existing auth helper
if (!accessToken) return null;
const nonce = newLaunchNonce();
const returnTo = new URL(location.href); returnTo.search = ""; returnTo.hash = "";
const r = await fetch("https://YOUR-PRODUCT-DOMAIN/api/bmai/identity", {
method: "POST", credentials: "include", cache: "no-store",
headers: { "content-type": "application/json", authorization: "Bearer " + accessToken },
body: JSON.stringify({ nonce, returnTo: returnTo.href }),
});
if (r.status === 401) return null; // signed out -> anonymous chat
if (!r.ok) throw new Error("AI identity mint failed (" + r.status + ")");
// MUST be a newly minted { token, nonce } pair on every call. Never
// persist either value in localStorage, sessionStorage, cookies, React
// state, or module state.
const identity = await r.json();
if (identity.nonce !== nonce) throw new Error("AI identity nonce mismatch");
return { token: identity.token, nonce: identity.nonce };
},
};
// Call after YOUR product completes login, an access-token/session
// rotation, or an account switch — upgrades the live conversation in
// place, same thread. Do not send an identity postMessage directly.
window.assistantIdentityChanged = () =>
window.BusymateAI?.identityChanged?.() ?? Promise.resolve(false);
// Call after YOUR product completes logout. Without this the signed-out
// visitor keeps a verified chat and the previous transcript until the tab
// closes — a privacy defect, not a missing nicety.
window.assistantSignedOut = () =>
window.BusymateAI?.signedOut?.("signed_out");
</script>
<script
src="https://your-assistant.busymate.ai/embed/v1.js"
data-assistant="your-assistant"
data-label="Ask mate"
async></script>When a signed-in user opens your assistant's address or your custom domain, pass the sign-in token in the URL fragment. The page clears it before the exchange.
// Full-page open with identity in the URL FRAGMENT — never sent in the
// request line, referrer, or logs; the destination strips it before exchange.
function newLaunchNonce() {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
const nonce = newLaunchNonce();
const accessToken = await getProductAccessToken(); // YOUR existing auth helper
if (!accessToken) throw new Error("Sign in before opening an identified assistant");
const returnTo = new URL(location.href); returnTo.search = ""; returnTo.hash = "";
const response = await fetch("https://YOUR-PRODUCT-DOMAIN/api/bmai/identity", {
method: "POST", credentials: "include", cache: "no-store",
headers: { "content-type": "application/json", authorization: "Bearer " + accessToken },
body: JSON.stringify({ nonce, returnTo: returnTo.href }),
});
if (response.status === 401) throw new Error("Sign in before opening an identified assistant");
if (!response.ok) throw new Error("AI identity mint failed (" + response.status + ")");
const identity = await response.json();
if (typeof identity.token !== "string" || typeof identity.nonce !== "string") {
throw new Error("AI identity mint returned an invalid response");
}
if (identity.nonce !== nonce) throw new Error("AI identity nonce mismatch");
const url = new URL("https://your-assistant.busymate.ai/");
url.hash = new URLSearchParams({
bmai_token: identity.token,
bmai_nonce: identity.nonce,
}).toString();
location.assign(url);04iPhone
Pin the Swift package version. Connect your authenticated backend and add native microphone permission with the platform guide and example app.
iOS installation, configuration and demo
// Swift Package: https://github.com/serebano/busymate-ai-sdk-ios.git (exact 1.0.1)
// Full lifecycle + demo: https://busymate.ai/docs/guides/ios-sdk
import WebKit
import BusymateAI
@MainActor
func installAssistant(
on webView: WKWebView,
account: @escaping () -> String?,
mint: @escaping @MainActor (BusymateBridge.MintRequest) async throws -> BusymateBridge.MintToken?
) {
BusymateBridge.install(on: webView, config: .init(
assistant: "your-assistant",
origins: ["https://your-assistant.busymate.ai"],
account: account,
mint: mint
))
// mint calls YOUR authenticated backend with request.nonce; never cache tokens.
// Backend endpoint: https://YOUR-PRODUCT-DOMAIN/api/bmai/identity
webView.load(URLRequest(url: URL(string: "https://your-assistant.busymate.ai/?channel=ios")!))
}
// Microphone: declare NSMicrophoneUsageDescription; install and retain
// BusymateMicrophone before load, and forward WKUIDelegate media permission.
// Call BusymateBridge.accountChanged() on login/logout/account changes.05Android
Pin the Android library version. Install before loading chat and follow the Activity lifecycle, microphone and renderer recovery guide.
Android installation, configuration and demo
// Gradle installation + lifecycle: https://busymate.ai/docs/guides/android-sdk
// Source: https://github.com/serebano/busymate-ai-sdk-android/tree/1.0.0
import android.webkit.WebView
import ai.busymate.bridge.BusymateBridge
fun installAssistant(
webView: WebView,
account: () -> String?,
mint: (Map<String, Any?>, (BusymateBridge.MintResult) -> Unit) -> Unit
) {
BusymateBridge.install(webView, BusymateBridge.Config(
assistant = "your-assistant",
origins = listOf("https://your-assistant.busymate.ai"),
account = account,
mint = mint
))
// mint calls YOUR authenticated backend with request["nonce"], then done(result).
// Backend endpoint: https://YOUR-PRODUCT-DOMAIN/api/bmai/identity
webView.loadUrl("https://your-assistant.busymate.ai/?channel=android")
}
// Microphone: declare INTERNET + RECORD_AUDIO; construct and retain
// BusymateMicrophone in Activity.onCreate BEFORE STARTED/loadUrl; forward
// WebChromeClient.onPermissionRequest. See the guide for renderer recovery.
// Call BusymateBridge.accountChanged() on login/logout/account changes.06Desktop
Use an isolated WebView, deny new in-view windows, open safe URLs externally and answer identity requests through the privileged host layer.
import { mountBusymateAI } from "https://busymate.ai/sdk/v1/index.js";
// productAuth is a narrow preload/Tauri command bridge. It calls YOUR
// authenticated backend; no cookie, signing key, or refresh token is exposed
// to the renderer.
const assistant = await mountBusymateAI({
assistant: "your-assistant",
origin: "https://your-assistant.busymate.ai",
label: "Ask mate",
getIdentity: () => window.productAuth.mintAssistantIdentity(),
});
window.productAuth.onSessionChanged((session) => {
if (session) void assistant.identityChanged();
else assistant.signedOut();
});
assistant.open();
// Electron main process (Tauri: use the equivalent shell/open allowlist):
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const target = new URL(url);
if (target.protocol === "https:" || target.protocol === "http:") {
void shell.openExternal(target.href);
}
return { action: "deny" };
});07MCP tools
Publish complete schemas at discovery. Give every tool one of three access levels, mark the changes that need a confirmation card, and re-check authorization on every call.
Anyone chatting may call it. Prices, status, features.
Needs a signed-in user. Order status, account answers.
Acts on the signed-in user's behalf through a scoped actor token or the user's own OAuth grant.
Every change is confirmed on the server. Tools you list in confirm_tools stop at a confirmation card before they run; so does any tool name the assistant does not recognize. Widget frames never run a change.
MCP endpoint ............ https://YOUR-DOMAIN/mcp
RFC 8707 resource ....... https://YOUR-DOMAIN/mcp (the token is audience-bound to this)
AS metadata (RFC 8414) .. https://YOUR-DOMAIN/.well-known/oauth-authorization-server
Resource meta (RFC 9728) https://YOUR-DOMAIN/.well-known/oauth-protected-resource
Client registration ..... Dynamic (RFC 7591) — public client, no secret
PKCE .................... S256 required (RFC 7636)
Authorization response .. iss parameter checked (RFC 9207)
Grants .................. authorization_code + refresh_token
Delegated tools/call .... no bearer -> 401; user derived from the bearer,
NEVER from an account id in tool arguments
Customer experience ..... one separate Authorize account tools action is expected;
use signed_actor_token instead for automatic SSO// POST https://YOUR-DOMAIN/mcp
// Authorization: Bearer <the per-user OAuth token mate obtained>
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": { "name": "get_my_account", "arguments": {} }
}
// Your server verifies the bearer, derives the user from its signed subject,
// and returns ONLY that user's data.The same protocol set applies whether the platform is the client to your server or the server for your MCP client.
REST API and webhooks
One workspace-scoped API key authenticates the REST endpoints and the MCP server alike. List and read conversations, post an operator reply, or subscribe to a webhook instead of polling.
Create a key in Console → API keys, then send it as a Bearer token.
curl https://busymate.ai/api/v1/conversations \
-H "Authorization: Bearer bmai_sk_..."Full reference: the API guide, the webhooks guide, the OpenAPI document.
08Handoff and insights
A customer asks for a person, a tool fails, a refund is above your limit, a keyword appears, or any event you define.
One Inbox; assignment by hand, in turns or to whoever has the fewest open chats; alerts in the app, by Web Push, APNs and personal Telegram.
Watch the conversation, join with the context, reply in the same chat; the assistant makes no changes while a person is active.
Repeat questions, missing knowledge and failed paths, grouped from real conversations with links to the evidence.
09Workspace login
Configure the workspace login and account URLs in the Console. A guest who chooses Sign in leaves the frame, authenticates on your product, and returns to the exact originating URL. Your backend then supplies a one-time sign-in token; AI Assistant never receives the user's password or your signing key.
A change only ever touches the signed-in user's own data. Account tools take the customer from the delegated actor, re-check authorization on every call, and ask for confirmation on the changes you configure.
10Launch checklist
Ready to set up your workspace?
The Console's Integration page generates the code and settings for every surface from your published settings.
WebMCP
WebMCP is the W3C Web Machine Learning Community Group's draft for pages that list their own actions as tools. Three ways in, from the smallest change to the fullest control.
book_appointment, check_order_status, start_return — one tool, one job.
“next Tuesday at ten”, “Express” — never an internal id the assistant has to guess.
Offer the tools this page can run; unregister them when the page moves on.
A booking, a payment, a return: flag it, and the customer confirms before it runs.
Confirmation stays with the customer. Tools run in the visitor's own browser, on your origin. A tool you mark as consequential stops at a confirmation the customer sees; exposedTo limits which origins may see a tool at all.
The smallest change: a name and a one-line description on a form you already have. The browser turns it into a tool; the form stays visible and keeps working for everyone.
<form action="/book" toolname="book_appointment"
tooldescription="Book an appointment for a date, a time and a service."
toolautosubmit>
<input name="date" type="date" toolparamdescription="The day, e.g. next Tuesday">
<input name="time" type="time" toolparamdescription="The start time">
<select name="service" toolparamdescription="Which service to book">
<option>Consultation</option>
<option>Follow-up</option>
</select>
<button type="submit">Book</button>
</form>Full control: the browser API. Feature-detect it — browsers without WebMCP simply get the page as it is today. Chrome 149 and Edge 150 carry it as an origin trial; Firefox and Safari are reviewing the proposal.
// The browser API (draft). Load the polyfill BEFORE this file so every browser has it
// (it stands down where the browser is native):
// <script src="https://busymate.ai/webmcp/polyfill-1.0.1.js" integrity="sha384-pLZ5lO3gwZIn8Nim+hDrUQY+bvpliRtZhrS1O1OGRFB3lvoW0sZ71DUFHIk3m+DG" crossorigin="anonymous"></script>
const ctx = document.modelContext ?? navigator.modelContext;
if (!ctx) throw new Error("no WebMCP registry: the polyfill did not load");
ctx?.registerTool({
name: "start_return",
description: "Start a return for a delivered order. Asks the customer to confirm first.",
inputSchema: { type: "object", properties: { orderNumber: { type: "string", description: "The order number, as printed on the receipt" } }, required: ["orderNumber"] },
annotations: { consequentialHint: true },
execute: async ({ orderNumber }) => startReturn(orderNumber),
}, { exposedTo: ["self"] });Where the assistant is already installed, one call lists your tools for it and for every other assistant that visits — on the browser API where it exists, and over the assistant's own bridge everywhere else.
// One call, both transports: the browser's own WebMCP where it exists,
// and the assistant's bridge everywhere else (Safari, Firefox, app WebViews).
BusymateAI.registerPageTools([
{
name: "check_order_status",
description: "Look up an order by its number and say where it is.",
inputSchema: {
type: "object",
properties: { orderNumber: { type: "string", description: "The order number, as printed on the receipt" } },
required: ["orderNumber"],
additionalProperties: false,
},
annotations: { readOnlyHint: true },
execute: async ({ orderNumber }) => (await fetch(`/api/orders/${orderNumber}`)).json(),
},
]);The adoption how-to and the copy-paste promptThe plain-language page for your team
Management MCP
Management tools over MCP with OAuth 2.1. The client opens a browser sign-in on first connection; nothing is pasted.
https://busymate.ai/mcp| Client | How it connects |
|---|---|
| Claude Code (CLI) | Run the command below — the browser handles OAuth sign-in. |
| Cursor | Paste the mcp.json entry below into Cursor's MCP settings. |
| Other MCP clients | Give the client the endpoint below; OAuth 2.1 discovery + PKCE handle the rest. |
The account you sign in with decides which workspaces the client may manage. Every change asks for confirmation.
claude mcp add --transport http busymate-ai https://busymate.ai/mcpCursor, Claude Desktop and any client that reads an mcp.json accept this entry; clients that take a remote URL take the endpoint above.
{
"mcpServers": {
"busymate-ai": {
"type": "http",
"url": "https://busymate.ai/mcp"
}
}
}Latest changes
build 1215
Keeping an assistant now saves every page already read by its website preview before setup can publish it. The saved pages survive preview expiry and remain available in Console even when the workspace's plan limits how much can enter its searchable knowledge index. Console distinguishes saved pages from indexed pages, explains a paused import, and lets authorized members read the original page text, headings and retained details through the existing knowledge page view. The same saved-page view is available through the existing management MCP tool. An interrupted import resumes from its saved cursor through the normal knowledge writer and plan limits. Worker completion and cursor updates require the current lease, so an older worker cannot overwrite a newly claimed job. Conversation sidebar accessibility labels follow the interface language. Hosted business chats keep their business context when selecting and reopening a conversation. Contact and product links in answers resolve to the published business website. Independently checked machine translations can be approved with truthful machine-review provenance. Existing human review keeps its current behavior, while automatic review now pins both the reviewed English source and translated value so changed rows are held for another review.
build 1214
Request human support directly in chat, with optional contact details.
build 1213
Valid onboarding forms keep their current behavior.