Busymate AI

Developers

Give your AI assistant real tools with MCP

Give the assistant real tools through MCP, the open standard for connecting AI to your systems. Ship it on web, iOS, Android and desktop. Manage everything from any MCP client.

claude mcp add --transport http busymate-ai https://busymate.ai/mcp
TERMINAL$claude mcp add --transport http busymate-ai https://busymate.ai/mcpconnectedJSON-RPC 2.0 · tools/call{ "jsonrpc": "2.0", "id": 12, "method": "tools/call", "params": { "name": "get_my_account", "arguments": {} } }{ "jsonrpc": "2.0", "id": 12, "result": { "content": [{ "type": "text", "text": "…" }] } }

Ways in

Choose where to start

Add the assistant to your website

One script tag on any page you allow — the launcher and guest chat work right away.

Put AI support inside your iOS app

Load the chat in a WKWebView; a small three-message bridge signs your users in.

Put AI support inside your Android app

Load the chat in a WebView; one JavaScript interface signs your users in.

Connect your MCP server as tools

Your own MCP server becomes the assistant's actions, on each customer's behalf.

Manage every workspace from your terminal

Drive the platform from Claude Code, Cursor or any MCP client over OAuth.

Let AI agents read your site

A plain-text map of your site at /llms.txt, written for AI agents.

01Quickstart

How it fits together

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.

Your customersYour website or appAssistantYour systemsYour settingsapplied automatically

01

Your app

Keeps identity and account data; signs a two-minute token that says who is signed in.

02

The assistant

Applies your workspace's settings: content, models, tool access levels, confirmation steps, handoff.

03

Your MCP server

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

YOUR HELP SITErobots-aware1–50 pagesIndexRetrieve for an answerwith a source

02Signed-in customers

Recognize the customer already signed in

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 tenant. The private key and your product session never reach the browser or the chat.

Your backendThe browserThe assistantsigns the tokenthe key never leavescarries itchecks your JWKS120s maxchecked
server/bmai-identity.tstypescript
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": "00000000-0000-0000-0000-000000000000", // Your product's registered tenant claim
      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("https://YOUR-PRODUCT-DOMAIN (set when you register the provider)")
    .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

One script for an embedded launcher

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.

index.htmlhtml
<!-- Floating "bro" 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, logout, access-token/session
  // rotation, or account switch. Do not send an identity postMessage directly.
  window.refreshAssistantIdentity = () =>
    window.BusymateAI?.refreshIdentity?.() ?? Promise.resolve();
</script>
<script
  src="https://your-assistant.busymate.ai/embed/v1.js"
  data-assistant="your-assistant"
  data-label="Ask bro"
  async></script>

Open the full-page chat at your address

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.

open-assistant.tstypescript
// 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

WKWebView with a narrow message bridge

Allow only the three versioned messages: identity request, identity response and external URL. Mint identity through the app's existing authenticated API client.

AssistantBridge.swiftswift
// SDK source: https://busymate.ai/sdk/v1/ios/BusymateAI.swift
// Load https://your-assistant.busymate.ai/?channel=ios in a WKWebView.
final class AssistantBridge: NSObject, WKScriptMessageHandler {
  let webView: WKWebView

  func userContentController(_ controller: WKUserContentController,
                             didReceive message: WKScriptMessage) {
    guard message.name == "BusymateAI",
          let body = message.body as? [String: Any],
          body["type"] as? String == "busymate.ai.v1.identity_request"
    else { return }

    Task { // mint through YOUR authenticated API — never a key in the app
      let identity = try await api.mintLaunchIdentity()
      let payload: [String: Any] = [
        "type": "busymate.ai.v1.identity",
        "token": identity.token,
        "nonce": identity.nonce,
      ]
      let data = try JSONSerialization.data(withJSONObject: payload)
      let json = String(decoding: data, as: UTF8.self)
      await webView.evaluateJavaScript("window.postMessage(\(json), '*')")
    }
  }
}

05Android

WebView with one allowlisted interface

Keep navigation on your tenant AI origin, send outside links to the system browser and expose no general-purpose native methods.

AssistantBridge.ktkotlin
// SDK source: https://busymate.ai/sdk/v1/android/BusymateAI.kt
// Load https://your-assistant.busymate.ai/?channel=android in a WebView.
class AssistantBridge(private val webView: WebView) {
  @JavascriptInterface
  fun postMessage(raw: String) {
    val message = JSONObject(raw)
    if (message.optString("type") != "busymate.ai.v1.identity_request") return

    lifecycleScope.launch { // mint through YOUR authenticated API client
      val identity = api.mintLaunchIdentity()
      val response = JSONObject()
        .put("type", "busymate.ai.v1.identity")
        .put("token", identity.token)
        .put("nonce", identity.nonce)
      webView.evaluateJavascript(
        "window.postMessage(${JSONObject.quote(response.toString())}, '*')", null
      )
    }
  }
}

webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(AssistantBridge(webView), "BusymateAINative")
// SupportChatNative + support.chat.v1.* remain accepted for shipped apps.

06Desktop

The same protocol fits Electron, Tauri and native shells

Use an isolated WebView, deny new in-view windows, open safe URLs externally and answer identity requests through the privileged host layer.

assistant-renderer.tstypescript
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 bro",
  getIdentity: () => window.productAuth.mintAssistantIdentity(),
});

window.productAuth.onSessionChanged(() => assistant.refreshIdentity());
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

Give the assistant tools without giving it your customer database

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.

Public

Anyone chatting may call it. Prices, status, features.

Identified

Needs a signed-in user. Order status, account answers.

Delegated

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.

oauth-requirements.txttext
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
delegated-call.jsonjson
// POST https://YOUR-DOMAIN/mcp
// Authorization: Bearer <the per-user OAuth token bro 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.

What the platform speaks

The same protocol set applies whether the platform is the client to your server or the server for your MCP client.

  • OAuth 2.1 with PKCE (S256); public clients register dynamically (RFC 7591).
  • Discovery through authorization-server metadata (RFC 8414) and protected-resource metadata (RFC 9728).
  • Grants: authorization_code and refresh_token; the authorization response carries the issuer (RFC 9207).
  • Resource indicators (RFC 8707) when the platform is the OAuth client to your MCP server.
  • Token exchange (RFC 8693) where configured.

08Handoff and insights

Set the rules for when a person steps in

When it hands off

A customer asks for a person, a tool fails, a refund is above your limit, a keyword appears, or any event you define.

How your team is told

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.

Your team takes over

Watch the conversation, join with the context, reply in the same chat; the assistant makes no changes while a person is active.

Insights

Repeat questions, missing knowledge and failed paths, grouped from real conversations with links to the evidence.

09Workspace login

Keep your own user base and authentication

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; Busymate AI 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

Production launch checklist

  1. 01Set up branding, SEO, your web address and suggested questions.
  2. 02Register your sign-in: issuer, JWKS, audience, claims and token age.
  3. 03Connect MCP, set each tool's access level, prove that reads are self-scoped and writes are confirmed.
  4. 04Set the handoff rules, staff the Inbox, set hours and response targets.
  5. 05Test guests, signed-in users, logout and account switch, replayed tokens, and a swapped workspace id.
  6. 06Check the full-page chat, the embed, iOS, Android, desktop, accessibility and external links.
  7. 07Publish only the version you checked; watch connection and domain health.

Ready to set up your workspace?

The Console's Integration page generates the code and settings for every surface from your published settings.

Open the Console

Management MCP

Manage every workspace from any MCP client

218 management tools over MCP with OAuth 2.1. The client opens a browser sign-in on first connection; nothing is pasted.

Discovery documents

The account you sign in with decides which workspaces the client may manage. Every change asks for confirmation.

Your MCP clientcreate_artifactJSON-RPCcreated: truehttps://your-assistant.busymate.ai/artifact/…VISIBILITYSitemapGallery

Claude Code

claude-code.shbash
claude mcp add --transport http busymate-ai https://busymate.ai/mcp

Manage the platform from any MCP client

Cursor, Claude Desktop and any client that reads an mcp.json accept this entry; clients that take a remote URL take the endpoint above.

mcp.jsonjson
{
  "mcpServers": {
    "platform-management": {
      "type": "http",
      "url": "https://busymate.ai/mcp"
    }
  }
}

Connect your first tool today

Register your MCP server, choose who may use each tool, publish. The assistant can use it the same minute.