Busymate AI

開発者

MCPでAIアシスタントに本物のツールを

AIとお客様のシステムをつなぐオープン標準MCPを通じて、アシスタントに本物のツールを与えます。ウェブ、iOS、Android、デスクトップに展開し、すべてを任意のMCPクライアントから管理できます。

claude mcp add --transport http busymate-ai https://busymate.ai/mcp
ターミナル$claude mcp add --transport http busymate-ai https://busymate.ai/mcp接続済みJSON-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": "…" }] } }

入口

どこから始めるか選ぶ

ウェブサイトにアシスタントを追加する

許可したページに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サーバー

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

02サインイン済みの顧客

すでにサインインしている顧客を認識する

アシスタントが尋ね手を信頼できるよう、お客様のバックエンドの認証済みエンドポイントが、安定した顧客IDと少数の安全な表示用フィールドを含む短命のトークン(ES256のJWT)に署名します。

トークンの有効期間は最大120秒で、ワンタイムのnonceとjtiを持ち、お客様のテナントを明示します。秘密鍵と製品側のセッションが、ブラウザやチャットに渡ることはありません。

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 });
});

トークンが伝えるのは誰がいるかであって、何をしてよいかではありません。 残高、デバイス、設定、そしてすべての変更を、1人のユーザーにだけ応答するツールの背後に置いてください。

03ウェブと全画面

埋め込みランチャーを実現する1つのスクリプト

SDKは必要になったときにお客様のバックエンドへ身元を問い合わせ、ゲストにも対応し、ログイン・ログアウト後に更新し、外部リンクはフレームの外で開き、サードパーティCookieを必要としません。

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>

お客様のアドレスで全画面チャットを開く

サインイン済みユーザーがアシスタントのアドレスやカスタムドメインを開くときは、URLフラグメントでサインイントークンを渡してください。ページは交換の前にそれを消去します。

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

バージョン付きの3つのメッセージ(身元リクエスト、身元レスポンス、外部URL)だけを許可します。身元の発行は、アプリ既存の認証済みAPIクライアントを通じて行ってください。

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

ナビゲーションはテナントの AI オリジン内に限定し、外部リンクはシステムブラウザで開き、汎用のネイティブメソッドは公開しないでください。

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.

06デスクトップ

同じプロトコルがElectron、Tauri、ネイティブシェルにも収まります

隔離されたWebViewを使い、ビュー内での新規ウィンドウを禁止し、安全なURLは外部で開き、身元のリクエストには特権を持つホスト層で応答してください。

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ツール

顧客データベースを渡さずに、アシスタントへツールを与える

ディスカバリで完全なスキーマを公開し、各ツールに3つのアクセスレベルのいずれかを与え、確認カードが必要な変更に印を付け、呼び出しのたびに認可を再確認してください。

公開

チャットしている人なら誰でも呼び出せます。価格、稼働状況、機能など。

本人確認済み

サインイン済みユーザーが必要です。注文状況やアカウントに関する回答など。

委任

権限を絞ったアクタートークン、またはユーザー自身のOAuth許可を通じて、サインイン済みユーザーに代わって実行します。

すべての変更はサーバー側で確認されます。 confirm_toolsに挙げたツールは、実行前に確認カードで止まります。アシスタントが認識できないツール名も同様です。ウィジェットのフレームが変更を実行することはありません。

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.

プラットフォームが話す言葉

プラットフォームがお客様のサーバーのクライアントであっても、お客様の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公開前チェックリスト

本番公開前のチェックリスト

  1. 01ブランディング、SEO、ウェブアドレス、質問の候補を設定する。
  2. 02サインインを登録する:issuer、JWKS、audience、クレーム、トークンの有効期間。
  3. 03MCPを接続し、各ツールのアクセスレベルを設定し、読み取りが本人限定で、書き込みに確認が入ることを検証する。
  4. 04引き継ぎルールを設定し、受信箱に担当者を配置し、対応時間と応答目標を決める。
  5. 05ゲスト、サインイン済みユーザー、ログアウトとアカウント切り替え、トークンの再送、ワークスペースIDの差し替えをテストする。
  6. 06全画面チャット、埋め込み、iOS、Android、デスクトップ、アクセシビリティ、外部リンクを確認する。
  7. 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

サインインに使ったアカウントが、クライアントの管理できるワークスペースを決めます。変更のたびに確認を求めます。

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

任意の MCP クライアントからプラットフォームを管理

Cursor、Claude Desktop、そしてmcp.jsonを読むあらゆるクライアントがこの記述を受け付けます。リモートURLを取るクライアントには、上のエンドポイントをお使いください。

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

最初のツールを今日つなぐ

MCPサーバーを登録し、各ツールを誰が使えるか決めて、公開する。アシスタントはその瞬間から使えます。