@buildbase/sdk 0.0.50 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +41 -13
  2. package/CHANGELOG.md +109 -0
  3. package/README.md +343 -44
  4. package/dist/css/styles.css +1 -1
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.d.ts +874 -53
  7. package/dist/index.mjs +1 -1
  8. package/dist/mcp/index.cjs +1 -0
  9. package/dist/mcp/index.d.ts +1402 -0
  10. package/dist/mcp/index.mjs +1 -0
  11. package/dist/react/chunks/SettingsDialog-BxDYsie6.mjs +2 -0
  12. package/dist/react/chunks/SettingsDialog-Dy8W3NA6.cjs +2 -0
  13. package/dist/react/chunks/SubscriptionDialog-6c5dHoQd.mjs +2 -0
  14. package/dist/react/chunks/SubscriptionDialog-CpVnnXJg.cjs +2 -0
  15. package/dist/react/chunks/ar-1Z1MOGAP.mjs +2 -0
  16. package/dist/react/chunks/ar-VGzBv3AZ.cjs +2 -0
  17. package/dist/react/chunks/de-BjdDg60Y.cjs +2 -0
  18. package/dist/react/chunks/de-CXUItplO.mjs +2 -0
  19. package/dist/react/chunks/es-BBPsSZUp.cjs +2 -0
  20. package/dist/react/chunks/es-BTbzVutz.mjs +2 -0
  21. package/dist/react/chunks/fr-Cax48X_S.cjs +2 -0
  22. package/dist/react/chunks/fr-DPMeOQu3.mjs +2 -0
  23. package/dist/react/chunks/hi-BJqvpG6d.cjs +2 -0
  24. package/dist/react/chunks/hi-JfJx-LV7.mjs +2 -0
  25. package/dist/react/chunks/ja-CDGtLzTk.mjs +2 -0
  26. package/dist/react/chunks/ja-DvVyj5xX.cjs +2 -0
  27. package/dist/react/chunks/react-4SlWxHdh.cjs +2 -0
  28. package/dist/react/chunks/react-OL1uSkpr.mjs +2 -0
  29. package/dist/react/chunks/zh-BAcqxkxk.cjs +2 -0
  30. package/dist/react/chunks/zh-BNZ2Ooe8.mjs +2 -0
  31. package/dist/react/index.cjs +1 -1
  32. package/dist/react/index.d.ts +484 -93
  33. package/dist/react/index.mjs +1 -1
  34. package/package.json +22 -5
  35. package/dist/react/chunks/SubscriptionDialog-A6oETKbb.cjs +0 -2
  36. package/dist/react/chunks/SubscriptionDialog-nuMunBn4.mjs +0 -2
  37. package/dist/react/chunks/ar-BRvKHRYS.cjs +0 -2
  38. package/dist/react/chunks/ar-IhPhJAZA.mjs +0 -2
  39. package/dist/react/chunks/de-D3q0vQBG.cjs +0 -2
  40. package/dist/react/chunks/de-bvh5IFSx.mjs +0 -2
  41. package/dist/react/chunks/es-B9kiwJmF.cjs +0 -2
  42. package/dist/react/chunks/es-TyUz4_E1.mjs +0 -2
  43. package/dist/react/chunks/fr-C500Ytao.cjs +0 -2
  44. package/dist/react/chunks/fr-CKm-bSvG.mjs +0 -2
  45. package/dist/react/chunks/hi-CfFhH2uQ.cjs +0 -2
  46. package/dist/react/chunks/hi-Ck9cPpd-.mjs +0 -2
  47. package/dist/react/chunks/ja-AM4H1tXw.mjs +0 -2
  48. package/dist/react/chunks/ja-DfROrx_j.cjs +0 -2
  49. package/dist/react/chunks/react-CTX0_nOV.cjs +0 -2
  50. package/dist/react/chunks/react-DzQupoqH.mjs +0 -2
  51. package/dist/react/chunks/zh-CLmBh-it.cjs +0 -2
  52. package/dist/react/chunks/zh-DCVPxie-.mjs +0 -2
package/README.md CHANGED
@@ -9,6 +9,7 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
9
9
  - [Features](#-features)
10
10
  - [Installation](#installation)
11
11
  - [Quick Start](#quick-start)
12
+ - [UI Configuration](#️-ui-configuration)
12
13
  - [Authentication](#-authentication)
13
14
  - [Redirect Preservation](#redirect-preservation)
14
15
  - [Affiliate / Referral Tracking](#affiliate--referral-tracking)
@@ -38,6 +39,7 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
38
39
  - [Server-Side Usage](#server-side-usage)
39
40
  - [Webhook Verification](#webhook-verification)
40
41
  - [Agent Readiness (Discovery)](#agent-readiness-discovery)
42
+ - [MCP Server](#mcp-server-buildbasesdkmcp)
41
43
  - [OAuth2 App Bridge](#oauth2-app-bridge)
42
44
 
43
45
  ## 🚀 Features
@@ -204,6 +206,85 @@ function WorkspaceExample() {
204
206
  }
205
207
  ```
206
208
 
209
+ ## 🎛️ UI Configuration
210
+
211
+ The optional `ui` prop on `SaaSOSProvider` controls which parts of the SDK UI are shown and lets you override individual UI strings. Everything defaults to visible/current behavior — the config can only **hide** UI and never bypasses platform permissions, which remain the security floor.
212
+
213
+ ```tsx
214
+ <SaaSOSProvider
215
+ serverUrl="..."
216
+ version="v1"
217
+ orgId="..."
218
+ ui={{
219
+ settings: {
220
+ // Hide whole sections of the workspace settings dialog.
221
+ // Hidden sections are also unreachable via deep links / defaultSection.
222
+ sections: { credits: false, notifications: false, 'connected-agents': false },
223
+
224
+ // Per-screen feature toggles
225
+ profile: { currency: false, timezone: false },
226
+ security: { passkeyDelete: false },
227
+ general: { iconEditor: false },
228
+ users: { invite: false, seatPricing: false },
229
+ subscription: { cancel: false, invoicesTab: false, planDetails: false },
230
+ credits: { buyButton: false, transactions: false },
231
+ notifications: { push: false, emailToggles: false, pushToggles: false },
232
+ },
233
+
234
+ // Workspace switcher (client-side; ANDed with server settings)
235
+ workspaceSwitcher: { createButton: false, planBadge: false, memberCount: false },
236
+
237
+ // Behaviors that are otherwise automatic
238
+ behavior: {
239
+ autoOpenPlanDialog: false, // don't auto-open the plan picker when no subscription
240
+ trialEndingDays: 7, // global default for <WhenTrialEnding>
241
+ },
242
+
243
+ // Per-key string overrides, deep-merged over the active locale bundle
244
+ messages: {
245
+ settings: { sidebar: { credits: 'Tokens', subscription: 'Billing' } },
246
+ },
247
+
248
+ // Default fallback strings for the top-level error boundary
249
+ errorBoundary: { title: 'Oops!', retryLabel: 'Retry' },
250
+
251
+ // Date formatting for SDK-rendered dates (passkeys, connected agents)
252
+ formats: { date: { dateStyle: 'short' } },
253
+ }}
254
+ >
255
+ ```
256
+
257
+ Section keys match the `SettingsScreen` values: `profile`, `security`, `connected-agents`, `general`, `users`, `subscription`, `usage`, `credits`, `features`, `notifications`, `permissions`, `danger`. Hidden sections are removed from the sidebar (empty groups collapse) and unreachable via deep links or `defaultSection` — the dialog falls back to the first enabled section.
258
+
259
+ **Per-dialog override** — `WorkspaceSettingsDialog` accepts its own `ui` prop, deep-merged over the global config, so one app can render differently-configured dialogs:
260
+
261
+ ```tsx
262
+ <WorkspaceSettingsDialog
263
+ workspace={ws}
264
+ ui={{ settings: { sections: { credits: true, danger: false } } }}
265
+ />
266
+ ```
267
+
268
+ In your own components, use the same single-call helper the SDK uses internally — it combines the `ui` config flag with a permission check:
269
+
270
+ ```tsx
271
+ import { useUIVisibility, Permission } from '@buildbase/sdk/react';
272
+
273
+ function MembersPanel() {
274
+ const { visible } = useUIVisibility();
275
+
276
+ // Config flag AND permission in one decision
277
+ if (!visible(ui => ui.settings?.users?.invite, Permission.WORKSPACE_MEMBERS_INVITE)) {
278
+ return null;
279
+ }
280
+ return <InviteForm />;
281
+ }
282
+ ```
283
+
284
+ `useUIConfig()` gives you the raw `ui` object if you only need the config values; `mergeUIConfig(base, override)` is the deep-merge used for per-dialog overrides.
285
+
286
+ 📖 **Full guide with the complete toggle reference, precedence rules, and recipes (single-tenant, external billing, white-label, read-only): [docs/UI-CONFIG.md](./docs/UI-CONFIG.md)**
287
+
207
288
  ## 🔐 Authentication
208
289
 
209
290
  ### Authentication Hook
@@ -2022,12 +2103,13 @@ All SDK API clients extend a shared base class and are exported from the package
2022
2103
 
2023
2104
  Server-only toolkits (framework-agnostic, zero React) are exported from `@buildbase/sdk`:
2024
2105
 
2025
- | Export group | Purpose |
2026
- | ------------------------ | ---------------------------------------------------------------------------------------------------------- |
2027
- | `BuildBase()` factory | Session-scoped server actions — see [Server-Side Usage](#server-side-usage) |
2028
- | Webhook verification | `verifyWebhookSignature`, `parseWebhookEvent` — see [Webhook Verification](#webhook-verification) |
2029
- | Agent readiness | `resolveWellKnown`, `buildAgentCard`, `buildLlmsTxt`, … — see [Agent Readiness](#agent-readiness-discovery) |
2030
- | OAuth2 app bridge | `handleAppTokenRequest`, `handleAppRevokeRequest`, `bearerChallenge`, … — see [OAuth2 App Bridge](#oauth2-app-bridge) |
2106
+ | Export group | Purpose |
2107
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
2108
+ | `BuildBase()` factory | Session-scoped server actions — see [Server-Side Usage](#server-side-usage) |
2109
+ | Webhook verification | `verifyWebhookSignature`, `parseWebhookEvent` — see [Webhook Verification](#webhook-verification) |
2110
+ | Agent readiness | `resolveAgentPath`, `buildRobotsTxt`, `buildAgentCard`, `buildLlmsTxt`, … — see [Agent Readiness](#agent-readiness-discovery) |
2111
+ | OAuth2 app bridge | `handleAppTokenRequest`, `handleAppRevokeRequest`, `signClientJwt`, `bearerChallenge`, … — see [OAuth2 App Bridge](#oauth2-app-bridge) |
2112
+ | MCP server | `createMcpHandler`, `defineMcpTool`, … from **`@buildbase/sdk/mcp`** — see [MCP Server](#mcp-server-buildbasesdkmcp) |
2031
2113
 
2032
2114
  ### Components
2033
2115
 
@@ -2841,72 +2923,285 @@ app.post('/webhooks/buildbase', (req, res) => {
2841
2923
 
2842
2924
  `verifyWebhookSignature(options)` → `boolean` — use directly when you want to control parsing yourself.
2843
2925
 
2844
- | Option | Type | Notes |
2845
- | --------------- | ------------------- | ----------------------------------------------------------- |
2846
- | `body` | `string` | Raw request body, byte-for-byte as sent. |
2847
- | `signature` | `string \| null` | `x-buildbase-signature` header (`sha256=<hex>`). |
2848
- | `timestamp` | `string \| null` | `x-buildbase-timestamp` header. |
2849
- | `secret` | `string` | Your endpoint's signing secret. |
2850
- | `maxAgeSeconds` | `number` (def. 300) | Replay window. Set to `0` to skip the timestamp age check. |
2926
+ | Option | Type | Notes |
2927
+ | --------------- | ------------------- | ---------------------------------------------------------- |
2928
+ | `body` | `string` | Raw request body, byte-for-byte as sent. |
2929
+ | `signature` | `string \| null` | `x-buildbase-signature` header (`sha256=<hex>`). |
2930
+ | `timestamp` | `string \| null` | `x-buildbase-timestamp` header. |
2931
+ | `secret` | `string` | Your endpoint's signing secret. |
2932
+ | `maxAgeSeconds` | `number` (def. 300) | Replay window. Set to `0` to skip the timestamp age check. |
2851
2933
 
2852
2934
  > **Next.js App Router:** read the raw body with `await req.text()` — do not use `req.json()`, which discards the exact bytes the signature was computed over.
2853
2935
 
2854
2936
  ## Agent Readiness (Discovery)
2855
2937
 
2856
- Make a consuming app [agent ready](https://isitagentready.com) by serving the standard machine-readable discovery documents an AI agent looks for — Agent Card, OAuth protected-resource metadata (RFC 9728), Agent Skills, `security.txt` (RFC 9116), and `llms.txt` — with the BuildBase server as the source of truth. Framework-agnostic and zero React: every function returns a plain `DiscoveryDocument` (`{ status, contentType, body, cacheControl }`); you wire it into your own router.
2938
+ Make a consuming app [agent ready](https://isitagentready.com) by serving the standard machine-readable discovery documents an AI agent looks for — Agent Card, OAuth metadata (RFC 8414/9728), Agent Skills, API Catalog (RFC 9727), MCP Server Card, `robots.txt` with AI-bot rules + Content Signals, `sitemap.xml`, `security.txt` (RFC 9116), `llms.txt`, and `auth.md` — with the BuildBase server as the source of truth. Framework-agnostic and zero React: every function returns a plain `DiscoveryDocument` (`{ status, contentType, body, cacheControl, vary? }`); you wire it into your own router.
2857
2939
 
2858
2940
  ```ts
2859
2941
  // lib/agent-ready.ts
2860
- import type { AgentReadyConfig } from '@buildbase/sdk';
2942
+ import { resolveAgentPath, type AgentReadyConfig } from '@buildbase/sdk';
2861
2943
 
2862
2944
  export const agentConfig: AgentReadyConfig = {
2863
2945
  serverUrl: process.env.BUILDBASE_URL!,
2864
2946
  orgId: process.env.BUILDBASE_ORG_ID!,
2865
2947
  siteUrl: 'https://imejis.io',
2866
2948
  site: { name: 'Imejis', description: 'Generate images from templates via API.' },
2867
- // skills, security, cacheTtlSeconds are all optional
2949
+ // Discovery content is YOURS — defined here in code, not in the platform:
2950
+ llmsTxt: '# Imejis\n\n> Generate images from templates via API.',
2951
+ robots: { contentSignals: { search: true, aiInput: true, aiTrain: false } },
2952
+ sitemap: { urls: ['/', '/pricing'] }, // omit if you already generate sitemaps
2953
+ protectedResources: [{ scopes: ['read:profile', 'write:posts'] }], // RFC 9728
2954
+ // skills, security, apiCatalog, mcpServerCard, authMd, cacheTtlSeconds — all optional
2868
2955
  };
2869
- ```
2870
-
2871
- Serve every `.well-known/*` document from one catch-all route via `resolveWellKnown`:
2872
-
2873
- ```ts
2874
- // app/.well-known/[...path]/route.ts (Next.js App Router)
2875
- import { resolveWellKnown } from '@buildbase/sdk';
2876
- import { agentConfig } from '@/lib/agent-ready';
2877
2956
 
2878
- export async function GET(req: Request) {
2879
- const doc = await resolveWellKnown(new URL(req.url).pathname, agentConfig);
2957
+ export async function serveAgentPath(req: Request): Promise<Response> {
2958
+ const doc = await resolveAgentPath(new URL(req.url).pathname, agentConfig);
2880
2959
  if (!doc) return new Response('{"error":"not_found"}', { status: 404 });
2881
2960
  return new Response(doc.body, {
2882
2961
  status: doc.status,
2883
- headers: { 'Content-Type': doc.contentType, 'Cache-Control': doc.cacheControl },
2962
+ headers: {
2963
+ 'Content-Type': doc.contentType,
2964
+ 'Cache-Control': doc.cacheControl,
2965
+ ...(doc.vary ? { Vary: doc.vary } : {}),
2966
+ },
2884
2967
  });
2885
2968
  }
2886
2969
  ```
2887
2970
 
2888
- `resolveWellKnown` handles `agent.json`, `oauth-protected-resource` (+ per-resource paths), the `oauth-authorization-server` pointer, the Agent Skills index + `SKILL.md`, `security.txt`, the **API Catalog** (RFC 9727, `/.well-known/api-catalog`), and the **MCP Server Card** (`/.well-known/mcp/server-card.json`) — the last two are served only when you set `apiCatalog` / `mcpServerCard` in the config. Serve `llms.txt` from its own route (it lives at the site root, not under `.well-known`):
2971
+ `resolveAgentPath` resolves the app's **entire agent-readiness surface**: the root-level documents (`/robots.txt`, `/sitemap.xml`, `/llms.txt`, `/auth.md`, `/security.txt`) plus everything under `/.well-known/*` — `agent.json`, `oauth-protected-resource` (+ per-resource paths), the full RFC 8414 `oauth-authorization-server` metadata (proxied fail-soft from the platform), the Agent Skills index + `SKILL.md`, `security.txt`, the API Catalog, and the MCP Server Card. Wire it once:
2889
2972
 
2890
2973
  ```ts
2891
- // app/llms.txt/route.ts
2892
- import { buildLlmsTxt, fetchAgentReadiness } from '@buildbase/sdk';
2893
- import { agentConfig } from '@/lib/agent-ready';
2974
+ // app/.well-known/[...path]/route.ts (Next.js App Router)
2975
+ import { serveAgentPath } from '@/lib/agent-ready';
2976
+ export const GET = serveAgentPath;
2894
2977
 
2895
- export async function GET() {
2896
- const doc = buildLlmsTxt(agentConfig, await fetchAgentReadiness(agentConfig));
2897
- if (!doc) return new Response('Not found', { status: 404 });
2898
- return new Response(doc.body, { headers: { 'Content-Type': doc.contentType } });
2899
- }
2978
+ // plus identical two-liners at app/robots.txt/route.ts, app/llms.txt/route.ts,
2979
+ // app/auth.md/route.ts, app/security.txt/route.ts, app/sitemap.xml/route.ts
2980
+ ```
2981
+
2982
+ ```ts
2983
+ // Express — one middleware covers everything
2984
+ app.use(async (req, res, next) => {
2985
+ const doc = await resolveAgentPath(req.path, agentConfig);
2986
+ if (!doc) return next();
2987
+ res
2988
+ .status(doc.status)
2989
+ .setHeader('Content-Type', doc.contentType)
2990
+ .setHeader('Cache-Control', doc.cacheControl)
2991
+ .send(doc.body);
2992
+ });
2900
2993
  ```
2901
2994
 
2995
+ (`resolveWellKnown` remains available for `.well-known`-only wiring; `resolveAgentPath` is a superset.)
2996
+
2902
2997
  `fetchAgentReadiness` is **fail-soft** — any network/HTTP/parse error resolves to `{ enabled: false }`, so a discovery route never 500s and never leaks that the platform is down; agents simply see the app as not (yet) agent-ready. Results are cached in-memory for `cacheTtlSeconds` (default 300s); call `clearAgentReadinessCache()` to reset (mainly for tests).
2903
2998
 
2904
- Two more documents live at the site root, so they get their own routes like `llms.txt`:
2999
+ ### robots.txt, Content Signals & AI bots
3000
+
3001
+ `buildRobotsTxt(config)` always returns a document. By default it emits an allow-all policy plus an explicit `Allow: /` group for every known AI crawler (`AI_BOT_USER_AGENTS`: GPTBot, ClaudeBot, PerplexityBot, …) so readiness checkers see AI bots addressed. Tune it via `config.robots`:
3002
+
3003
+ - `policies` — your base groups (default `[{ userAgent: '*', allow: ['/'] }]`)
3004
+ - `aiBots` — `'allow'` (default) | `'deny'` | explicit `RobotsPolicy[]` for per-bot control
3005
+ - `contentSignals` — [Content Signals](https://contentsignals.org): `{ search, aiInput, aiTrain }` → `Content-Signal: search=yes, ai-input=yes, ai-train=no`
3006
+ - `sitemaps` — `Sitemap:` lines (defaults to `${siteUrl}/sitemap.xml` when `config.sitemap` is set)
3007
+
3008
+ ### Markdown content negotiation & Link headers
3009
+
3010
+ - `wantsMarkdown(acceptHeader)` — q-value-aware predicate: does this request prefer `text/markdown` over HTML? Use it in middleware to rewrite content pages to a markdown endpoint.
3011
+ - `negotiateMarkdown(acceptHeader, { html, markdown })` — picks the variant and returns a `DiscoveryDocument` with `vary: 'Accept'`.
3012
+ - `buildDiscoveryLinkHeader(config)` — the `Link` response header value advertising `llms.txt`, the Agent Card, and (when configured) sitemap, API catalog, and MCP server card. Sync and pure — safe in edge middleware.
3013
+
3014
+ ### Who owns what
3015
+
3016
+ Clean split, no duplication: **your app owns discovery content, the platform owns auth.** You define `llms.txt`, `robots.txt`, `sitemap`, `protectedResources` (RFC 9728), the API catalog, and skills right here in `AgentReadyConfig` — the SDK serves them from your own origin with no platform round-trip. The platform only supplies the pointer to its OAuth **authorization server** (the one thing it owns); that's the sole content of `fetchAgentReadiness`'s bundle (`{ enabled, authorizationServer }`).
2905
3017
 
2906
- - **`/auth.md`** — agent registration/auth instructions. `buildAuthMd(config, await fetchAgentReadiness(config))` returns one generated from your OAuth metadata (or serves `config.authMd` verbatim).
3018
+ - **`/llms.txt`** — from `config.llmsTxt`. Returns null when unset (serve your own static file instead).
3019
+ - **`/.well-known/oauth-protected-resource`** — built from `config.protectedResources`; `authorization_servers` is derived from `serverUrl`/`orgId`.
3020
+ - **`/auth.md`** — agent registration/auth instructions. Generated from the OAuth metadata, or serves `config.authMd` verbatim.
2907
3021
  - **WebMCP** — expose in-page actions to browser agents. In a client component, call `provideWebMcpTools([{ name, description, inputSchema, execute }])` (no-op off-browser).
2908
3022
 
2909
- **Functions:** `resolveWellKnown`, `fetchAgentReadiness`, `clearAgentReadinessCache`, `buildAgentCard`, `buildProtectedResourceMetadata`, `buildAgentSkillsIndex`, `buildSkillMd`, `buildSecurityTxt`, `buildLlmsTxt`, `buildApiCatalog`, `buildMcpServerCard`, `buildAuthMd`, `provideWebMcpTools`, `sha256Digest`. **Types:** `AgentReadyConfig`, `AgentReadinessBundle`, `AgentSkill`, `ApiCatalogApi`, `McpServerCard`, `WebMcpTool`, `DiscoveryDocument`.
3023
+ ### isitagentready.com coverage
3024
+
3025
+ | Check | Covered by |
3026
+ | ---------------------------------------------------------- | -------------------------------------------------------------------------- |
3027
+ | robots.txt + AI bot rules + Content Signals | `buildRobotsTxt` |
3028
+ | Sitemap | `buildSitemap` (or your own generator) |
3029
+ | Link response headers | `buildDiscoveryLinkHeader` |
3030
+ | Markdown content negotiation | `wantsMarkdown` / `negotiateMarkdown` |
3031
+ | llms.txt | `buildLlmsTxt` |
3032
+ | MCP Server Card + live MCP server | `buildMcpServerCard` + [`@buildbase/sdk/mcp`](#mcp-server-buildbasesdkmcp) |
3033
+ | Agent Skills / API Catalog | `buildAgentSkillsIndex` / `buildApiCatalog` |
3034
+ | OAuth discovery (RFC 8414) + protected resource (RFC 9728) | `resolveAgentPath` (platform-backed) |
3035
+ | DNS-AID | out of SDK scope — add the DNS record at your registrar |
3036
+ | Web Bot Auth, commerce (x402/ACP/UCP) | not yet — planned |
3037
+
3038
+ **Functions:** `resolveAgentPath`, `resolveWellKnown`, `fetchAgentReadiness`, `clearAgentReadinessCache`, `buildAgentCard`, `buildProtectedResourceMetadata`, `buildAgentSkillsIndex`, `buildSkillMd`, `buildSecurityTxt`, `buildLlmsTxt`, `buildRobotsTxt`, `buildSitemap`, `buildApiCatalog`, `buildMcpServerCard`, `buildAuthMd`, `buildDiscoveryLinkHeader`, `wantsMarkdown`, `negotiateMarkdown`, `provideWebMcpTools`, `sha256Digest`. **Constants:** `AI_BOT_USER_AGENTS`. **Types:** `AgentReadyConfig`, `AgentReadinessBundle`, `AgentSkill`, `ApiCatalogApi`, `McpServerCard`, `RobotsConfig`, `RobotsPolicy`, `ContentSignals`, `SitemapUrl`, `WebMcpTool`, `DiscoveryDocument`.
3039
+
3040
+ ## MCP Server (`@buildbase/sdk/mcp`)
3041
+
3042
+ Turn your app into a **live MCP server** so AI agents can operate it: the built-in tools expose your BuildBase state (workspaces, subscription, quotas, credits, feature flags, permissions) and you add your own product tools with zod schemas. Stateless Streamable HTTP — pure functions plus a Web-standard `Request`/`Response` adapter, so it runs on Node 18+, edge, Deno, and Bun. Server-only, zero React; split from the core entry because it uses `zod` at runtime.
3043
+
3044
+ ```ts
3045
+ // lib/mcp.ts
3046
+ import BuildBase from '@buildbase/sdk';
3047
+ import { createMcpHandler, defineMcpTool, verifyClientJwt } from '@buildbase/sdk/mcp';
3048
+ import { z } from 'zod';
3049
+
3050
+ const buildbase = BuildBase({
3051
+ serverUrl: process.env.BUILDBASE_URL!,
3052
+ orgId: process.env.BUILDBASE_ORG_ID!,
3053
+ });
3054
+
3055
+ export const mcp = createMcpHandler({
3056
+ buildbase,
3057
+ serverInfo: { name: 'imejis', version: '1.0.0' },
3058
+ auth: {
3059
+ // Verify the Bearer token your app minted in the OAuth2 app-bridge flow
3060
+ // and map it to the BuildBase session the tools run under.
3061
+ verify: token => {
3062
+ const c = verifyClientJwt(token, process.env.BUILDBASE_CLIENT_SECRET!);
3063
+ return { sessionId: String(c.sid), userId: String(c.sub), scopes: c.scope as string[] };
3064
+ },
3065
+ // Unauthenticated requests get a 401 whose WWW-Authenticate header points
3066
+ // here, so agents bootstrap the whole OAuth discovery flow (RFC 9728).
3067
+ resourceMetadataUrl: 'https://imejis.io/.well-known/oauth-protected-resource',
3068
+ },
3069
+ builtinTools: 'readonly', // default; 'all' opts into the mutating tools
3070
+ tools: [
3071
+ defineMcpTool({
3072
+ name: 'generate_image',
3073
+ description: 'Render an image from a template',
3074
+ inputSchema: z.object({ templateId: z.string(), data: z.record(z.string(), z.any()) }),
3075
+ annotations: { readOnlyHint: false },
3076
+ execute: async (input, { bb, workspaceId }) => {
3077
+ await bb.usage.record(workspaceId!, { quotaSlug: 'images', quantity: 1 });
3078
+ return renderTemplate(input.templateId, input.data);
3079
+ },
3080
+ }),
3081
+ ],
3082
+ });
3083
+ ```
3084
+
3085
+ ```ts
3086
+ // app/api/mcp/route.ts (Next.js App Router)
3087
+ import { mcp } from '@/lib/mcp';
3088
+ export const POST = (req: Request) => mcp.fetch(req);
3089
+ export const GET = (req: Request) => mcp.fetch(req); // 405 per spec (no SSE)
3090
+ export const DELETE = (req: Request) => mcp.fetch(req); // 405 (stateless)
3091
+ ```
3092
+
3093
+ ```ts
3094
+ // Express — the pure form
3095
+ app.post('/mcp', async (req, res) => {
3096
+ const r = await mcp.handle({ method: req.method, headers: req.headers, body: req.body });
3097
+ res.status(r.status).set(r.headers).send(r.body);
3098
+ });
3099
+ ```
3100
+
3101
+ Advertise it via the server card so agents find the endpoint:
3102
+
3103
+ ```ts
3104
+ export const agentConfig: AgentReadyConfig = {
3105
+ // ...
3106
+ mcpServerCard: mcp.serverCard({ endpoint: 'https://imejis.io/api/mcp' }),
3107
+ };
3108
+ ```
3109
+
3110
+ ### Built-in tools
3111
+
3112
+ The built-ins cover the **full BuildBase surface**. By default (`builtinTools: 'readonly'`) only the read tools are exposed — **least privilege**: no agent can mutate, bill, or delete anything until you opt in. Set `'all'` to add the writes and destructive/billing ops. Every call still runs under the user's session, so the platform enforces the user's real permissions on top of whatever you expose — an agent can never exceed what the user can do.
3113
+
3114
+ - **Read** (`builtinTools: 'readonly'` for just these): `list_workspaces`, `get_workspace`, `get_user_profile`, `list_workspace_users`, `get_subscription`, `get_plans`, `get_plan_versions`, `get_public_plans`, `get_plan_version`, `list_invoices`, `get_invoice`, `get_quota_usage`, `get_all_quota_usage`, `get_usage_logs`, `get_credit_balance`, `list_credit_transactions`, `get_credit_packages`, `get_credit_buckets`, `get_expiring_credits`, `get_public_credit_packages`, `check_feature_flag`, `check_permission`, `resolve_permissions`, `get_settings`.
3115
+ - **Write / destructive**: `record_usage`, `record_usage_batch`, `consume_credits`, `send_notification`, `create_workspace`, `update_workspace`, `delete_workspace`, `invite_workspace_user`, `remove_workspace_user`, `update_workspace_user_role`, `update_user_profile`, `update_feature_flag`, `create_subscription_checkout`, `update_subscription`, `cancel_subscription`, `resume_subscription`, `get_billing_portal_url`, `purchase_credits`.
3116
+
3117
+ **You choose how much to expose** — the default is safe, and you widen it:
3118
+
3119
+ ```ts
3120
+ builtinTools: 'readonly'; // default — reads only (least privilege)
3121
+ builtinTools: 'all'; // reads + writes + destructive/billing ops
3122
+ builtinTools: false; // none (custom tools only)
3123
+ builtinTools: {
3124
+ include: ['get_quota_usage', 'record_usage'];
3125
+ } // exactly these
3126
+ builtinTools: {
3127
+ exclude: ['delete_workspace', 'purchase_credits'];
3128
+ } // all reads, minus these (an `include` list can be trimmed the same way)
3129
+ ```
3130
+
3131
+ The agent acts as an authenticated app user — it knows your app, not BuildBase — so the built-ins carry **no BuildBase-specific scope requirement**. Workspace-scoped tools take an optional `workspaceId` and fall back to the one pinned on the verified token (`McpAuthInfo.workspaceId`); pin it and cross-workspace access is refused even for tools you exposed.
3132
+
3133
+ Want per-scope gating on top? Set `requiredScopes` on your own custom tools and return the granted `scopes` from `auth.verify` — the handler hides a tool from `tools/list` (and refuses the call) when its `requiredScopes` aren't all granted. A token that carries **no** scopes sees only tools that require none (the built-ins), so a token minted without a `scope` claim can never unlock a scoped tool. Opt-in, using **your** scope names — the SDK never imposes its own.
3134
+
3135
+ ### Production hardening
3136
+
3137
+ The handler ships with safe defaults; these are the knobs to review before going live:
3138
+
3139
+ - **`builtinTools` defaults to `'readonly'`.** Writes and destructive/billing ops are opt-in. Prefer an explicit `{ include: [...] }` allowlist over `'all'` in production.
3140
+ - **Request-body cap.** Bodies over `maxRequestBytes` (default **1 MiB**) are rejected with 413 before parsing — and, on the `fetch` adapter, before the stream is even read (via `Content-Length`). Set `0` to disable.
3141
+ - **Rate limiting.** The server is stateless and keeps no counters. Supply a `rateLimit(auth, req)` gate — called after auth, before dispatch — backed by your own store (KV/Redis/edge limiter), returning `false` or `{ ok: false, retryAfter }` to answer 429. A failing limiter fails **closed**. Also put platform/edge rate limiting in front of the endpoint.
3142
+ - **Origin allowlist.** Set `allowedOrigins` for DNS-rebinding protection; browser-origin requests with a non-matching `Origin` get 403 (server-to-server requests with no `Origin` still pass).
3143
+ - **Token expiry.** `verifyClientJwt` **requires a numeric `exp` by default** — a token with no expiry never expires. `signClientJwt` always stamps one. Pin `issuer`/`audience` too for defense in depth: `verifyClientJwt(token, secret, { issuer, audience })`.
3144
+ - **Error visibility.** Pass `onError` — it fires for auth-callback, rate-limit, context-factory, tool-execution, and schema-conversion failures.
3145
+
3146
+ ```ts
3147
+ export const mcp = createMcpHandler({
3148
+ buildbase,
3149
+ auth: { verify, resourceMetadataUrl },
3150
+ builtinTools: { include: ['get_quota_usage', 'record_usage'] },
3151
+ allowedOrigins: ['https://app.imejis.io'],
3152
+ maxRequestBytes: 512 * 1024,
3153
+ rateLimit: async auth => ({ ok: await limiter.allow(auth?.userId), retryAfter: 30 }),
3154
+ onError: (err, ctx) => logger.error({ err, ...ctx }, 'mcp error'),
3155
+ });
3156
+ ```
3157
+
3158
+ ### Your tools, your rules — no lock-in
3159
+
3160
+ The SDK accelerates; it never gates. Everything BuildBase-specific is optional:
3161
+
3162
+ - **Standalone mode** — omit `buildbase` entirely and ship an MCP server with only your own tools (built-ins are simply off). Nothing in the protocol layer depends on the BuildBase platform.
3163
+ - **Your own context** — the `context` factory runs once per call and its return value reaches every tool as `ctx.custom`: your Prisma client, your services, whatever your tools need. Type it with the second generic of `defineMcpTool`.
3164
+
3165
+ ```ts
3166
+ type AppCtx = { db: PrismaClient };
3167
+
3168
+ const mcp = createMcpHandler({
3169
+ auth: { verify: verifyMyToken }, // any token format — yours
3170
+ context: (): AppCtx => ({ db: prisma }),
3171
+ tools: [
3172
+ defineMcpTool<z.ZodType, AppCtx>({
3173
+ name: 'search_orders',
3174
+ description: 'Search orders in our own database',
3175
+ inputSchema: z.object({ query: z.string() }),
3176
+ execute: (input, ctx) =>
3177
+ ctx.custom.db.order.findMany({ where: { name: { contains: input.query } } }),
3178
+ }),
3179
+ ],
3180
+ });
3181
+ ```
3182
+
3183
+ - **Override built-ins** — a custom tool named like a built-in (e.g. your own `get_subscription`) replaces it; your definition wins.
3184
+ - **Raw JSON Schema** — tools don't have to use zod; pass a plain JSON Schema object as `inputSchema` and receive arguments as-is.
3185
+ - **Composable transport** — `handle()` is a pure `{method, headers, body} → {status, headers, body}` function. Route around it, wrap it, or serve extra JSON-RPC methods yourself before delegating; you own the endpoint.
3186
+ - **Browser side** — `provideWebMcpTools` (core entry) registers any in-page tools with WebMCP-capable browsers, unrelated to BuildBase state.
3187
+
3188
+ ### Auth in one line of plumbing
3189
+
3190
+ The OAuth2 app-bridge flow already mints your app's Bearer tokens (see [OAuth2 App Bridge](#oauth2-app-bridge)); `signClientJwt(payload, secret)` is the symmetric counterpart of `verifyClientJwt` for exactly that. The mint claims include `sessionId` — a per-user BuildBase session bound to the grant (fresh on every mint, including refresh). Embed it **encrypted** (JWTs are signed, not encrypted — the agent can read its own token) and keep it out of any storage, so it simply expires with the token; your MCP `auth.verify` decrypts it back into `McpAuthInfo.sessionId`:
3191
+
3192
+ ```ts
3193
+ mintToken: (user) => ({
3194
+ token: signClientJwt(
3195
+ { sub: user.id, scope: user.scope, aud: user.resource, sid: encrypt(user.sessionId) },
3196
+ process.env.BUILDBASE_CLIENT_SECRET!
3197
+ ),
3198
+ expiresIn: 3600,
3199
+ }),
3200
+ ```
3201
+
3202
+ Set `auth: false` to disable auth entirely (local development only). Test with the MCP Inspector: `npx @modelcontextprotocol/inspector --cli http://localhost:3000/api/mcp --transport http --header "Authorization: Bearer <token>" --method tools/list`.
3203
+
3204
+ **Exports:** `createMcpHandler`, `defineMcpTool`, `builtinMcpTools`, `selectBuiltinTools`, plus re-exported auth helpers (`signClientJwt`, `verifyClientJwt`, `extractBearerToken`, `bearerChallenge`, `AppBridgeError`). **Types:** `CreateMcpHandlerConfig`, `McpHandler`, `McpToolDefinition`, `McpToolContext`, `McpAuthInfo`, `McpHttpRequest`, `McpHttpResponse`, `McpToolAnnotations`, `McpBuildBaseClient`, `BuiltinMcpToolName`, `McpServerCard`, `ScopedActions`.
2910
3205
 
2911
3206
  ## OAuth2 App Bridge
2912
3207
 
@@ -2925,7 +3220,7 @@ export default async function handler(req, res) {
2925
3220
  const { status, body } = await handleAppTokenRequest({
2926
3221
  authorization: req.headers.authorization,
2927
3222
  clientSecret: process.env.BUILDBASE_CLIENT_SECRET!,
2928
- mintToken: (user) => ({
3223
+ mintToken: user => ({
2929
3224
  token: signMyAccessToken(user, { aud: user.resource }), // your token, your format
2930
3225
  expiresIn: 3600,
2931
3226
  }),
@@ -2968,7 +3263,7 @@ Each integration (an AI agent, Zapier, n8n, …) is its own BuildBase OAuth2 cli
2968
3263
 
2969
3264
  ```ts
2970
3265
  const SECRETS: Record<string, string> = {
2971
- [process.env.AGENT_CLIENT_ID!]: process.env.AGENT_CLIENT_SECRET!,
3266
+ [process.env.AGENT_CLIENT_ID!]: process.env.AGENT_CLIENT_SECRET!,
2972
3267
  [process.env.ZAPIER_CLIENT_ID!]: process.env.ZAPIER_CLIENT_SECRET!,
2973
3268
  };
2974
3269
 
@@ -2976,14 +3271,18 @@ const clientSecret = SECRETS[String(req.query.clientId)];
2976
3271
  if (!clientSecret) {
2977
3272
  return res.status(401).json({ success: false, token: '', message: 'unknown_client' });
2978
3273
  }
2979
- const { status, body } = await handleAppTokenRequest({ authorization: req.headers.authorization, clientSecret, mintToken });
3274
+ const { status, body } = await handleAppTokenRequest({
3275
+ authorization: req.headers.authorization,
3276
+ clientSecret,
3277
+ mintToken,
3278
+ });
2980
3279
  ```
2981
3280
 
2982
- Adding a new integration is then just: register it in BuildBase, point its `applicationTokenUrl` at the same endpoint, and add its `id → secret` to the map. Selecting the secret by `clientId` is safe — the `clientId` is public and only *picks* the key; the HS256 signature check is still the real gate.
3281
+ Adding a new integration is then just: register it in BuildBase, point its `applicationTokenUrl` at the same endpoint, and add its `id → secret` to the map. Selecting the secret by `clientId` is safe — the `clientId` is public and only _picks_ the key; the HS256 signature check is still the real gate.
2983
3282
 
2984
3283
  ### `applicationProfileUrl` (userinfo)
2985
3284
 
2986
- If your client also registers an `applicationProfileUrl`, treat it as a userinfo / token-validation endpoint: validate the access token **you** minted (with your own verifier — *not* the client secret) and return the profile.
3285
+ If your client also registers an `applicationProfileUrl`, treat it as a userinfo / token-validation endpoint: validate the access token **you** minted (with your own verifier — _not_ the client secret) and return the profile.
2987
3286
 
2988
3287
  ```ts
2989
3288
  import { extractBearerToken } from '@buildbase/sdk';