入口
どこから始めるか選ぶ
ウェブサイトにアシスタントを追加する
許可したページにscriptタグを1つ。ランチャーとゲストチャットがすぐに動きます。
iOSアプリの中にAIサポートを置く
WKWebViewでチャットを読み込み、3つのメッセージからなる小さなブリッジでユーザーをサインインさせます。
Androidアプリの中にAIサポートを置く
WebViewでチャットを読み込み、1つのJavaScriptインターフェースでユーザーをサインインさせます。
MCPサーバーをツールとしてつなぐ
お客様自身のMCPサーバーが、顧客ごとに代わって実行するアシスタントのアクションになります。
ターミナルからすべてのワークスペースを管理する
Claude Code、Cursor、その他のMCPクライアントからOAuth経由でプラットフォームを操作します。
AIエージェントにサイトを読ませる
AIエージェント向けに書かれた、/llms.txtというプレーンテキストのサイトマップ。
01クイックスタート
全体の組み立て方
真実の源はお客様のアプリのままです。アシスタントは、誰がその場にいるかを示す短命の署名付きトークンを受け取り、権限を絞った別のアクターとしてお客様のツールを呼び出します。
01
お客様のアプリ
身元とアカウントデータを保持し、誰がサインインしているかを示す2分間のトークンに署名します。
02
アシスタント
ワークスペースの設定を適用します:コンテンツ、モデル、ツールのアクセスレベル、確認ステップ、引き継ぎ。
03
お客様のMCPサーバー
一度に1人のユーザーに対してツールを応答させ、ユーザーはツールの引数ではなく、サインイン済みのベアラートークンから取得します。
つなぎ方は7通り、アシスタントは1つ: ウェブ埋め込み · サインイン済みの顧客 · iOS · Android · デスクトップ · REST API · お客様のMCPサーバー
02サインイン済みの顧客
すでにサインインしている顧客を認識する
アシスタントが尋ね手を信頼できるよう、お客様のバックエンドの認証済みエンドポイントが、安定した顧客IDと少数の安全な表示用フィールドを含む短命のトークン(ES256のJWT)に署名します。
トークンの有効期間は最大120秒で、ワンタイムのnonceと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": "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 });
});トークンが伝えるのは誰がいるかであって、何をしてよいかではありません。 残高、デバイス、設定、そしてすべての変更を、1人のユーザーにだけ応答するツールの背後に置いてください。
03ウェブと全画面
埋め込みランチャーを実現する1つのスクリプト
SDKは必要になったときにお客様のバックエンドへ身元を問い合わせ、ゲストにも対応し、ログイン・ログアウト後に更新し、外部リンクはフレームの外で開き、サードパーティCookieを必要としません。
<!-- 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>お客様のアドレスで全画面チャットを開く
サインイン済みユーザーがアシスタントのアドレスやカスタムドメインを開くときは、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
限定的なメッセージブリッジを備えたWKWebView
バージョン付きの3つのメッセージ(身元リクエスト、身元レスポンス、外部URL)だけを許可します。身元の発行は、アプリ既存の認証済みAPIクライアントを通じて行ってください。
// 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
ナビゲーションはテナントの AI オリジン内に限定し、外部リンクはシステムブラウザで開き、汎用のネイティブメソッドは公開しないでください。
// 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.06デスクトップ
同じプロトコルがElectron、Tauri、ネイティブシェルにも収まります
隔離されたWebViewを使い、ビュー内での新規ウィンドウを禁止し、安全なURLは外部で開き、身元のリクエストには特権を持つホスト層で応答してください。
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ツール
顧客データベースを渡さずに、アシスタントへツールを与える
ディスカバリで完全なスキーマを公開し、各ツールに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 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.プラットフォームが話す言葉
プラットフォームがお客様のサーバーのクライアントであっても、お客様のMCPクライアントのサーバーであっても、同じプロトコル一式が適用されます。
- OAuth 2.1とPKCE(S256)。パブリッククライアントは動的に登録されます(RFC 7591)。
- 認可サーバーのメタデータ(RFC 8414)と保護リソースのメタデータ(RFC 9728)によるディスカバリ。
- グラントはauthorization_codeとrefresh_token。認可レスポンスはissuerを含みます(RFC 9207)。
- プラットフォームがお客様のMCPサーバーに対するOAuthクライアントとなる場合のリソースインジケーター(RFC 8707)。
- 設定されている場合のトークン交換(RFC 8693)。
08引き継ぎとインサイト
人が加わるタイミングのルールを決める
引き継ぐ条件
顧客が人を求めたとき、ツールが失敗したとき、返金が上限を超えたとき、キーワードが現れたとき、その他お客様が定義した任意のイベント。
チームへの通知方法
受信箱は1つ。割り当ては手動、順番、または対応中が最も少ない担当者へ。通知はアプリ内、Web Push、APNs、個人のTelegramで届きます。
チームが引き継ぐ
会話を見守り、文脈を持ったまま参加し、同じチャットで返信します。人が対応している間、アシスタントは変更を行いません。
インサイト
繰り返される質問、足りないナレッジ、失敗した経路を、実際の会話からまとめ、根拠へのリンクとともに表示します。
09ワークスペースのログイン
自社のユーザー基盤と認証をそのまま使う
ワークスペースのログインURLとアカウントURLをコンソールで設定します。ゲストがサインインを選ぶとフレームを離れ、お客様の製品で認証し、元のURLへ正確に戻ります。その後、お客様のバックエンドがワンタイムのサインイントークンを発行します。Busymate AIがユーザーのパスワードや署名鍵を受け取ることはありません。
変更が触れるのは、サインイン済みユーザー本人のデータだけです。 アカウント系のツールは、委任されたアクターから顧客を受け取り、呼び出しのたびに認可を再確認し、設定した変更については確認を求めます。
10公開前チェックリスト
本番公開前のチェックリスト
- 01ブランディング、SEO、ウェブアドレス、質問の候補を設定する。
- 02サインインを登録する:issuer、JWKS、audience、クレーム、トークンの有効期間。
- 03MCPを接続し、各ツールのアクセスレベルを設定し、読み取りが本人限定で、書き込みに確認が入ることを検証する。
- 04引き継ぎルールを設定し、受信箱に担当者を配置し、対応時間と応答目標を決める。
- 05ゲスト、サインイン済みユーザー、ログアウトとアカウント切り替え、トークンの再送、ワークスペースIDの差し替えをテストする。
- 06全画面チャット、埋め込み、iOS、Android、デスクトップ、アクセシビリティ、外部リンクを確認する。
- 07確認したバージョンだけを公開し、接続とドメインの状態を監視する。
ワークスペースの設定を始めますか?
コンソールの連携ページが、公開済みの設定からすべての接点向けのコードと設定を生成します。
管理用MCP
任意のMCPクライアントから、すべてのワークスペースを管理する
OAuth 2.1に対応した218個の管理ツールをMCPで提供します。クライアントは初回接続時にブラウザでのサインインを開くだけで、貼り付ける作業はありません。
ディスカバリ文書
- MCPエンドポイント
- https://busymate.ai/mcp
- 認可サーバーのメタデータ(RFC 8414)
- https://busymate.ai/.well-known/oauth-authorization-server/mcp
- 保護リソースのメタデータ(RFC 9728)
- https://busymate.ai/.well-known/oauth-protected-resource/mcp
サインインに使ったアカウントが、クライアントの管理できるワークスペースを決めます。変更のたびに確認を求めます。
Claude Code
claude mcp add --transport http busymate-ai https://busymate.ai/mcp任意の MCP クライアントからプラットフォームを管理
Cursor、Claude Desktop、そしてmcp.jsonを読むあらゆるクライアントがこの記述を受け付けます。リモートURLを取るクライアントには、上のエンドポイントをお使いください。
{
"mcpServers": {
"platform-management": {
"type": "http",
"url": "https://busymate.ai/mcp"
}
}
}