Agent integration rules
Paste this into AGENTS.md, Cursor rules, or your agent system prompt when adding Bootstrapware Chat. The same file ships inside the npm package.
# Integrating @bootstrapware/chat
Use this file as agent instructions.
## What this package does
Embeddable private messaging inside an authenticated product: 1:1 and groups (max 20), read/unread, plain text with light markup, attachments.
**Two modes**
- **BYO ($9.99):** you own conversations and files. Pass a `ChatAdapter`. Bootstrapware only hosts app config.
- **Hosted ($19.99):** omit adapter; pass `appId` + `publishableKey`. Messages and files live on Bootstrapware. Customer moderates in the dashboard.
Never send message body or file bytes through MCP. MCP configures apps only.
## Install algorithm (do this in order)
1. Read this file. Call `list_capabilities` on Chat MCP before inventing tools.
2. If the user only needs a working UI in this app: Path A. Do not invent keys.
3. If they need live Hosted or BYO config: Path B or C.
4. Never invent `user.id`. Wire the **real** session id from the host app.
5. Never mint API keys via MCP. If no publishable key is in env, **stop and ask the human**.
### Path A — local, zero keys (true seamless)
```bash
pnpm add @bootstrapware/chat
```
```tsx
import { Chat, createLocalAdapter } from "@bootstrapware/chat";
import "@bootstrapware/chat/styles.css";
<Chat
user={{ id: session.user.id, name: session.user.name, avatarUrl: session.user.image }}
people={workspaceMembers.map((m) => ({ id: m.id, name: m.name, avatarUrl: m.image }))}
adapter={createLocalAdapter({ storageKey: "demo-chat" })}
/>
```
`people` is display and picker only. Identity stays `user.id`. If omitted, the New form still has a collapsed **Other id** field.
Optional corner launcher (default off):
```tsx
<Chat
launcher
user={{ id: session.user.id, name: session.user.name }}
people={workspaceMembers}
adapter={createLocalAdapter({ storageKey: "demo-chat" })}
/>
```
Nudge with `--bsw-ch-launcher-right` and `--bsw-ch-launcher-bottom`. Escape or the backdrop closes it. Unread total is the sum of `unreadCount`.
Optional browser notifications stay **off** until the end user clicks **Notify** in the widget header. That click is the only time the widget requests `Notification` permission. Incoming messages from others can then show a system notification while the tab is hidden. Visible-tab and self messages do not. Denied permission stays off with no re-prompt loop. No email, SMS, or server push.
### Path B — Hosted
1. Confirm Cursor MCP `bootstrapware-chat` at `https://chat.bootstrapware.co/mcp` (OAuth URL-only). If disconnected, tell the human to click **Add to Cursor (OAuth)** on https://app.bootstrapware.co/chat/keys
2. `list_capabilities` → `create_app` → `update_draft` (toggles + `allowedOrigins` for localhost and production) → `publish_app`
3. `get_install_snippet` (or the `nextSteps` from create/publish). `appId` is real. The key is a placeholder.
4. If env has no `bsw_test_pub_` / `bsw_live_pub_` value, **stop**. Ask the human to mint a test publishable key at https://app.bootstrapware.co/chat/keys and paste it into `.env.local`
5. Embed with the real session user id, never `"user_1"` in production.
```tsx
<Chat
appId="cha_..."
publishableKey={process.env.NEXT_PUBLIC_BSW_CHAT_PUBLISHABLE_KEY}
user={{ id: session.user.id, name: session.user.name }}
people={workspaceMembers}
/>
```
Optional `apiBaseUrl` defaults to `https://chat.bootstrapware.co`.
### Path C — BYO
Same MCP app config as Path B. Implement `ChatAdapter` on the customer backend. Bodies and file bytes never go to Bootstrapware.
`createConversation` / `addMembers` may send optional `participants: { id, name? }[]` (same ids as today). You can ignore names; the widget still titles and avatars from `people`.
```tsx
<Chat
appId="cha_..."
user={currentUser}
adapter={{
listConversations: async (input) =>
fetch(`/api/chat/conversations?q=${input.q ?? ""}`).then((r) => r.json()),
createConversation: async (input) =>
fetch("/api/chat/conversations", { method: "POST", body: JSON.stringify(input) }).then((r) => r.json()),
listMessages: async (input) =>
fetch(`/api/chat/conversations/${input.conversationId}/messages`).then((r) => r.json()),
sendMessage: async (input) =>
fetch(`/api/chat/conversations/${input.conversationId}/messages`, {
method: "POST",
body: JSON.stringify(input),
}).then((r) => r.json()),
editMessage: async (input) =>
fetch(`/api/chat/messages/${input.messageId}`, { method: "PATCH", body: JSON.stringify(input) }).then((r) => r.json()),
deleteMessage: async (input) =>
fetch(`/api/chat/messages/${input.messageId}`, { method: "POST", body: JSON.stringify({ action: "delete", ...input }) }).then((r) => r.json()),
markRead: async (input) => {
await fetch(`/api/chat/conversations/${input.conversationId}/read`, { method: "POST", body: JSON.stringify(input) });
},
uploadAttachment: async ({ file }) => {
const body = new FormData();
body.append("file", file);
return fetch("/api/chat/attachments", { method: "POST", body }).then((r) => r.json());
},
}}
/>
```
## Identity
Bootstrapware does **not** authenticate end users. Your app asserts:
```tsx
user={{ id: "opaque-stable-id", name: "Ada" }}
```
`id` is opaque. Optional `email` / `avatarUrl` are display metadata only. Anonymous chat is not supported. Pass `people` for the directory (picker, titles, avatars). Avatars resolve from `people` then `user.avatarUrl`, else initials. Hosted can persist optional participant `name` on create/addMembers; there is no avatar column.
Optional `authorToken`: short-lived host-signed assertion minted with your Chat **secret** key via `POST /api/v1/author-tokens`. Required when the app has `requireAuthorToken: true`.
```bash
curl -s -X POST https://chat.bootstrapware.co/api/v1/author-tokens \
-H "Authorization: Bearer $BSW_SECRET" \
-H "Content-Type: application/json" \
-d '{"authorId":"user_123","appId":"cha_...","expiresInSec":3600}'
```
Pass the returned `authorToken` into `<Chat authorToken={...} />`. Never mint with the publishable key.
## Hosted management (secret key)
Secret keys stay server-side. `Authorization: Bearer` against `https://chat.bootstrapware.co`.
```bash
curl -s -X POST https://chat.bootstrapware.co/api/v1/apps \
-H "Authorization: Bearer $BSW_SECRET" \
-H "Content-Type: application/json" \
-d '{"name":"In-app chat"}'
curl -s -X PATCH https://chat.bootstrapware.co/api/v1/apps/APP_ID \
-H "Authorization: Bearer $BSW_SECRET" \
-H "Content-Type: application/json" \
-d '{"name":"In-app chat","config":{"allowDirect":true,"allowGroups":true,"allowAttachments":true,"allowMemberManage":true,"requireAuthorToken":false,"emptyState":"No conversations yet. Start one.","allowedOrigins":["http://localhost:3000"]}}'
curl -s -X POST https://chat.bootstrapware.co/api/v1/apps/APP_ID \
-H "Authorization: Bearer $BSW_SECRET" \
-H "Content-Type: application/json" \
-d '{"action":"publish"}'
```
Also available: `GET /api/v1/apps`, `GET /api/v1/apps/:id`, `GET /api/v1/usage`.
## Hosted MCP (Cursor)
HTTP MCP endpoint: `https://chat.bootstrapware.co/mcp`
**Preferred:** OAuth Connect, URL-only. **Add to Cursor (OAuth)** on https://app.bootstrapware.co/chat/keys
**Fallback:** test/live secret as `Authorization: Bearer`.
Tools: `list_apps`, `get_app`, `create_app`, `update_draft` (does not publish), `publish_app`, `get_published_config`, `get_install_snippet`, `list_capabilities`.
**Dashboard-only (do not invent MCP tools):** API key mint/revoke, webhooks, app delete, branding, billing, Hosted inbox (including Team send). Call `list_capabilities`.
Publishing stores hosted app config. Live `bsw_live_pub_` config fetch needs BYO ($9.99) or Hosted ($19.99). Live Hosted message and file storage needs Hosted ($19.99). Test publishable keys work without that.
Hosted files: 1 GB included. Optional Chat storage add-on $9.99/mo raises the cap to 10 GB. Uploads freeze at the cap; text still works.
## Limits
Plain text 4,000 characters with a tiny client-rendered subset (`**bold**`, `_italic_`, `` `code` ``, simple lists). Hosted inbox and JSON export stay raw text. 50 messages per page. Groups max 20. Attachments: jpeg/png/webp/gif/pdf, 10 MB, max 4 per message. Poll every 4s if there is no `subscribe` or the stream errors; about 20s heartbeat when SSE is healthy. Hosted `subscribe` is app-scoped so inbox unread updates while a thread is open. Conversation search is debounced (~250ms). Browser notifications are opt-in in the widget (default off); permission is never requested on load.
## Security
- Do not put `bsw_live_sec_` or `bsw_test_sec_` in client code.
- Do not upload message bodies or file bytes to Bootstrapware MCP.
- `user.id` is a trust boundary. Prefer origins + `requireAuthorToken` in production Hosted.
Related: Cursor guide · Agent brief · Prompt template · Quickstart · API