시작하는 방법
허용한 페이지에 스크립트 태그 하나만 넣으면 런처와 방문자 채팅이 바로 동작합니다.
계정 식별, 채팅, 컨트롤을 탭할 때 요청되는 마이크 권한을 위해 버전이 지정된 Swift 패키지를 설치하세요.
계정 식별, 채팅, 컨트롤을 탭할 때 요청되는 마이크 권한을 위해 버전이 지정된 Android 라이브러리를 설치하세요.
고객님의 MCP 서버가 각 고객을 대신해 어시스턴트가 수행할 작업이 됩니다.
OAuth를 통해 Claude, Claude Code, Cursor 또는 어떤 MCP 클라이언트에서든 플랫폼을 운영하세요.
WebMCP: 페이지가 예약, 주문, 상태 확인 같은 자체 작업을 도구로 나열하며, 확인 절차가 내장되어 있습니다.
01빠른 시작
진실의 원천은 고객님의 앱입니다. 어시스턴트는 누가 있는지 알려 주는 수명이 짧은 서명 토큰을 받고, 별도의 제한된 액터로서 고객님의 도구를 호출합니다.
01
신원과 계정 데이터를 보유하며, 누가 로그인했는지 알려 주는 2분짜리 토큰에 서명합니다.
02
워크스페이스 설정을 적용합니다: 콘텐츠, 모델, 도구 접근 등급, 확인 단계, 인계.
03
한 번에 한 사용자에 대해 도구를 처리하며, 사용자 정보는 도구 인자가 아니라 로그인 베어러 토큰에서만 가져옵니다.
연결 방법 7가지, 어시스턴트는 하나: 웹 임베드 · 로그인한 고객 · 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: 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 });
});토큰은 누가 있는지를 말할 뿐, 무엇을 할 수 있는지는 말하지 않습니다. 잔액, 기기, 설정, 그리고 모든 변경 작업은 한 사용자에게만 응답하는 도구 뒤에 두십시오.
03웹과 전체 화면
SDK는 필요할 때 백엔드에 신원을 요청하고, 방문자에게도 동작하며, 로그인과 로그아웃 후 갱신하고, 외부 링크는 프레임 밖에서 열며, 서드파티 쿠키가 필요 없습니다.
<!-- 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에 불러오고, 권한 있는 호스트 계층을 통해 신원 요청에 응답하며, 안전한 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 도구
디스커버리 단계에서 완전한 스키마를 공개하십시오. 도구마다 세 가지 접근 등급 중 하나를 부여하고, 확인 카드가 필요한 변경을 표시하고, 호출할 때마다 권한을 다시 확인하십시오.
대화 중인 누구나 호출할 수 있습니다. 가격, 상태, 기능 등.
로그인한 사용자가 필요합니다. 주문 상태, 계정 관련 답변 등.
제한된 액터 토큰이나 사용자 본인의 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 및 웹훅
하나의 워크스페이스 범위 API 키가 REST 엔드포인트와 MCP 서버를 모두 인증합니다. 대화를 나열하고 읽거나, 운영자 회신을 게시하거나, 폴링 대신 웹훅을 구독하십시오.
콘솔 → API 키에서 키를 만든 다음 Bearer 토큰으로 전송하십시오.
curl https://busymate.ai/api/v1/conversations \
-H "Authorization: Bearer bmai_sk_..."전체 참고: API 가이드, 웹훅, OpenAPI 문서.
08인계와 인사이트
고객이 사람을 요청할 때, 도구가 실패할 때, 환불이 한도를 넘을 때, 특정 키워드가 나올 때, 또는 직접 정의한 모든 이벤트에서.
하나의 인박스, 직접 배정이나 순번 배정 또는 진행 중 대화가 가장 적은 담당자 배정, 그리고 앱 내 알림과 Web Push, APNs, 개인 Telegram 알림.
대화를 지켜보고, 맥락을 그대로 가진 채 합류하고, 같은 채팅에서 답합니다. 사람이 응대하는 동안 어시스턴트는 아무것도 변경하지 않습니다.
반복되는 질문, 부족한 지식, 실패한 경로를 실제 대화에서 묶어 근거 링크와 함께 보여 줍니다.
09워크스페이스 로그인
콘솔에서 워크스페이스의 로그인 URL과 계정 URL을 설정하십시오. 로그인을 선택한 방문자는 프레임을 벗어나 고객님의 제품에서 인증한 뒤, 출발했던 정확한 URL로 돌아옵니다. 그다음 고객님의 백엔드가 일회용 로그인 토큰을 제공합니다. Busymate AI는 사용자의 비밀번호나 고객님의 서명 키를 절대 받지 않습니다.
변경은 언제나 로그인한 사용자 본인의 데이터에만 적용됩니다. 계정 도구는 위임된 액터에서 고객 정보를 가져오고, 호출할 때마다 권한을 다시 확인하며, 설정하신 변경 작업에 대해서는 확인을 요청합니다.
10출시 체크리스트
워크스페이스를 설정할 준비가 되셨나요?
콘솔의 연동 페이지가 게시된 설정을 바탕으로 모든 창구에 필요한 코드와 설정을 생성해 줍니다.
WebMCP
WebMCP는 페이지가 자체 작업을 도구로 나열하도록 하는 W3C Web Machine Learning Community Group의 초안입니다. 가장 작은 변경부터 완전한 제어까지, 세 가지 방법으로 시작할 수 있습니다.
book_appointment, check_order_status, start_return — 도구 하나에 역할 하나.
"다음 주 화요일 10시", "특급" — 어시스턴트가 추측해야 하는 내부 id는 절대 사용하지 마십시오.
이 페이지에서 실행할 수 있는 도구를 제공하고, 페이지를 벗어나면 등록을 해제하십시오.
예약, 결제, 반품은 표시해 두면 실행 전에 고객이 확인합니다.
확인은 고객의 몫으로 남습니다. 도구는 방문자 자신의 브라우저에서, 귀하의 출처(origin)에서 실행됩니다. 결과가 따르는 것으로 표시한 도구는 고객이 보는 확인 단계에서 멈추며, exposedTo는 어떤 출처가 도구를 볼 수 있는지 제한합니다.
가장 작은 변경: 이미 있는 폼에 이름과 한 줄 설명을 추가하십시오. 브라우저가 이를 도구로 바꾸며, 폼은 그대로 보이고 모두에게 계속 작동합니다.
<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"] });어시스턴트가 이미 설치된 곳에서는 한 번의 호출로 해당 어시스턴트와 방문하는 다른 모든 어시스턴트에게 도구를 나열합니다 — 브라우저 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"
}
}
}최근 변경 사항
빌드 1624
G4: with a workspace's pii read masking ON, the Inbox thread translation and the email copilot (Console and MCP summarize_email_thread, suggest_email_reply, translate_email, and the Console copilot on an email conversation) now reads the mail thread masked, so drafts and summaries cannot quote raw card numbers, IBANs or national ids. With the setting off nothing changes.
빌드 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.