入口
許可したページにscriptタグを1つ。ランチャーとゲストチャットがすぐに動きます。
アカウント識別、チャット、タップ時にリクエストされるマイク権限用のバージョン管理された Swift パッケージをインストールしてください。
アカウント識別、チャット、タップ時にリクエストされるマイク権限用のバージョン管理された Android ライブラリをインストールしてください。
お客様自身のMCPサーバーが、顧客ごとに代わって実行するアシスタントのアクションになります。
Claude、Claude Code、Cursor、または任意の MCP クライアントから OAuth 経由でプラットフォームを操作できます。
WebMCP: ページ内のアクションが自分自身をツール(予約、注文、ステータス確認など)として宣言し、確認がビルトインされています。
01クイックスタート
真実の源はお客様のアプリのままです。アシスタントは、誰がその場にいるかを示す短命の署名付きトークンを受け取り、権限を絞った別のアクターとしてお客様のツールを呼び出します。
01
身元とアカウントデータを保持し、誰がサインインしているかを示す2分間のトークンに署名します。
02
ワークスペースの設定を適用します:コンテンツ、モデル、ツールのアクセスレベル、確認ステップ、引き継ぎ。
03
一度に1人のユーザーに対してツールを応答させ、ユーザーはツールの引数ではなく、サインイン済みのベアラートークンから取得します。
つなぎ方は7通り、アシスタントは1つ: ウェブ埋め込み · サインイン済みの顧客 · iOS · Android · デスクトップ · REST API · お客様のMCPサーバー
02サインイン済みの顧客
アシスタントが尋ね手を信頼できるよう、お客様のバックエンドの認証済みエンドポイントが、安定した顧客IDと少数の安全な表示用フィールドを含む短命のトークン(ES256のJWT)に署名します。
トークンは最大120秒間有効で、ワンタイムノンスとjtiを含み、ワークスペースを識別します。秘密鍵とプロダクトセッションはブラウザやチャットに到達することはありません。
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 });
});トークンが伝えるのは誰がいるかであって、何をしてよいかではありません。 残高、デバイス、設定、そしてすべての変更を、1人のユーザーにだけ応答するツールの背後に置いてください。
03ウェブと全画面
SDKは必要になったときにお客様のバックエンドへ身元を問い合わせ、ゲストにも対応し、ログイン・ログアウト後に更新し、外部リンクはフレームの外で開き、サードパーティCookieを必要としません。
<!-- 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>サインイン済みユーザーがアシスタントのアドレスやカスタムドメインを開くときは、URLフラグメントでサインイントークンを渡してください。ページは交換の前にそれを消去します。
// 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
Swift パッケージのバージョンを固定してください。認証を行うバックエンドを接続し、プラットフォームガイドとサンプルアプリに従って OS のマイク権限を追加してください。
// Swift Package: https://github.com/serebano/busymate-ai-sdk-ios.git (exact 1.0.2)
// 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
Android ライブラリのバージョンを固定してください。チャットを読み込む前にインストールし、Activity のライフサイクル、マイク、レンダラーの復旧ガイドに従ってください。
// Gradle installation + lifecycle: https://busymate.ai/docs/guides/android-sdk
// Source: https://github.com/serebano/busymate-ai-sdk-android/tree/1.0.2
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.06デスクトップ
macOS、Windows、Electron、Linux、Tauri 向けの公式 SDKは、ホスト型チャットを隔離された WebViewに読み込み、特権ホスト層を通じて ID 要求に応答し、安全な URLを外部で開きます。SDK のバージョンを固定し、モバイルと同じように認証済みバックエンドを接続してください。
// Official desktop SDK for your platform — macOS, Windows, Electron, Linux or Tauri:
// https://github.com/serebano/busymate-ai-sdk-electron (and -macos, -windows, -linux, -tauri).
// Every SDK takes these settings and installs the identity bridge before the first load.
const chatOptions = {
url: "https://your-assistant.busymate.ai/?channel=desktop_sdk", // the window's top-level page
assistant: "your-assistant",
account: () => productAuth.currentUserId(), // null when signed out
// YOUR authenticated backend signs { token, nonce } for request.nonce.
mint: (request) => productAuth.mintAssistantIdentity(request),
};07MCPツール
ディスカバリで完全なスキーマを公開し、各ツールに3つのアクセスレベルのいずれかを与え、確認カードが必要な変更に印を付け、呼び出しのたびに認可を再確認してください。
チャットしている人なら誰でも呼び出せます。価格、稼働状況、機能など。
サインイン済みユーザーが必要です。注文状況やアカウントに関する回答など。
権限を絞ったアクタートークン、またはユーザー自身のOAuth許可を通じて、サインイン済みユーザーに代わって実行します。
すべての変更はサーバー側で確認されます。 confirm_toolsに挙げたツールは、実行前に確認カードで止まります。アシスタントが認識できないツール名も同様です。ウィジェットのフレームが変更を実行することはありません。
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.プラットフォームがお客様のサーバーのクライアントであっても、お客様のMCPクライアントのサーバーであっても、同じプロトコル一式が適用されます。
REST APIとウェブフック
1つのワークスペーススコープのAPIキーでRESTエンドポイントとMCPサーバーの両方を認証します。会話を一覧表示・読み取り、オペレーターの返信を投稿、またはポーリングの代わりにウェブフックにサブスクライブしてください。
Console → API keysで鍵を作成し、Bearerトークンとして送信してください。
curl https://busymate.ai/api/v1/conversations \
-H "Authorization: Bearer bmai_sk_..."詳細リファレンス: APIガイド, ウェブフック, OpenAPIドキュメント.
08引き継ぎとインサイト
顧客が人を求めたとき、ツールが失敗したとき、返金が上限を超えたとき、キーワードが現れたとき、その他お客様が定義した任意のイベント。
受信箱は1つ。割り当ては手動、順番、または対応中が最も少ない担当者へ。通知はアプリ内、Web Push、APNs、個人のTelegramで届きます。
会話を見守り、文脈を持ったまま参加し、同じチャットで返信します。人が対応している間、アシスタントは変更を行いません。
繰り返される質問、足りないナレッジ、失敗した経路を、実際の会話からまとめ、根拠へのリンクとともに表示します。
09ワークスペースのログイン
ワークスペースのログインURLとアカウントURLをコンソールで設定します。ゲストがサインインを選ぶとフレームを離れ、お客様の製品で認証し、元のURLへ正確に戻ります。その後、お客様のバックエンドがワンタイムのサインイントークンを発行します。Busymate AIがユーザーのパスワードや署名鍵を受け取ることはありません。
変更が触れるのは、サインイン済みユーザー本人のデータだけです。 アカウント系のツールは、委任されたアクターから顧客を受け取り、呼び出しのたびに認可を再確認し、設定した変更については確認を求めます。
10公開前チェックリスト
ワークスペースの設定を始めますか?
コンソールの連携ページが、公開済みの設定からすべての接点向けのコードと設定を生成します。
WebMCP
WebMCPはW3C Web Machine Learning Community Groupのドラフトで、ページが独自のアクションをツールとしてリストアップするための仕様です。最小限の変更から完全な制御まで、3つの方法があります。
book_appointment、check_order_status、start_return — 1つのツール、1つの仕事。
「来週火曜日の10時」、「Express」 — アシスタントが推測する必要のある内部IDは使いません。
このページが実行できるツールを提供し、ページが次に進んだら登録を解除します。
予約、支払い、返品: フラグを付けると、実行前に顧客が確認します。
確認は顧客のもとに留まります。 ツールはビジターの自分のブラウザ、あなたのオリジンで実行します。結果を伴うツールは、顧客が見る確認で停止します。exposedToはどのオリジンがそのツールを見られるかを制限します。
最小限の変更: すでにあるフォームに名前と1行の説明を追加するだけです。ブラウザがそれをツールに変え、フォームは表示されたままみんなのために機能します。
<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>完全な制御: ブラウザAPI。機能検出してください。WebMCPなしのブラウザは今日のページをそのまま取得します。Chrome 149とEdge 150はそれをオリジンの試験として実装しています。FirefoxとSafariはこの提案を検討中です。
// 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"] });アシスタントがすでにインストールされている場所では、1回の呼び出しであなたのツールをそのアシスタントと訪問するすべてのアシスタント向けにリストアップします。ブラウザAPIが存在する場所ではそのAPI経由で、他のすべての場所ではアシスタント独自のブリッジ経由で。
// 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(),
},
]);管理用MCP
OAuth 2.1を使用したMCP経由の管理ツール。クライアントは初回接続時にブラウザでサインインを開き、何も貼り付けることはありません。
https://busymate.ai/mcp| クライアント | 接続方法 |
|---|---|
| Claude | Claude のコネクタディレクトリからコネクタを追加してサインインするだけです。貼り付けるものはありません。 |
| ChatGPT and Codex | アプリは OpenAI の審査中で、まだ掲載されていません。 |
| Claude Code (CLI) | 下のコマンドを実行してください。ブラウザがOAuth サインインを処理します。 |
| Cursor | 下のmcp.jsonエントリをCursorのMCP設定に貼り付けてください。 |
| その他のMCPクライアント | クライアントに下のエンドポイントを指定します。OAuth 2.1ディスカバリーとPKCEがそれ以外を処理します。 |
サインインに使ったアカウントが、クライアントの管理できるワークスペースを決めます。変更のたびに確認を求めます。
claude mcp add --transport http busymate-ai https://busymate.ai/mcpCursor、Claude Desktop、そしてmcp.jsonを読むあらゆるクライアントがこの記述を受け付けます。リモートURLを取るクライアントには、上のエンドポイントをお使いください。
{
"mcpServers": {
"busymate-ai": {
"type": "http",
"url": "https://busymate.ai/mcp"
}
}
}最新の変更
ビルド 1623
G4: with a workspace's pii read masking ON, the transcript Markdown export (Console and MCP) now masks the Handoff header row, and the MCP draft_tenant_support_reply copilot reads the conversation masked like the Console copilot does. With the setting off both are unchanged. MCP activity records name the workspace and the result size. Each call an AI assistant makes to Busymate AI is now recorded with the workspace it targeted and how big its answer was, in bytes and rows. When a ChatGPT or Claude view shows no data, the cause can be found from the activity record alone.
ビルド 1622
The script tag now says, once in the browser console, which call replaces an old one. Identify and refreshIdentity on the embed script keep working. The first call of each prints one console note naming the call to use instead (identityChanged), so integrations can move to the same names as the Web SDK (getIdentity, identityChanged, signedOut, account). The old v1 client drop-ins under /sdk/v1 are served unchanged and now answer with a Deprecation header.
ビルド 1621
WhatsApp, Messenger and Telegram customers can sign in with your own login. A workspace can now let its chat customers sign in through the workspace's own login page. The customer sends "sign in", opens a one-time link, signs in, and sends the 6-digit code shown back in the same chat. Account tools then run as that customer with no extra card. The feature is off until the workspace lists the channels in channels.customerSignIn. ChatGPT and Claude: a write now asks first. The first call shows a preview (the card with Apply) and changes nothing; the change happens only when you press Apply or say yes in the chat . Signed-out shoppers get a reply again on stores whose site says "nobody is signed in". A signed-out shopper on a Shopify storefront (and any site whose getIdentity returns nothing) pressed Send and the chat stayed on "Still working…".