시작하는 방법
어디서 시작할지 고르십시오
웹사이트에 어시스턴트 추가하기
허용한 페이지에 스크립트 태그 하나만 넣으면 런처와 방문자 채팅이 바로 동작합니다.
iOS 앱 안에 AI 고객 지원 넣기
WKWebView로 채팅을 불러오고, 세 가지 메시지로 된 작은 브리지가 사용자를 로그인시킵니다.
Android 앱 안에 AI 고객 지원 넣기
WebView로 채팅을 불러오고, JavaScript 인터페이스 하나로 사용자를 로그인시킵니다.
MCP 서버를 도구로 연결하기
고객님의 MCP 서버가 각 고객을 대신해 어시스턴트가 수행할 작업이 됩니다.
터미널에서 모든 워크스페이스 관리하기
Claude Code, Cursor 등 어떤 MCP 클라이언트에서든 OAuth로 플랫폼을 제어합니다.
AI 에이전트가 사이트를 읽게 하기
AI 에이전트를 위해 작성된 /llms.txt의 일반 텍스트 사이트 지도.
01빠른 시작
구성 방식
진실의 원천은 고객님의 앱입니다. 어시스턴트는 누가 있는지 알려 주는 수명이 짧은 서명 토큰을 받고, 별도의 제한된 액터로서 고객님의 도구를 호출합니다.
01
고객님의 앱
신원과 계정 데이터를 보유하며, 누가 로그인했는지 알려 주는 2분짜리 토큰에 서명합니다.
02
어시스턴트
워크스페이스 설정을 적용합니다: 콘텐츠, 모델, 도구 접근 등급, 확인 단계, 인계.
03
고객님의 MCP 서버
한 번에 한 사용자에 대해 도구를 처리하며, 사용자 정보는 도구 인자가 아니라 로그인 베어러 토큰에서만 가져옵니다.
연결 방법 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": "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 });
});토큰은 누가 있는지를 말할 뿐, 무엇을 할 수 있는지는 말하지 않습니다. 잔액, 기기, 설정, 그리고 모든 변경 작업은 한 사용자에게만 응답하는 도구 뒤에 두십시오.
03웹과 전체 화면
임베드 런처를 위한 스크립트 하나
SDK는 필요할 때 백엔드에 신원을 요청하고, 방문자에게도 동작하며, 로그인과 로그아웃 후 갱신하고, 외부 링크는 프레임 밖에서 열며, 서드파티 쿠키가 필요 없습니다.
<!-- 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
버전이 지정된 세 가지 메시지(신원 요청, 신원 응답, 외부 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 도구
고객 데이터베이스를 넘기지 않고 어시스턴트에 도구를 주십시오
디스커버리 단계에서 완전한 스키마를 공개하십시오. 도구마다 세 가지 접근 등급 중 하나를 부여하고, 확인 카드가 필요한 변경을 표시하고, 호출할 때마다 권한을 다시 확인하십시오.
공개
대화 중인 누구나 호출할 수 있습니다. 가격, 상태, 기능 등.
식별됨
로그인한 사용자가 필요합니다. 주문 상태, 계정 관련 답변 등.
위임
제한된 액터 토큰이나 사용자 본인의 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 클라이언트의 서버든 동일한 프로토콜 집합이 적용됩니다.
- PKCE(S256)를 사용하는 OAuth 2.1. 퍼블릭 클라이언트는 동적으로 등록됩니다(RFC 7591).
- 인가 서버 메타데이터(RFC 8414)와 보호 리소스 메타데이터(RFC 9728)를 통한 디스커버리.
- 그랜트: authorization_code와 refresh_token. 인가 응답에는 발급자가 포함됩니다(RFC 9207).
- 플랫폼이 고객님의 MCP 서버에 대한 OAuth 클라이언트가 될 때는 리소스 인디케이터(RFC 8707)를 사용합니다.
- 설정된 경우 토큰 교환(RFC 8693)을 사용합니다.
08인계와 인사이트
사람이 개입할 시점을 규칙으로 정하십시오
인계가 일어나는 시점
고객이 사람을 요청할 때, 도구가 실패할 때, 환불이 한도를 넘을 때, 특정 키워드가 나올 때, 또는 직접 정의한 모든 이벤트에서.
팀에 알리는 방법
하나의 인박스, 직접 배정이나 순번 배정 또는 진행 중 대화가 가장 적은 담당자 배정, 그리고 앱 내 알림과 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을 사용하는 MCP 관리 도구 218개. 클라이언트가 첫 연결 시 브라우저 로그인을 열며, 무언가를 붙여 넣을 필요가 없습니다.
디스커버리 문서
- 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"
}
}
}