> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended** (the `-y` flags skip prompts, so the command runs without an interactive terminal):
> ```bash
> npx -y @scalekit-inc/cli setup -y
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup -y
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for other Agent Skills-compatible agents in `~/.agents/skills`.
> Skills: integrate-agentkit, implement-saaskit, add-mcp-oauth, implement-sso, implement-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# Connect AI agents to the Butterbase MCP server

The Butterbase MCP connector routes your AI agent's tool calls to Butterbase's own MCP server through Scalekit. Each user connects their own Butterbase access token once, and Scalekit sends it with every call, so your agent never handles credentials. It comes with 40 tools.

**Authentication:** Bearer Token
**Categories:** Developer Tools, Databases
**Tools:** 40: 5 read, 28 write, 7 destructive
**Users sign in with:** Access token
**Built by:** Vendor MCP
**Try it:** [Playground in the Scalekit dashboard](https://app.scalekit.com/ws/signup?sk_intent=playground&provider=BUTTERBASEMCP)

## Setup

1. ### Install the SDK

   **Node.js**

   ```bash
   npm install @scalekit-sdk/node dotenv
   ```

   **Python**

   ```bash
   pip install scalekit-sdk-python python-dotenv
   ```

2. ### Set your credentials

   Add your Scalekit credentials to your `.env` file. Find values in **[app.scalekit.com](https://app.scalekit.com)** > **Developers** > **API Credentials**.

   ```sh title=".env"
   SCALEKIT_ENVIRONMENT_URL=<your-environment-url>
   SCALEKIT_CLIENT_ID=<your-client-id>
   SCALEKIT_CLIENT_SECRET=<your-client-secret>
   ```

3. ### Create the Butterbase MCP connection

   In **AgentKit > Connections**, create a Butterbase MCP connection. The name you give it is the `connection_name` your code passes. See [Configure connections](/agentkit/connections/).

   **Console steps with screenshots:**

   Register your Butterbase API key with Scalekit so it can authenticate requests to the Butterbase MCP server on behalf of your users. Butterbase MCP uses API key authentication with an account-scoped key.

   1. ### Generate a Butterbase API key

      - Sign in to the [Butterbase dashboard](https://butterbase.ai) and open **API keys** in the left sidebar.
      - Click **Generate key**.

        > Image: Butterbase API keys page with the Generate key button
      - Enter a **Name** for the key, such as `Scalekit`.

        > Image: New API key dialog with the Name field
      - Expand **Advanced** and set **Key scope** to **Account**. The MCP server needs full access across all apps, so don't pick **This app**.
      - Under **Additional scopes**, keep **ai:gateway** and **substrate** checked.

        > Image: Advanced section with Account key scope and the ai:gateway and substrate scopes checked
      - Click **Generate**.

      > caution: Copy the key now
      >
      > Butterbase shows the full key, which starts with `bb_sk_`, only once. Copy it before you dismiss the panel. If you lose it, revoke the key and generate a new one.

      > Image: Butterbase shows the new key once, and the API keys table lists it with its prefix and scopes

   2. ### Create a connection in Scalekit

      - In the [Scalekit dashboard](https://app.scalekit.com), go to **AgentKit** → **Connections** → **Create Connection**.
      - Search for **Butterbase MCP** and click **Create**.
      - Note the **Connection name** — use this as `connection_name` in your code (e.g., `butterbasemcp`).

   3. ### Add a connected account

      Connected accounts link a specific user identifier in your system to a Butterbase API key. Add one in the dashboard to test. In production, each user adds their own through the authorization link: they enter their credentials on the page it opens.

      **In the dashboard, to test**

      - Open the connection and click the **Connected Accounts** tab → **Add account**.
      - Fill in **Your User's ID** and **API Key**, then click **Save**.

      **From your backend, if your app already has the credentials**

      For example, when users enter them on a settings page in your app:

        **Node.js**

        ```typescript
        import { ConnectorStatus } from '@scalekit-sdk/node'

        const authorizationDetails = {
          details: {
            case: 'staticAuth',
            value: { details: { token: 'bb_sk_...' } },
          },
        };

        let { connectedAccount } = await scalekit.actions.upsertConnectedAccount({
          connectionName: 'butterbasemcp',
          identifier: 'user_123',
          authorizationDetails,
        });

        // Make sure the account is ACTIVE before the first tool call.
        if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
          ({ connectedAccount } = await scalekit.actions.upsertConnectedAccount({
            connectionName: 'butterbasemcp',
            identifier: 'user_123',
            authorizationDetails,
          }));
        }
        ```

        **Python**

        ```python
        authorization_details = {"static_auth": {"token": "bb_sk_..."}}

        response = scalekit_client.actions.upsert_connected_account(
            connection_name="butterbasemcp",
            identifier="user_123",
            authorization_details=authorization_details,
        )

        # Make sure the account is ACTIVE before the first tool call.
        if response.connected_account.status != "ACTIVE":
            response = scalekit_client.actions.upsert_connected_account(
                connection_name="butterbasemcp",
                identifier="user_123",
                authorization_details=authorization_details,
            )
        ```

4. ### Authorize a user and make your first call

   **Node.js** (`quickstart.mts`)

   ```typescript
   import { ScalekitClient } from '@scalekit-sdk/node'
   import 'dotenv/config'
   import { createInterface } from 'node:readline/promises'

   const scalekit = new ScalekitClient(
     process.env.SCALEKIT_ENVIRONMENT_URL,
     process.env.SCALEKIT_CLIENT_ID,
     process.env.SCALEKIT_CLIENT_SECRET,
   )
   const actions = scalekit.actions

   const connector = 'butterbasemcp'
   const identifier = 'user_123'

   // Generate an authorization link for the user
   const { link } = await actions.getAuthorizationLink({ connectionName: connector, identifier })
   console.log('Authorize Butterbase MCP:', link)
   const rl = createInterface({ input: process.stdin, output: process.stdout })
   await rl.question('Press Enter after authorizing...')
   rl.close()

   // Make your first call
   const result = await actions.executeTool({
     connector,
     identifier,
     toolName: 'butterbasemcp_butterbase_docs',
     toolInput: {},
   })
   console.log(result)
   ```

   ```bash
   npx tsx quickstart.mts
   ```

   **Python** (`quickstart.py`)

   ```python
   import os
   from scalekit import ScalekitClient
   from dotenv import load_dotenv
   load_dotenv()

   scalekit_client = ScalekitClient(
       env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
       client_id=os.getenv("SCALEKIT_CLIENT_ID"),
       client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
   )
   actions = scalekit_client.actions

   connection_name = "butterbasemcp"
   identifier = "user_123"

   # Generate an authorization link for the user
   link_response = actions.get_authorization_link(
       connection_name=connection_name,
       identifier=identifier,
   )
   print("Authorize Butterbase MCP:", link_response.link)
   input("Press Enter after authorizing...")

   # Make your first call
   result = actions.execute_tool(
       tool_input={},
       tool_name="butterbasemcp_butterbase_docs",
       connection_name=connection_name,
       identifier=identifier,
   )
   print(result)
   ```

   ```bash
   python quickstart.py
   ```

   Each user opens the link once and enters their Butterbase MCP server credentials there. If your app already has a user's credentials, add the account from your backend instead, as the console steps above show. See [Authorize a user](/agentkit/tools/authorize/) for the full flow and statuses.

## Tools

Pass the exact name to `execute_tool`, with an input like each tool's example. To ask for a tool that's missing, use the [request form](https://scalekitsupport.portal.usepylon.com/forms/request-a-connector-tool).

**Node.js**

```typescript
const result = await actions.executeTool({
  toolName: 'butterbasemcp_butterbase_docs',
  toolInput: {},
  connector: 'butterbasemcp',
  identifier: 'user_123',
})
```

**Python**

```python
result = actions.execute_tool(
    tool_name="butterbasemcp_butterbase_docs",
    tool_input={},
    connection_name="butterbasemcp",
    identifier="user_123",
)
```

`result.data` is the app's response as JSON, and `result.execution_id` (`executionId` in Node.js) is the ID of the call.

### `butterbasemcp_butterbase_docs`

Butterbase Docs · Read-only

Read comprehensive Butterbase documentation (local, no API calls).

Available topics:
  - all: Complete documentation (default)
  - overview: Platform introduction and key features
  - mcp: MCP tool reference and examples
  - rest: HTTP data API (auto-generated REST endpoints)
  - auth: End-user authentication (OAuth, JWT)
  - storage: File upload/download with S3
  - functions: Serverless functions (triggers, context)
  - frontend: Static frontend deployment (upload zip, deploy to live URL)
  - ai: AI model gateway (chat completions, BYOK, usage)
  - meetings: Meeting bots that join Zoom/Meet/Teams/Webex calls and return recordings + transcripts
  - billing: Your Butterbase plan, usage meters, app-level Stripe Connect (subscriptions and one-time payments)
  - platform: MCP over HTTP, /llms.txt, subdomains, suggestions, rate limits
  - regions: Choosing a region at app creation, moving apps between regions, discovering the live region list
  - schema: Schema DSL reference (types, indexes, constraints)
  - sdk: TypeScript SDK installation, client setup, query builder, auth, storage, functions
  - cli: CLI installation, commands for apps, schema, functions, storage, config
  - integrations: Third-party integrations (OAuth connect flow, tool execution, SDK, CLI)
  - substrate: Per-user memory + action coordination plane for AI agents (entities, decisions, attention rules, action ledger, outbox, ws stream, ctx.substrate inside functions)
  - office: Organization Ask, conversations, decisions, browser confirmation and native business memory across MCP/CLI/SDK

Example:
  Input: { topic: "auth" }
  Output: Full authentication documentation with OAuth setup, JWT handling, etc.

Don't know the topic slug? Pass a freeform { query: "..." } instead and the tool
returns the best-matching section plus an index of related topics:
  Input: { query: "how do I send email" }
  Output: Ranked topic index + the full text of the top-matching section.

Use this to:
  - Learn Butterbase features and APIs
  - Get code examples for common tasks
  - Reference schema DSL syntax
  - Understand authentication flow
  - Learn about app monetization (subscriptions and one-time purchases)

Note: This is a local documentation tool. No network requests are made.

Idempotency: Safe to call anytime (read-only operation).

Read only: yes. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `query` (`string`, optional): Freeform search across all docs. When provided (and no topic is given), returns the best-matching section plus a ranked index of related topics. Use when you do not know the exact topic slug.
- `topic` (`string`, optional): Section to return: all (default), overview, mcp, rest, auth, storage, functions, frontend, ai, meetings, billing, platform, regions, schema, sdk, cli, realtime, rag, integrations, substrate, office. One of: `all`, `overview`, `mcp`, `rest`, `auth`, `storage`, `functions`, `frontend`, `ai`, `meetings`, `billing`, `platform`, `regions`, `schema`, `sdk`, `cli`, `realtime`, `rag`, `integrations`, `substrate`, `office`.

Example input: `{}`

### `butterbasemcp_list_regions`

List Regions · Read-only

List the regions an app can be created or moved to.

Returns the live set of supported regions. Use this before calling init_app or
manage_app (action: "move") to validate a region slug, or to present region choices to a user.

Returns: { regions: string[] }  e.g. { regions: ["us-east-1", "us-west-2"] }

Idempotency: Safe to call anytime (read-only operation).

Read only: yes. Destructive: no. Idempotent (safe to retry): yes.

Inputs: none.

Example input: `{}`

### `butterbasemcp_query_audit_logs`

Query Audit Logs · Read-only

Query audit events for an app — authentication, admin mutations, and function invocations.

Returns a unified event stream. Each event has:
  category       'auth' | 'admin' | 'function'
  event_type     e.g. 'login', 'schema.apply', 'function.deploy', 'function.invoke'
  action         'create' | 'update' | 'delete' | 'invoke' | 'enable' | 'disable' | null
  resource_type  which resource the event acted on
  resource_id    the resource identifier (function name, policy name, deployment id, etc.)
  actor_type     'platform_user' | 'app_user' | 'api_key' | 'system' | 'anonymous'
  actor_id       platform user id / app user id / api key id
  event_data     event-specific payload
  success        whether the event succeeded
  correlation_id request id (ties related events together)

Use this to:
  - Investigate who did what and when
  - Debug failing auth / admin / function flows
  - Monitor suspicious activity
  - Trace a request across subsystems via correlation_id

Common filters:
  - category='admin' to see only administrative mutations
  - resource_type='function' + resource_id='my-fn' to see one function's history
  - actor_id=<user-id> to see one actor's activity
  - from / to to narrow to a time window

Use nextCursor to resume from the last row without deduplication; omit offset when cursor is set.

Idempotency: Safe to call anytime (read-only). Historical auth events predating migration 034 are included (normalized).

Read only: yes. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `app_id` (`string`, required): The app ID (e.g. app_abc123def456)
- `action` (`string`, optional): Action filter. One of: `create`, `update`, `delete`, `invoke`, `enable`, `disable`.
- `actor_id` (`string`, optional): Actor ID filter (platform user, app user, or API key id)
- `category` (`string`, optional): Event category filter. One of: `auth`, `admin`, `function`.
- `cursor` (`string`, optional): Opaque nextCursor from a previous response. Do not combine with offset.
- `event_type` (`string`, optional): Exact event type match (e.g. "login", "schema.apply", "function.invoke")
- `from` (`string`, optional): ISO-8601 timestamp, inclusive lower bound on created_at
- `limit` (`integer`, optional): Number of logs to return (default 100, max 500). Default: `100`.
- `offset` (`integer`, optional): Pagination offset (default 0). Do not combine with cursor. Default: `0`.
- `resource_id` (`string`, optional): Resource identifier filter
- `resource_type` (`string`, optional): Resource type filter (e.g. "function", "rls_policy")
- `to` (`string`, optional): ISO-8601 timestamp, exclusive upper bound on created_at
- `user_id` (`string`, optional): DEPRECATED — alias for actor_id

Example input: `{"app_id":"<app_id>"}`

### `butterbasemcp_rag_query`

Query RAG Collection · Read-only

Query a RAG collection using natural language to retrieve relevant document chunks.

Performs semantic search over the collection's indexed documents and returns the most
relevant chunks ranked by similarity. Optionally synthesizes an AI-generated answer
using the retrieved context.

Parameters:
  - query: Natural language question or search phrase
  - top_k: Number of chunks to retrieve (default 5, max 20)
  - threshold: Minimum similarity score 0-1 (only return chunks above this score)
  - synthesize: If true, uses an LLM to generate a natural language answer from the
    retrieved chunks (default false — returns raw chunks only)
  - model: LLM model to use for synthesis (only relevant when synthesize is true,
    default: anthropic/claude-haiku-4.5)
  - filter: Metadata filter to narrow results (e.g. { category: "faq" })

Example — raw retrieval:
  Input: {
    app_id: "app_abc123",
    collection: "knowledge-base",
    query: "How do I reset my password?",
    top_k: 3
  }
  Output: {
    chunks: [
      {
        text: "To reset your password, go to Settings > Security > Reset Password...",
        score: 0.92,
        document_id: "doc_abc",
        metadata: { category: "faq", source: "help-center" }
      },
      ...
    ]
  }

Example — with synthesis:
  Input: {
    app_id: "app_abc123",
    collection: "knowledge-base",
    query: "How do I reset my password?",
    top_k: 5,
    synthesize: true
  }
  Output: {
    answer: "To reset your password, navigate to Settings > Security and click...",
    chunks: [ ... ],
    model: "gpt-4o-mini"
  }

Example — with metadata filter:
  Input: {
    app_id: "app_abc123",
    collection: "knowledge-base",
    query: "pricing plans",
    filter: { category: "billing", version: "2.0" }
  }

Use this to:
  - Search documentation or knowledge bases using natural language
  - Build AI-powered Q&A features for end users
  - Find relevant context for AI assistants
  - Power search bars with semantic understanding

Common errors:
  - RESOURCE_NOT_FOUND: App or collection doesn't exist
  - COLLECTION_EMPTY: No documents have been ingested yet

Idempotency: Safe to call anytime (read-only operation).

Read only: yes. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `app_id` (`string`, required): The app ID (e.g. app_abc123def456)
- `collection` (`string`, required): The collection name to query
- `query` (`string`, required): Natural language query or search phrase
- `filter` (`object`, optional): Metadata filter to narrow results (e.g. { category: "faq" })
- `model` (`string`, optional): LLM model for synthesis (only used when synthesize is true)
- `synthesize` (`boolean`, optional): If true, generate a natural language answer from retrieved chunks using an LLM. Default: `false`.
- `threshold` (`number`, optional): Minimum similarity score (0-1). Only chunks scoring above this threshold are returned.
- `top_k` (`number`, optional): Number of top matching chunks to return (default 5, max 20). Default: `5`.

Example input: `{"app_id":"<app_id>","collection":"<collection>","query":"<query>"}`

### `butterbasemcp_select_rows`

Select Rows · Read-only

Query rows from a table using the auto-generated REST API.

By default, this tool authenticates with the platform API key (butterbase_service role),
which bypasses Row-Level Security and returns ALL rows regardless of RLS policies.

To test RLS enforcement from this tool, use the as_role and as_user parameters:
  - as_role: "anon" — simulate an anonymous request (butterbase_anon role)
  - as_role: "user", as_user: "<user-uuid>" — simulate a specific end-user (butterbase_user role)

Without as_role, this tool always runs as butterbase_service (full access).

Use this to:
  - Fetch data from tables (as admin/service — sees all rows)
  - Filter, sort, and paginate results
  - Select specific columns

Example — Basic query:
  Input: {
    app_id: "app_abc123",
    table: "posts",
    limit: 10
  }
  Output: [
    { id: "uuid-1", title: "Hello World", created_at: "2024-01-15T10:00:00Z" },
    ...
  ]

Example — With filters:
  Input: {
    app_id: "app_abc123",
    table: "posts",
    filters: {
      "status": "eq.published",
      "created_at": "gt.2024-01-01"
    },
    order: "created_at.desc",
    limit: 20
  }

Filter operators:
  - eq (equals): status=eq.published
  - neq (not equals): status=neq.draft
  - gt (greater than): age=gt.18
  - gte (greater than or equal): age=gte.18
  - lt (less than): price=lt.100
  - lte (less than or equal): price=lte.100
  - like (pattern match): title=like.%hello%
  - ilike (case-insensitive): title=ilike.%hello%
  - is (null/true/false): deleted_at=is.null
  - in (list): id=in.(1,2,3)
  - fts (full-text search): title=fts.hello world

Common errors:
  - VALIDATION_TABLE_NOT_FOUND: Table doesn't exist, use manage_schema (action: "get") to verify
  - VALIDATION_INVALID_SCHEMA: Invalid filter format

Idempotency: Safe to call multiple times (read-only operation).

Read only: yes. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `app_id` (`string`, required): The app ID
- `table` (`string`, required): The table name
- `as_role` (`string`, optional): Simulate a specific role for RLS testing. "anon" for anonymous, "user" for authenticated end-user (requires as_user). One of: `anon`, `user`.
- `as_user` (`string`, optional): User ID (UUID) to simulate when as_role is "user". Required when as_role is "user".
- `filters` (`object`, optional): Filter conditions (column: "operator.value")
- `limit` (`integer`, optional): Maximum number of rows to return
- `offset` (`integer`, optional): Number of rows to skip
- `order` (`string`, optional): Sort order (e.g., "created_at.desc")
- `select` (`string`, optional): Comma-separated column names to return

Example input: `{"app_id":"<app_id>","table":"<table>"}`

### `butterbasemcp_create_frontend_deployment`

Create Frontend Deployment · Write

Create a frontend deployment and get an upload URL. Upload your built frontend as a zip file to the returned URL, then use manage_frontend (action: "start_deployment") to trigger the deploy.

Steps:
  1. Call this tool to get an upload URL
  2. Upload your zip file to the URL (e.g. curl -X PUT "{uploadUrl}" -H "Content-Type: application/zip" --data-binary @frontend.zip)
  3. Call manage_frontend (action: "start_deployment") with the returned deployment_id

Example:
  Input: { app_id: "app_abc123", framework: "react-vite" }
  Output: {
    deployment_id: "uuid-1234",
    uploadUrl: "https://...",
    expiresIn: 900,
    maxSizeBytes: 104857600
  }

Prerequisites:
  - App must exist (use init_app to create)

Free plan: 1 deployment per app. Deploying again automatically replaces the previous deployment (no need to delete first).
Starter+: unlimited deployments.

Framework options:
  - react-vite: React app built with Vite (zip the dist/ folder)
  - nextjs-static: Next.js static export (zip the out/ folder)
  - static: Plain HTML/CSS/JS
  - other: Any framework that produces static output

SPA routing: For SPA frameworks (react-vite, nextjs-static, other), a _redirects file is auto-injected so all routes serve index.html. If your zip already includes a _redirects file, it is preserved.

IMPORTANT — Zip file paths must use forward slashes (/), not backslashes (\). On Windows, zips created with built-in tools use backslashes, which causes all files to be served as text/html (breaking JS/CSS with MIME errors). On Windows use Git Bash or WSL to run: cd dist && zip -r ../frontend.zip .

Common errors:
  - RESOURCE_NOT_FOUND: App doesn't exist

Idempotency: Not idempotent — creates a new deployment each time (replaces existing on free plan).

Your frontend will be deployed to https://<app-name>.butterbase.dev.

Next steps: Upload your zip to the returned URL, then call manage_frontend (action: "start_deployment").

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `app_id` (`string`, required): The app ID
- `framework` (`string`, optional): Frontend framework type. One of: `react-vite`, `nextjs-static`, `static`, `other`.

Example input: `{"app_id":"<app_id>"}`

### `butterbasemcp_deploy_function`

Deploy Function · Write

Deploy or update a serverless function with custom business logic.

Example:
  Input: {
    app_id: "app_abc123",
    name: "send-welcome-email",
    code: "export async function handler(req, ctx) { ... }",
    trigger: {
      type: "http",
      config: { method: "POST", path: "/welcome", auth: "required" }
    }
  }
  Output: {
    function_id: "fn_xyz789",
    name: "send-welcome-email",
    url: "https://api.butterbase.ai/v1/app_abc123/fn/send-welcome-email",
    status: "deployed"
  }

Function signature:
  export async function handler(request: Request, context: {
    db: PostgresClient,    // Query your app database
    env: Record<string, string>,  // Access envVars
    user: { id: string } | null,  // Current user (if auth: required)
    waitUntil: (promise: Promise) => void,  // Keep alive for background work after response
    idempotency: {                          // Webhook / event dedup primitive
      claim: (key: string, opts?: { scope?: string; ttlSeconds?: number }) => Promise<boolean>
    }
  }): Promise

Console output: console.log(), console.info(), console.warn(), console.error(), and console.debug() calls are captured and stored with invocation logs. View them via manage_function (action: "get_logs").

IMPORTANT: Handlers MUST return a Response object (Web API standard).
Do NOT return plain objects like { status: 200, body: "..." }.

Idempotent webhook handlers with ctx.idempotency.claim():
  Third-party webhook providers (Stripe, Telegram, GitHub, Slack, Twilio, Discord)
  retry delivery on non-2xx responses with the same event id. Use ctx.idempotency.claim()
  to atomically dedupe — it returns true if you're the first to see this key, false if
  another invocation already claimed it.

  export async function handler(req, ctx) {
    const event = await req.json();
    if (!(await ctx.idempotency.claim(event.id))) {
      // Already processed — ack the retry without re-doing work.
      return new Response('duplicate', { status: 200 });
    }
    await processEvent(event);
    return new Response('ok', { status: 200 });
  }

  Options:
    - scope: 'stripe' | 'telegram' | ... (default: 'default'). Namespace claims so
      keys from different providers can never collide.
    - ttlSeconds: mark the claim with an expiry. Cleanup is your responsibility:
      DELETE FROM _idempotency_keys WHERE expires_at < now();

Background work with ctx.waitUntil():
  Use ctx.waitUntil(promise) to keep the function alive after the response is sent.
  This is useful for fire-and-forget tasks like sending emails or logging.
  Background work has a 30-second timeout. ctx.db is available inside waitUntil promises.

  export async function handler(req, ctx) {
    ctx.waitUntil(fetch("https://api.email.com/send", { method: "POST", body: "..." }));
    return new Response(JSON.stringify({ accepted: true }), {
      headers: { "Content-Type": "application/json" }
    });
  }

Example:
  export async function handler(req, ctx) {
    const data = { hello: "world" };
    return new Response(JSON.stringify(data), {
      status: 200,
      headers: { "Content-Type": "application/json" }
    });
  }

Server-to-server calls from inside a function:
  - ctx.invoke('fn-name', body?, opts?) — call a sibling function in the same app.
    Platform-managed bearer, no env plumbing. ctx.user.id propagates automatically;
    the callee sees ctx.caller.type === 'loopback'. Loop-depth capped at 4.
  - ctx.invokeDO('class-name', 'instance-key', body?, opts?) — call a sibling
    Durable Object in the same app. Routed through a platform shim; no public
    URL or bearer to plumb. Depth shared with ctx.invoke.
  See docs → Functions → Server-to-server, and Durable Objects → Server-to-server.

Row-Level Security in Functions:
  Functions respect RLS policies based on how they're invoked:

  - Invoked with end-user JWT → butterbase_user role (RLS enforced)
    * ctx.db queries see only the user's data
    * ctx.user.id contains the authenticated user ID
    * Use case: User-facing operations

  - Invoked with platform API key → butterbase_service role (RLS bypassed)
    * ctx.db queries see all data
    * ctx.user is null
    * Use case: Admin operations, background jobs

  - Invoked by cron trigger → butterbase_service role (RLS bypassed)
    * ctx.db queries see all data
    * ctx.user is null
    * Use case: Scheduled tasks, cleanup jobs

Trigger types:
  - http: Invoke via HTTP request (GET, POST, etc)
  - cron: Schedule periodic execution (e.g., "0 9 * * *" = daily at 9am)
  - websocket: Trigger on WebSocket event from client via realtime connection
  - s3_upload: Trigger on file upload [not yet implemented]
  - webhook: Receive webhooks from external services [not yet implemented]

Common errors:
  - VALIDATION_INVALID_SCHEMA: Check code exports a handler function
  - RESOURCE_NOT_FOUND: App doesn't exist
  - Syntax error: Code must be valid TypeScript/JavaScript

envVars on redeploy: MERGES into the function's existing env by default —
incoming keys overwrite matching existing keys, everything else is kept.
Pass envVarsReplace: true to instead REPLACE the entire env blob with just
the keys you send (deletes every key you don't include). Use manage_function
(action: "update_env") for a dedicated partial update, including deleting a
single key by setting its value to null.

Idempotency: Safe to call multiple times (updates existing function with same name).

Next steps: Use invoke_function to test, then manage_function (action: "get_logs") to debug.

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `app_id` (`string`, required): The app ID to deploy the function to
- `code` (`string`, required): TypeScript/JavaScript code that exports a handler function
- `name` (`string`, required): Function name (alphanumeric, hyphens, underscores)
- `agent_tool` (`boolean`, optional): When true, this function is exposed to agents in this app as a tool. Agents must still list its name under their graph_spec.tools.functions[] to call it.
- `agent_tool_description` (`string`, optional): Short imperative description shown to the LLM (max 500 chars). Required for any function the model is expected to choose intelligently.
- `agent_tool_exposed_to` (`string`, optional): developer_only (default) — only dashboard/CLI test runs can call it. end_user — also callable by public agent invocations. One of: `developer_only`, `end_user`.
- `agent_tool_mode` (`string`, optional): read_only (default) | read_write. read_write tools require human approval per call (HITL). One of: `read_only`, `read_write`.
- `allow_service_key_impersonation` (`boolean`, optional): Default true. Lets an app-scoped service-key caller assert "act as user X" via the X-Butterbase-As-User header — the runtime populates ctx.user with the asserted id before invoking. Set to false on admin-only or billing-webhook handlers that must never accept an as-user assertion: control-api will 403 such calls at the edge.
- `description` (`string`, optional): Function description
- `envVars` (`object`, optional): Environment variables (will be encrypted). On redeploy, MERGES into the existing env by default (incoming keys overwrite matching keys; other existing keys are kept). Pass envVarsReplace: true to replace the entire env blob instead.
- `envVarsReplace` (`boolean`, optional): Default false (merge). Set true to REPLACE the entire env blob with just the envVars you send on this call, deleting any existing key not included.
- `memoryLimitMb` (`number`, optional): Memory limit in MB (default: 128)
- `timeoutMs` (`number`, optional): Execution timeout in milliseconds (default: 30000)
- `trigger` (`object`, optional): Single-trigger shorthand. Prefer `triggers` for new code; this is kept for back-compat.
- `triggers` (`array`, optional): Canonical multi-trigger array. At most one trigger per type.

Example input: `{"app_id":"<app_id>","code":"<code>","name":"Production Deploy Key"}`

### `butterbasemcp_init_app`

Initialize App · Write

Create a new backend app with isolated database and API endpoints.

Returns: app_id, api_url, url (frontend URL), and provisioning status.

Example:
  Input: { name: "my-blog" }
  Output: {
    app_id: "app_abc123",
    api_url: "https://api.butterbase.dev/v1/app_abc123",
    url: "https://my-blog.butterbase.dev",
    _meta: { next_actions: [...] }
  }

URL guide:
  - api_url: Your API endpoint for database queries, auth, and functions (e.g. https://api.butterbase.dev/v1/app_abc123)
  - url: Your frontend URL where your deployed site is served (e.g. https://my-blog.butterbase.dev)
  - These are different! The api_url is for backend requests, the url is where users visit your app.

Next steps: Use manage_schema (action: "apply") to define tables, then manage_oauth (action: "configure") for auth.

Common errors:
  - Name already exists: Choose a different name or use manage_app (action: "list") to find existing app
  - Invalid characters: Use only lowercase letters, numbers, hyphens, underscores
  - Name too long: Maximum 63 characters

The response includes _meta.next_actions with recommended next steps.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `name` (`string`, required): App name (lowercase alphanumeric, hyphens, underscores)
- `organization_id` (`string`, optional): Organization to create the app under. Requires membership. Defaults to the API key's bound org, or the caller's personal org.
- `region` (`string`, optional): Region to provision in (e.g. "us-east-1", "us-west-2"). Defaults to the control-api's home region.

Example input: `{"name":"Production Deploy Key"}`

### `butterbasemcp_insert_row`

Insert Row · Write

Insert a new row into a table using the auto-generated REST API.

By default, this tool authenticates with the platform API key (butterbase_service role),
which bypasses Row-Level Security. Inserts via this tool are not subject to RLS policies.

To test RLS enforcement on writes, use the as_role and as_user parameters:
  - as_role: "anon" — simulate an anonymous insert (butterbase_anon role)
  - as_role: "user", as_user: "<user-uuid>" — simulate a specific end-user (butterbase_user role)

Without as_role, this tool always runs as butterbase_service (full access, bypasses RLS).

Use this to:
  - Add new records to tables (as admin/service — bypasses RLS)
  - Bootstrap initial data
  - Create test data

Example:
  Input: {
    app_id: "app_abc123",
    table: "posts",
    data: {
      "title": "Hello World",
      "body": "This is my first post",
      "status": "draft"
    }
  }
  Output: {
    id: "uuid-1234",
    title: "Hello World",
    body: "This is my first post",
    status: "draft",
    created_at: "2024-01-15T10:00:00Z"
  }

Notes:
  - Only provide columns that exist in the table schema
  - Columns with defaults (like id, created_at) can be omitted
  - The response includes the full inserted row with generated values

Common errors:
  - VALIDATION_TABLE_NOT_FOUND: Table doesn't exist, use manage_schema (action: "get") to verify
  - VALIDATION_UNIQUE_CONSTRAINT_VIOLATION: Duplicate value in unique column
  - VALIDATION_FOREIGN_KEY_VIOLATION: Referenced record doesn't exist
  - VALIDATION_NOT_NULL_VIOLATION: Required field is missing

Idempotency: Not idempotent - creates a new row each time.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `app_id` (`string`, required): The app ID
- `data` (`object`, required): Column values to insert
- `table` (`string`, required): The table name
- `as_role` (`string`, optional): Simulate a specific role for RLS testing. "anon" for anonymous, "user" for authenticated end-user (requires as_user). One of: `anon`, `user`.
- `as_user` (`string`, optional): User ID (UUID) to simulate when as_role is "user". Required when as_role is "user".

Example input: `{"app_id":"<app_id>","data":{},"table":"<table>"}`

### `butterbasemcp_invoke_function`

Invoke Function · Write

Invoke a deployed function and return its full HTTP response.

Example — POST with body:
  Input: {
    app_id: "app_abc123",
    function_name: "submit-inquiry",
    body: { email: "user@example.com", message: "hello" }
  }
  Output: {
    status: 200,
    headers: { "content-type": "application/json" },
    body: { id: "uuid-1234" },
    duration_ms: 47
  }

Example — GET (no body):
  Input: {
    app_id: "app_abc123",
    function_name: "public-catalog",
    method: "GET"
  }

Parameters:
  - method defaults to POST. The function's trigger config determines which methods are valid.
  - body is sent as JSON. Omit for GET/HEAD requests.
  - headers are merged with the default auth headers.

Use this to:
  - Test a function immediately after deployment
  - Debug function logic with different inputs
  - Verify function response format and status codes

Common errors:
  - RESOURCE_NOT_FOUND: Function doesn't exist, use manage_function (action: "list") to verify
  - Function timeout: Increase timeoutMs in deploy_function
  - Runtime error: Check manage_function (action: "get_logs") for stack trace

Idempotency: Depends on function implementation (may have side effects).

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `app_id` (`string`, required): The app ID
- `function_name` (`string`, required): The function name to invoke
- `body` (`string`, optional): Request body (sent as JSON)
- `headers` (`object`, optional): Additional request headers
- `method` (`string`, optional): HTTP method (default: POST). One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Default: `POST`.

Example input: `{"app_id":"<app_id>","function_name":"<function_name>"}`

### `butterbasemcp_manage_ai`

Manage AI · Write

Use the app's AI gateway: chat, embeddings, list models, read/update config, read usage.

Actions:
  - chat              { app_id, messages, model?, temperature?, max_tokens? }
                       Synchronous (no streaming). Returns the full assistant response.
                       Default model is the app's configured default, or "openai/gpt-4o-mini".
  - embed             { app_id, input (string | string[]), model?, encoding_format? }
                       Returns OpenAI-shaped embedding response.
  - decide            { app_id, questions, state?, model? }
                       Typed decisions (choice / yes-no "noul" / score) with probabilities from a
                       decision model (default typesafe/jev-1.13). NOT for text generation — use
                       chat. Billed on input tokens. Unrelated to substrate decisions.
  - list_models       { app_id, modality? }
                       Returns { models: AiModel[] } — discover what the app can call.
                       modality: chat | embedding | image | video | audio | decisions.
  - get_config        { app_id }
                       Returns { defaultModel, allowedModels, maxTokensPerRequest, ... }
  - update_config     { app_id, config }
                       Set defaultModel, defaultDecisionModel, allowedModels, maxTokensPerRequest (1–100000), or rotate BYOK.
  - get_usage         { app_id, startDate?, endDate? }
                       Aggregate token counts and costs over a window.
  - submit_video      { app_id, model, prompt, duration?, resolution?, aspect_ratio?, generate_audio?, seed?, input_images?, input_references?, frame_images?, reference_declarations?, prepare_references?, provider? }
                       Submits an async video-generation job. Returns { job_id, status, polling_url }.
                       Poll the returned URL until status is "completed".
                       For image-to-video, supply seed imagery in the shape the target model's
                       upstream expects — frame_images / input_references objects are forwarded
                       verbatim; input_images accepts plain HTTPS URL strings. Seed params are
                       passed through without inspection, so consult list_models for the model's
                       accepted vocabulary rather than assuming a single canonical form.
  - poll_video        { app_id, job_id }
                       Returns current { status, model, content_urls?, error?, created_at }.
                       When status is "completed", content_urls contains absolute URLs (same origin
                       as the polling_url) that the caller can fetch() directly using the same
                       Authorization header. Use this to drive your own polling loop.
  - submit_image      { app_id, model, prompt, size?, aspect_ratio?, n?, seed?, input_images?, mask?, negative_prompt?, provider? }
                       Submits an async image-generation job. Returns { job_id, status, polling_url }.
                       Model IDs: openai/gpt-image-2, google/gemini-3-pro-image-preview, bytedance/seedream-5-pro,
                       alibaba/wan-2.7-image-pro, prunaai/p-image-edit — see list_models for the full catalog.
                       Different models accept different params; a param outside the model's whitelist returns
                       400 UNSUPPORTED_PARAM listing what the model does accept. Use "provider" for model-specific
                       extras (e.g. { bbox_list, color_palette } for Wan; { quality, background, mask } for GPT Image).
  - poll_image        { app_id, job_id }
                       Returns current { status, model, content_urls?, error?, created_at, charged_credits_usd }.
                       When status is "completed", content_urls contains absolute URLs (image/png or image/jpeg)
                       fetchable with the same Authorization header. Content URLs expire ~30 days after generation —
                       mirror to your own storage if you need long-term retention.
  - start_meeting     { app_id, meeting_url, transcript?, recording?, metadata?, bot_name?, automatic_leave? }
                       Spawn a meeting bot that joins a Zoom/Meet/Teams/Webex call.
                       recording: "mp4" (default), "audio_only", or false. transcript defaults to true.
                       bot_name (1–64 chars) sets the display name attendees see; defaults to "Butterbase Notetaker".
                       automatic_leave: per-bot overrides for the auto-leave timers (in seconds, positive ints, max 86400).
                         Any sub-field omitted inherits the provider default.
                         - waiting_room_timeout_sec       — leave if still in the waiting room after N seconds
                         - no_one_joined_timeout_sec      — leave if no participants joined within N seconds
                         - everyone_left_timeout_sec      — leave N seconds after the last participant leaves
                         - in_call_not_recording_timeout_sec — leave if in-call but not recording for N seconds
                       Example: automatic_leave: { waiting_room_timeout_sec: 900 } makes the bot give up after 15 min in the waiting room.
                       Returns { id, status, botName, ... }. Save id to call get_meeting / stop_meeting later.
  - get_meeting       { app_id, meeting_id }
                       Current status + recordingUrl / transcriptUrl (populated when artifacts are ready).
  - list_meetings     { app_id, status?, limit?, cursor? }
                       Page through this app's bots. status filters to a lifecycle phase
                       (joining / waiting_room / in_call / recording / ended / done / fatal).
  - stop_meeting      { app_id, meeting_id }
                       Force the bot to leave the call. Returns 204 / no body on success.
  - estimate_meeting  { app_id, duration_minutes, transcript? }
                       Predict the USD charge for a hypothetical session at this duration.
  - configure_meetings_webhook  { app_id, forward_url, rotate_secret? }
                       Upsert the app's meetings webhook forward URL.
                       When rotate_secret is true (or no row exists), generates a new signing secret
                       (wsec_…) and returns it once — store it immediately. The secret is the
                       HMAC-SHA256 key that signs every forwarded event for THIS app — see the
                       "meetings" topic in butterbase_docs for verification details.
                       Returns { ok, app_id, forward_url, secret } where secret is null when not rotated.
  - usage_meetings    { app_id }
                       Returns the last 100 rows from actor_usage_logs for the app.
                       Each row has { id, dimension, seconds, usd_charged, created_at }.

This tool wraps the same /v1/:app_id/chat/completions, /embeddings, /ai/decide, /ai/config, /ai/models,
/ai/usage routes the SDK uses. The "chat" action sets stream: false; for streamed deltas,
drive the SDK from inside a function or DO.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `chat`, `embed`, `decide`, `list_models`, `get_config`, `update_config`, `get_usage`, `submit_video`, `poll_video`, `submit_image`, `poll_image`, `start_meeting`, `get_meeting`, `list_meetings`, `stop_meeting`, `estimate_meeting`, `configure_meetings_webhook`, `usage_meetings`.
- `app_id` (`string`, required): The app ID
- `aspect_ratio` (`string`, optional): No description.
- `automatic_leave` (`object`, optional): For start_meeting — per-bot overrides for auto-leave timers (seconds). Any field omitted inherits the provider default. e.g. { waiting_room_timeout_sec: 900 } leaves if not admitted within 15 min.
- `bot_name` (`string`, optional): For start_meeting — display name the bot uses when it joins (1–64 chars). Defaults to "Butterbase Notetaker".
- `config` (`object`, optional): Required for update_config
- `cursor` (`string`, optional): For list_meetings pagination
- `duration` (`integer`, optional): No description.
- `duration_minutes` (`integer`, optional): Required for estimate_meeting
- `encoding_format` (`string`, optional): No description. One of: `float`, `base64`.
- `endDate` (`string`, optional): No description.
- `forward_url` (`string`, optional): Required for configure_meetings_webhook
- `frame_images` (`array`, optional): For submit_video: seed frames in the upstream's native object form, forwarded as-is — e.g. { type: "image_url", image_url: { url }, frame_type: "first_frame" | "last_frame" }
- `generate_audio` (`boolean`, optional): No description.
- `input` (`string`, optional): Required for embed
- `input_images` (`array`, optional): For submit_image / submit_video: HTTPS URLs for reference / image-to-image / edit source
- `input_references` (`array`, optional): For submit_video: style/content reference images (reference-to-video). HTTPS URL strings, or the upstream's native object form, forwarded as-is
- `job_id` (`string`, optional): Required for poll_video / poll_image
- `limit` (`integer`, optional): For list_meetings (default 20)
- `mask` (`string`, optional): For submit_image: HTTPS URL of alpha mask (GPT Image 2 edit mode)
- `max_tokens` (`integer`, optional): No description.
- `meeting_id` (`string`, optional): Required for get_meeting / stop_meeting
- `meeting_url` (`string`, optional): Required for start_meeting
- `messages` (`array`, optional): Required for chat
- `metadata` (`object`, optional): For start_meeting — arbitrary string→string map; keys may not start with bb_
- `modality` (`string`, optional): For list_models: only return models of this type. One of: `chat`, `embedding`, `image`, `video`, `audio`, `decisions`.
- `model` (`string`, optional): No description.
- `n` (`integer`, optional): For submit_image: number of images, subject to the selected model limit (up to 10 for GPT Image; 1 for Grok and FLUX 3)
- `negative_prompt` (`string`, optional): For submit_image: text of what to avoid (Wan 2.6 only)
- `prepare_references` (`string`, optional): For submit_video with reference_declarations: prepare declared references on an exact input-face/privacy rejection (default) or upfront. Does not retry general moderation failures. One of: `on_rejection`, `upfront`.
- `prompt` (`string`, optional): Required for submit_video / submit_image
- `provider` (`object`, optional): For submit_image: model-specific passthrough params (e.g. bbox_list, quality, optimize_prompt_options)
- `questions` (`object`, optional): Required for decide: { <name>: { type: "choice"|"noul"|"score", instructions, criteria } }
- `recording` (`string`, optional): For start_meeting: "mp4" (default), "audio_only", or false to skip recording. One of: `mp4`, `audio_only`, `false`, `false`.
- `reference_declarations` (`array`, optional): For submit_video: explicitly declare AI-generated fictional character references by canonical slot path, e.g. /input_images/0. Available only for enabled apps and supported models; never declare real people.
- `resolution` (`string`, optional): No description.
- `rotate_secret` (`boolean`, optional): For configure_meetings_webhook — generate a new signing secret
- `seed` (`integer`, optional): No description.
- `size` (`string`, optional): For submit_image: e.g. "1024x1024", "1K", "2K", "4K" (model-specific)
- `startDate` (`string`, optional): No description.
- `state` (`string`, optional): For decide: the application state the questions are about (any JSON)
- `status` (`string`, optional): For list_meetings — filter to one lifecycle phase. One of: `joining`, `waiting_room`, `in_call`, `recording`, `ended`, `done`, `fatal`.
- `temperature` (`number`, optional): No description.
- `transcript` (`boolean`, optional): For start_meeting / estimate_meeting (default true)

Example input: `{"action":"chat","app_id":"<app_id>"}`

### `butterbasemcp_manage_app`

Manage App · Write

Manage app lifecycle: list, delete, pause, resume, get config, update access mode, secure, update CORS, clone, find templates, and migrate regions.

Actions:
  - "list":                    List all backend apps with basic metadata (no app_id needed)
  - "delete":                  Delete an app and ALL its resources permanently (IRREVERSIBLE)
  - "pause":                   Pause all data-plane traffic for an app (kill-switch). Pass paused:false to resume (prefer "resume").
  - "resume":                  Resume a paused app. An app paused automatically after inactivity on a free plan returns 402 APP_RESUME_REQUIRES_UPGRADE; upgrading resumes it
  - "get_config":              Get detailed configuration for an app including CORS, storage settings, and metadata
  - "set_visibility":          Toggle the app's template visibility between "public" and "private"
  - "update_access_mode":      Toggle an app's access mode between "public" and "authenticated"
  - "secure":                  Lock down an app: sets access_mode to "authenticated" and optionally enables RLS user isolation
  - "update_cors":             Update CORS allowed origins to control which frontend domains can access your API
  - "preview_clone_env_vars":  Preview which env vars a source app's functions and Durable Objects need before cloning. Returns { functions: [{ fn_name, keys, conventions }], durable_objects: { env_keys, note? }, app_env: { keys, note } }. Call this before clone to decide what to supply via env_var_values or auto_mint_api_key. DO env values are never carried across clones — re-set them via manage_durable_objects action=set_env after clone completes. app_env.keys are the app-level env var names that WILL be copied to the clone (inherited config, not per-function overrides).
  - "clone":                   Create a clone of a public app. Returns { job_id, pending_env_vars }. The dest app is a fresh empty-DB app owned by the caller. Source must be public and have a repo snapshot. Supply env_var_values and/or auto_mint_api_key to pre-fill function env vars; pending_env_vars lists keys still needing values. app_env_vars are copied to the clone (values included). Override any of them with manage_app action: "update_env" after the clone completes.
  - "get_clone_job":           Look up the status of a previously-started clone job. Returns { status, dest_app_id?, error_message? }.
  - "find_templates":          Search public templates by name, region, sort order, and pagination. Returns paginated list of public app templates.
  - "set_clone_webhook":       Set or clear a webhook that fires when someone clones this app. Pass webhook_url + webhook_secret to configure, or clear_webhook: true to remove.
  - "link_substrate":              Link this app to the caller's substrate. Once linked, the app's deployed functions receive ctx.substrate and its actions/entities flow into the caller's substrate ledger.
  - "unlink_substrate":            Unlink this app from substrate. ctx.substrate stops being injected; in-flight actions are unaffected.
  - "set_substrate_autopropagate": Toggle per-event auto-mirroring of app activity into the linked owner's substrate. Currently supports 'users' (signup / email-verified / user-deleted). Requires the app to already be linked via 'link_substrate'.
  - "move":                    Migrate an app to a different region. Returns migration_id + initial status "queued".
  - "move_status":             Get the current status of an in-progress migration.
  - "teardown_source_replica": After a completed move, decommission the retained source-region replica.
  - "get_env":              Get app-level environment variables. Returns { keys, updated_at }.
  - "update_env":           Update app-level environment variables. Values null delete a key. Returns { updated_keys, invalidated }.
  - "publish_template_release": Publish a new release of this app as a template. Returns release number, timestamp, and metadata.
  - "list_template_releases": List all releases of a template. Returns array of release metadata.
  - "get_template_release": Get detailed info about a template release (commit hash, timestamp, notes, divergence summary).
  - "check_template_updates": Check if this fork has updates available from its template (returns divergence status and available release details).
  - "update_from_template": Reset this fork's code to match the template. Only allowed for unmodified forks; returns job_id on success.

Parameters by action:
  list:                    { action: "list" }
  delete:                  { action: "delete", app_id }
  pause:                   { action: "pause", app_id, paused, reason? }
  resume:                  { action: "resume", app_id }
  get_config:              { action: "get_config", app_id }
  set_visibility:          { action: "set_visibility", app_id, visibility, listed? }
  update_access_mode:      { action: "update_access_mode", app_id, access_mode }
  secure:                  { action: "secure", app_id, tables? }
  update_cors:             { action: "update_cors", app_id, allowed_origins }
  preview_clone_env_vars:  { action: "preview_clone_env_vars", source_app_id }
  clone:                   { action: "clone", source_app_id, name?, region?, env_var_values?, auto_mint_api_key? }
  get_clone_job:           { action: "get_clone_job", job_id }
  find_templates:          { action: "find_templates", q?, region?, sort?, limit?, offset? }
  set_clone_webhook:       { action: "set_clone_webhook", app_id, webhook_url, webhook_secret } or { action: "set_clone_webhook", app_id, clear_webhook: true }
  link_substrate:              { action: "link_substrate", app_id }
  unlink_substrate:            { action: "unlink_substrate", app_id }
  set_substrate_autopropagate: { action: "set_substrate_autopropagate", app_id, users? }
  move:                    { action: "move", app_id, dest_region }
  move_status:             { action: "move_status", app_id, migration_id }
  teardown_source_replica: { action: "teardown_source_replica", migration_id }
  get_env:                 { action: "get_env", app_id }
  update_env:              { action: "update_env", app_id, env }
  publish_template_release: { action: "publish_template_release", app_id, label?, notes? }
  list_template_releases: { action: "list_template_releases", app_id or source_app_id }
  get_template_release: { action: "get_template_release", app_id or source_app_id, release_number }
  check_template_updates: { action: "check_template_updates", app_id }
  update_from_template: { action: "update_from_template", app_id }

Common errors:
  - RESOURCE_NOT_FOUND: App doesn't exist, verify app_id with action: "list"
  - AUTH_INVALID_API_KEY: Check your API key is set correctly

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `list`, `delete`, `pause`, `resume`, `get_config`, `update_access_mode`, `secure`, `update_cors`, `set_visibility`, `preview_clone_env_vars`, `clone`, `get_clone_job`, `find_templates`, `set_clone_webhook`, `link_substrate`, `unlink_substrate`, `set_substrate_autopropagate`, `move`, `move_status`, `teardown_source_replica`, `get_env`, `update_env`, `publish_template_release`, `list_template_releases`, `get_template_release`, `check_template_updates`, `update_from_template`.
- `access_mode` (`string`, optional): Required for "update_access_mode". "public" allows anonymous access, "authenticated" requires end-user JWT or API key. One of: `public`, `authenticated`.
- `allowed_origins` (`array`, optional): Required for "update_cors". Array of allowed origin URLs (e.g. ["http://localhost:3000", "https://myapp.com"])
- `app_id` (`string`, optional): The app ID (e.g. app_abc123def456). Required for all actions except "list".
- `auto_mint_api_key` (`array`, optional): Optional for "clone". Keys to auto-mint a scoped bb_sk_* into, typically [{ fn_name: "agent-chat", key: "BUTTERBASE_API_KEY" }].
- `clear_webhook` (`boolean`, optional): Optional for "set_clone_webhook". Pass true to remove the clone webhook instead of setting one.
- `dest_region` (`string`, optional): Required for "move". Target region slug (e.g. "us-west-2").
- `env` (`object`, optional): Required for "update_env". App-level env vars to merge. Value null deletes a key.
- `env_var_values` (`object`, optional): Optional for "clone". Per-function env var values: { fn_name: { KEY: "value" } }. Use preview_clone_env_vars to see what keys the source needs.
- `job_id` (`string`, optional): Required for "get_clone_job".
- `label` (`string`, optional): Display label for a published release, e.g. "v1.2.0".
- `limit` (`integer`, optional): Optional for "find_templates". Max results per page (default 20).
- `listed` (`boolean`, optional): Optional for "set_visibility". When false and visibility=public, app is clonable by direct ID but not in /v1/templates.
- `migration_id` (`string`, optional): Required for "move_status" and "teardown_source_replica". The migration ID returned by action: "move".
- `name` (`string`, optional): Optional for "clone". A name for the new app; defaults to `Clone of <source_app_id>`.
- `notes` (`string`, optional): Release notes. For forks that have customized their app, these notes are the main way they benefit from an update — be specific about what changed.
- `offset` (`integer`, optional): Optional for "find_templates". Pagination offset (default 0).
- `organization_id` (`string`, optional): Optional for "clone" and "link_substrate". For "clone", the org to create the destination app under (requires membership; defaults to caller's personal org). For "link_substrate", the org whose substrate the app should be linked to (requires caller membership; server 403s otherwise; defaults to caller's active/personal org).
- `paused` (`boolean`, optional): Required for "pause". true to pause; false to resume.
- `q` (`string`, optional): Optional for "find_templates". Search query to filter templates by name.
- `reason` (`string`, optional): Optional for "pause". Human-readable reason; surfaced in 503 responses while paused.
- `region` (`string`, optional): Optional for "clone". The region for the new app; defaults to the source app's region.
- `release_number` (`integer`, optional): Which release to fetch.
- `sort` (`string`, optional): Optional for "find_templates". Sort order: "recent" or "popular". Defaults to "recent". One of: `recent`, `popular`.
- `source_app_id` (`string`, optional): Required for "clone" and "preview_clone_env_vars". The id of the public app to clone.
- `tables` (`array`, optional): Optional for "secure". Tables to enable RLS user isolation on. Omit to only toggle access_mode.
- `users` (`boolean`, optional): Optional for "set_substrate_autopropagate". true to mirror user signup / email-verified / user-deleted events into the substrate; false to disable. At least one toggle key must be provided.
- `visibility` (`string`, optional): Required for "set_visibility". Template visibility. One of: `private`, `public`.
- `webhook_secret` (`string`, optional): Required for "set_clone_webhook" (unless clear_webhook is true). Secret used to sign the HMAC-SHA256 webhook payload (16–256 characters).
- `webhook_url` (`string`, optional): Required for "set_clone_webhook" (unless clear_webhook is true). HTTPS URL to receive clone event POST requests.

Example input: `{"action":"list"}`

### `butterbasemcp_manage_auth_config`

Manage Auth Config · Write

Manage authentication configuration for an app.

Actions:
  - "configure_auth_hook": Configure a post-authentication hook function
  - "update_jwt":           Update JWT token expiration times
  - "generate_service_key": Generate a new API key (service key)

Parameters by action:
  configure_auth_hook: { app_id, action: "configure_auth_hook", post_auth_function }
  update_jwt:          { app_id, action: "update_jwt", accessTokenTtl?, refreshTokenTtlDays? }
  generate_service_key: { action: "generate_service_key", name }

---

### configure_auth_hook

Configure a post-authentication hook function for an app.

When set, the specified Butterbase function is invoked (fire-and-forget) after
every successful auth event: OAuth login, email login, and email signup.

The hook function receives a JSON POST body:
  {
    "event": "oauth_login" | "signup" | "login",
    "user": { "id": "uuid", "email": "...", "provider": "google", "display_name": "...", "avatar_url": "..." },
    "isNewUser": true | false,
    "provider": "google" | "github" | "email" | ...
  }

The function runs as butterbase_service (RLS bypassed, ctx.user is null).
Use the payload body to identify the user.

Set post_auth_function to null to remove the hook.

Prerequisites: The function must be deployed first (use deploy_function).

Example — set hook:
  Input: { app_id: "app_abc123", action: "configure_auth_hook", post_auth_function: "on-auth" }
  Output: { auth_hook_function: "on-auth", message: "Post-auth hook set to function \"on-auth\"" }

Example — remove hook:
  Input: { app_id: "app_abc123", action: "configure_auth_hook", post_auth_function: null }
  Output: { auth_hook_function: null, message: "Post-auth hook removed" }

Common errors:
  - Function not found: Deploy the function first before configuring it as a hook.

Idempotency: Safe to call multiple times (overwrites previous setting).

---

### update_jwt

Update JWT token expiration times for access and refresh tokens.

Example:
  Input: {
    app_id: "app_abc123",
    action: "update_jwt",
    accessTokenTtl: "1h",
    refreshTokenTtlDays: 30
  }
  Output: {
    message: "JWT config updated",
    app_id: "app_abc123",
    jwt_config: {
      accessTokenTtl: "1h",
      refreshTokenTtlDays: 30
    }
  }

Token types:
  - Access token: Short-lived token for API requests (default: 15m)
  - Refresh token: Long-lived token to get new access tokens (default: 7 days)

Time formats:
  - Access token: "15m", "1h", "2h", "1d" (s=seconds, m=minutes, h=hours, d=days)
  - Refresh token: Integer days (7, 30, 90)

Use this to:
  - Increase security with shorter access tokens
  - Improve UX with longer refresh tokens
  - Balance security vs. convenience

Common errors:
  - RESOURCE_NOT_FOUND: App doesn't exist
  - VALIDATION_INVALID_SCHEMA: Check time format is valid

Idempotency: Safe to call multiple times (updates config).

Note: Changes apply to new tokens only. Existing tokens keep their original expiration.

---

### generate_service_key

Generate a new API key (service key) for programmatic access to the Control API.

Use this to:
  - Create API keys for automation scripts
  - Generate keys for CI/CD pipelines
  - Provide keys to team members or services

The generated key (bb_sk_...) can be used to:
  - Access all MCP tools programmatically
  - Call the Control API directly
  - Manage apps, schemas, functions, and data

Example:
  Input: {
    action: "generate_service_key",
    name: "CI/CD Pipeline Key"
  }
  Output: {
    key: "bb_sk_a1b2c3d4e5f6...",
    key_id: "uuid-1234",
    prefix: "bb_sk_a1b2c3",
    name: "CI/CD Pipeline Key",
    created_at: "2024-01-15T10:00:00Z"
  }

IMPORTANT: The full key is only shown ONCE. Store it securely - it cannot be retrieved again.

Common errors:
  - AUTH_INSUFFICIENT_PERMISSIONS: Only authenticated users can generate keys

Idempotency: Not idempotent - creates a new key each time.

Security notes:
  - Keys have full access to all your apps and data
  - Treat keys like passwords - never commit them to git
  - Revoke keys immediately if compromised
  - Use descriptive names to track key usage

Example — with substrate access:
  Input: {
    action: "generate_service_key",
    name: "Agent Key",
    substrate_access: true
  }
  Output: {
    key: "bb_sk_a1b2c3d4e5f6...",
    key_id: "uuid-1234",
    prefix: "bb_sk_a1b2c3",
    name: "Agent Key",
    created_at: "2024-01-15T10:00:00Z"
  }
  Note: key works on app endpoints AND on substrate endpoints for this account.

### generate_service_key (app-scoped, for function impersonation)
Input: {
  app_id: "app_abc123",
  action: "generate_service_key",
  name: "My Function Caller",
  key_scope: "app"
}
Output: { key: "bb_sk_...", scopes: ["app:app_abc123", "ai:gateway"], ... }
Use the returned key with the X-Butterbase-As-User header to invoke auth:required functions.

### generate_service_key (account-scoped, default)
Input: {
  action: "generate_service_key",
  name: "Platform Admin"
}
Output: { key: "bb_sk_...", scopes: ["*"], ... }
Use for AI gateway, integrations, control-API surfaces.

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `configure_auth_hook`, `update_jwt`, `generate_service_key`.
- `accessTokenTtl` (`string`, optional): Access token TTL (e.g., "15m", "1h", "2h", "1d"). Supports: s (seconds), m (minutes), h (hours), d (days) (update_jwt only)
- `additional_scopes` (`array`, optional): Optional extra scope tokens to add. Allowed: 'ai:gateway', 'substrate'. Do NOT pass 'app:<id>' or '*' — use key_scope instead. (generate_service_key only)
- `app_id` (`string`, optional): The app ID (required for configure_auth_hook and update_jwt); also required for generate_service_key when key_scope === 'app'.
- `key_scope` (`string`, optional): Whether the key is scoped to your whole account or to a single app. Use 'app' (required for calling auth:required functions via X-Butterbase-As-User impersonation). Use 'account' for platform APIs. Default 'account'. (generate_service_key only). One of: `account`, `app`.
- `name` (`string`, optional): Descriptive name for the key (e.g., "Production Deploy Key") (generate_service_key only)
- `organization_id` (`string`, optional): Optional organization to bind the key to. Caller must be a member. Defaults to the caller's personal organization when omitted. Use this to mint keys that operate on a shared org's substrate/quota/billing. (generate_service_key only)
- `post_auth_function` (`string`, optional): Name of deployed function to call after auth events, or null to remove (configure_auth_hook only)
- `refreshTokenTtlDays` (`integer`, optional): Refresh token TTL in days (e.g., 7, 30) (update_jwt only)
- `substrate_access` (`boolean`, optional): When true, the generated key works for BOTH app operations and substrate operations on the caller's substrate. Default false (app-only). (generate_service_key only)

Example input: `{"action":"configure_auth_hook"}`

### `butterbasemcp_manage_billing`

Manage Billing · Write

Manage billing, plans, usage, and spending caps for the Butterbase platform account.

This is a platform-scoped tool — it operates on the authenticated account, not on a specific app.

Actions:
  - "status":    Get current plan, usage summary, and spending cap in one call
  - "portal":    Generate a Stripe billing portal URL for payment method / invoice management
  - "topup":     Add credit to the account balance (prepaid top-up)
  - "cap_get":   Retrieve the current monthly spending cap
  - "cap_raise": Raise the monthly spending cap by a given amount
  - "plans":     List all available subscription plans with pricing
  - "usage":     Query detailed metered usage for a date range and optional meter type

Parameters by action:
  status:    { action: "status" }
  portal:    { action: "portal" }
  topup:     { action: "topup", amount: <whole USD dollars, $5–$500> }
  cap_get:   { action: "cap_get" }
  cap_raise: { action: "cap_raise", raise_by: <whole USD dollars> }
  plans:     { action: "plans" }
  usage:     { action: "usage", start_date?: "YYYY-MM-DD", end_date?: "YYYY-MM-DD", meter?: "compute" | "storage" | ... }

Examples:

  Check current plan and balance:
    Input:  { action: "status" }
    Output: { plan: "launch", balance_usd: 50, spending_cap_usd: 200, usage: { ... } }

  Open billing portal:
    Input:  { action: "portal" }
    Output: { url: "https://billing.stripe.com/session/..." }

  Top up $25:
    Input:  { action: "topup", amount: 25 }
    Output: { success: true, new_balance_usd: 75 }

  Get current spending cap:
    Input:  { action: "cap_get" }
    Output: { spending_cap_usd: 200 }

  Raise spending cap by $50:
    Input:  { action: "cap_raise", raise_by: 50 }
    Output: { spending_cap_usd: 250 }

  List available plans:
    Input:  { action: "plans" }
    Output: [{ id: "free", name: "Free", ... }, { id: "pro", name: "Pro", ... }]

  Query compute usage for April 2025:
    Input:  { action: "usage", start_date: "2025-04-01", end_date: "2025-04-30", meter: "compute" }
    Output: { usage: [{ date: "2025-04-01", value: 1234 }, ...] }

Common errors:
  - AUTH_INSUFFICIENT_PERMISSIONS: Must be authenticated as the account owner
  - INSUFFICIENT_BALANCE: Account balance too low for top-up operation
  - INVALID_AMOUNT: amount / raise_by must be a positive whole-dollar integer

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The billing action to perform. One of: `status`, `portal`, `topup`, `cap_get`, `cap_raise`, `plans`, `usage`.
- `amount` (`integer`, optional): Amount in whole USD dollars, $5–$500 (required for "topup")
- `end_date` (`string`, optional): End date for usage query in YYYY-MM-DD format (used with "usage")
- `meter` (`string`, optional): Meter type to filter usage by, e.g. "compute", "storage" (used with "usage")
- `org_id` (`string`, optional): Organization id to view billing for. Requires membership. Defaults to the caller's personal org. Applies to "status" and "usage".
- `raise_by` (`integer`, optional): Amount in whole USD dollars to raise the spending cap by (required for "cap_raise")
- `start_date` (`string`, optional): Start date for usage query in YYYY-MM-DD format (used with "usage")

Example input: `{"action":"status"}`

### `butterbasemcp_manage_durable_objects`

Manage Durable Objects · Write

Manage Durable Object (DO) classes for an app: register/update code, list/get/delete, view usage, and manage shared env vars.

DOs are stateful per-key actors that persist state in memory and built-in storage. Use them when
you need state for a single room/user/agent across requests (multiplayer games, chat rooms, rate
limiters, long-running agents). For stateless work, use a Function instead.

Actions:
  - "deploy":     Register or update a DO class (single TypeScript file, one exported class)
  - "list":       List all DO classes for the app
  - "get":        Get a single DO class — includes the source code and current status
  - "delete":     Delete a DO class — Cloudflare immediately deletes all instances and storage; cannot be undone
  - "usage":      Get current-month DO usage (do_requests, do_cpu_ms) — refreshed every 15 min, app-wide totals
  - "list_env":   List all env vars set on the app's DOs
  - "set_env":    Create or overwrite a single env var
  - "delete_env": Remove a single env var

Parameters by action:
  deploy:     { app_id, action: "deploy", name, code, access_mode? }
  list:       { app_id, action: "list" }
  get:        { app_id, action: "get",    name }
  delete:     { app_id, action: "delete", name }
  usage:      { app_id, action: "usage",  name }
  list_env:   { app_id, action: "list_env" }
  set_env:    { app_id, action: "set_env",    key, value }
  delete_env: { app_id, action: "delete_env", key }

Deploy constraints:
  - One TypeScript file, exporting exactly ONE class with fetch(req) and optional state.storage / state.acceptWebSocket
  - No npm imports — only `import { ... } from 'cloudflare:workers'`
  - Max 5 DO classes per app. Bundle (sum of all DO code per app) ≤ 10 MB compressed.
  - Class name in code (PascalCase) is parsed automatically; URL "name" arg is kebab-case.

URL after deploy:
  https://<subdomain>.butterbase.dev/_do/<name>/<instance-id>   (HTTP and WebSocket)

access_mode (v1 — shape check only at the dispatcher; validate inside fetch() for strong auth):
  - "public":         open to anyone
  - "authenticated":  requires Authorization that looks like an end-user JWT (default)
  - "service_key":    requires Authorization starting with "Bearer bb_sk_"

Env vars are key-value pairs injected into every DO class at runtime, scoped to all DO classes within
the app. They are separate from function env vars. After set_env / delete_env, redeploy DOs for the
change to take effect.

Server-to-server ctx: DO code can opt into a per-request context object by calling
`const ctx = butterbase.ctx(req, this.env, this.state)` at the top of `fetch(req)`. From it, DO
code can call a sibling function with `ctx.invoke('fn-name', body)` and call a sibling DO with
`ctx.invokeDO('class-name', 'instance-key', body)` — no bearer plumbing, platform-managed auth.
Old DO code that doesn't call `butterbase.ctx(...)` continues to work unchanged.

Common errors:
  - RESOURCE_NOT_FOUND: app_id or DO class doesn't exist
  - AUTH_INSUFFICIENT_PERMISSIONS: must be app owner or collaborator
  - VALIDATION_ERROR: env key must be alphanumeric + underscores; class code must export exactly one class

Idempotency: deploy/set_env/delete_env are safe to retry. delete is irreversible.

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `deploy`, `list`, `get`, `delete`, `usage`, `list_env`, `set_env`, `delete_env`.
- `app_id` (`string`, required): The app ID (e.g. app_abc123)
- `access_mode` (`string`, optional): Optional for deploy. Default: authenticated. One of: `public`, `authenticated`, `service_key`.
- `code` (`string`, optional): Required for deploy. TypeScript source — must export exactly one class.
- `key` (`string`, optional): Env var name. Required for set_env and delete_env.
- `name` (`string`, optional): DO class URL name (kebab-case). Required for deploy/get/delete/usage.
- `value` (`string`, optional): Env var value. Required for set_env.

Example input: `{"action":"deploy","app_id":"<app_id>"}`

### `butterbasemcp_manage_edge_ssr`

Manage Edge SSR Deployments · Write

Manage Edge SSR (Cloudflare Workers) deployments: prebuilt-zip flow, server-side build flow, list history.

Actions:
  - "create":             Create a deployment from a locally-built zip; returns upload URL + deployment_id
  - "start":              Start the deployment after the zip is uploaded; polls until READY/ERROR (≤60s)
  - "create_from_source": Server-side build — Mode 1: create deployment + return upload_url
  - "start_from_source":  Server-side build — Mode 2: kick off the build after source upload
  - "list":               List recent deployments (status, URL, sizes)

Two flows (pick ONE):

  FLOW A — local build (you build with @cloudflare/next-on-pages locally):
    1. Run `npx @cloudflare/next-on-pages` then zip the CONTENTS of `.vercel/output/static/`
       (cd .vercel/output/static && zip -r ../../../edge-ssr.zip .)
       On Windows use Git Bash or WSL; built-in zip tools use backslashes which break uploads.
    2. action: "create"   → { deployment_id, uploadUrl, expiresIn }
    3. PUT zip to uploadUrl with Content-Type: application/zip
    4. action: "start"    → polls; returns { url, status: "READY" }

  FLOW B — server-side build (Butterbase runs the build for you):
    1. action: "create_from_source"  → { deployment_id, upload_url, max_source_bytes }
    2. PUT source zip (≤50 MB) to upload_url with Content-Type: application/zip
    3. action: "start_from_source" with deployment_id + lockfile_hash (sha256 of package-lock.json)
       → { build_id, status, logs_url, status_url }
    4. Stream logs_url for live build output; poll status_url for terminal status

Parameters by action:
  create:             { app_id, action, framework? }
  start:              { app_id, action, deployment_id }
  create_from_source: { app_id, action, framework? }
  start_from_source:  { app_id, action, deployment_id, lockfile_hash, build_command?, output_dir?, package_manager?, user_env? }
  list:               { app_id, action, limit? }

framework: "nextjs-edge" (default) | "remix-edge" | "other-edge"

Status values: WAITING | UPLOADING | BUILDING | READY | ERROR | CANCELED | TIMEOUT
On TIMEOUT: deployment did not reach a terminal state within 60s. Use action: "list" to check
the current status, or call "start" again if it is still BUILDING.

Plan limits: Free = 1 deployment per app (replaces previous). Starter+ = unlimited.

Common errors:
  - INVALID_STATUS / UPLOAD_EXPIRED: zip not uploaded before "start"
  - STATE_PREREQUISITE_MISSING: source zip not uploaded before "start_from_source"
  - QUOTA_FILE_SIZE_EXCEEDED: source zip exceeds 50 MB
  - RESOURCE_NOT_FOUND: app or deployment doesn't exist
  - EXTERNAL_CLOUDFLARE_ERROR: Workers for Platforms not configured

Build caching (start_from_source):
  lockfile_hash is the node_modules cache key — same hash means cached node_modules (faster builds).
  Compute it with: sha256sum package-lock.json | cut -d' ' -f1

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `create`, `start`, `create_from_source`, `start_from_source`, `list`.
- `app_id` (`string`, required): The app ID
- `build_command` (`string`, optional): start_from_source. Default: "npx @cloudflare/next-on-pages".
- `deployment_id` (`string`, optional): Required for start and start_from_source.
- `framework` (`string`, optional): Framework for create / create_from_source. Default: nextjs-edge. One of: `nextjs-edge`, `remix-edge`, `other-edge`.
- `limit` (`number`, optional): Optional for list. Default: 20.
- `lockfile_hash` (`string`, optional): Required for start_from_source. sha256 of package-lock.json — node_modules cache key.
- `output_dir` (`string`, optional): start_from_source. Default: ".vercel/output/static".
- `package_manager` (`string`, optional): start_from_source. Default: npm. One of: `npm`, `pnpm`, `yarn`.
- `user_env` (`object`, optional): start_from_source. Build-time env vars.

Example input: `{"action":"create","app_id":"<app_id>"}`

### `butterbasemcp_manage_frontend`

Manage Frontend · Write

Manage frontend deployments, environment variables, and custom domains for a Butterbase app.

Actions:
  - "start_deployment":       Start a frontend deployment after uploading your zip file. Call after uploading zip to the URL returned by create_frontend_deployment. Polls until complete (up to 5 minutes).
  - "list_deployments":       List frontend deployment history for an app (read-only).
  - "create_from_source":     Create a source-based deployment and get a presigned upload URL (Mode 1). Upload your source zip to the URL via HTTP PUT with Content-Type: application/zip (max 50 MB).
  - "start_from_source":      Start the build for a source-based deployment (Mode 2). Requires deployment_id from create_from_source and a lockfile_hash.
  - "set_env":                Set environment variables for frontend builds (upserts).
  - "configure_custom_domain": Manage custom domains. Requires domain_action sub-option.

Parameters by action:
  start_deployment:        { app_id, action: "start_deployment", deployment_id }
  list_deployments:        { app_id, action: "list_deployments" }
  create_from_source:      { app_id, action: "create_from_source" }
  start_from_source:       { app_id, action: "start_from_source", deployment_id, lockfile_hash, build_command?, output_dir?, package_manager?, user_env? }
  set_env:                 { app_id, action: "set_env", vars }
  configure_custom_domain: { app_id, action: "configure_custom_domain", domain_action, hostname?, domain_id?, validation_method? }
    domain_action sub-options:
      "add":    { hostname, validation_method? } — Register a new custom domain. validation_method is "http" (default) or "txt"; use "txt" for any apex hostname on a Cloudflare-hosted zone.
      "list":   {} — List all custom domains for an app
      "status": { domain_id } — Check verification/SSL status of a domain
      "remove": { domain_id } — Remove a custom domain
      "verify": { domain_id } — Trigger re-verification of a pending domain

Common errors:
  - RESOURCE_NOT_FOUND: App or deployment doesn't exist
  - INVALID_STATUS: Deployment is not in WAITING status (zip may not have been uploaded yet)
  - UPLOAD_EXPIRED: The upload URL expired before the zip was uploaded
  - STATE_PREREQUISITE_MISSING: Source zip not yet uploaded (PUT to upload_url first)
  - QUOTA_FILE_SIZE_EXCEEDED: Source zip exceeds 50 MB
  - BUILD_FAILED: Build command exited with non-zero status (check logs_url for details)
  - VALIDATION_INVALID_SCHEMA: vars must be a non-empty object
  - feature_not_available: Free plan — upgrade to Pro (custom domains)
  - RESOURCE_ALREADY_EXISTS: Hostname already registered

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `start_deployment`, `list_deployments`, `create_from_source`, `start_from_source`, `set_env`, `configure_custom_domain`.
- `app_id` (`string`, required): The app ID
- `build_command` (`string`, optional): "start_from_source": build command to run after install (default: npm run build)
- `deployment_id` (`string`, optional): The deployment ID (required for "start_deployment" and "start_from_source")
- `domain_action` (`string`, optional): Required for "configure_custom_domain": the domain sub-action to perform. One of: `add`, `list`, `status`, `remove`, `verify`.
- `domain_id` (`string`, optional): Domain ID (required for domain_action "status", "remove", "verify")
- `hostname` (`string`, optional): Custom domain hostname (required for domain_action "add", e.g. app.example.com)
- `lockfile_hash` (`string`, optional): Required for "start_from_source": hex sha256 hash of the lockfile — used as the node_modules cache key
- `output_dir` (`string`, optional): "start_from_source": output directory containing built static files (default: dist)
- `package_manager` (`string`, optional): "start_from_source": package manager to use for install (default: npm). One of: `npm`, `pnpm`, `yarn`.
- `user_env` (`object`, optional): "start_from_source": environment variables to inject during the build (default: {})
- `validation_method` (`string`, optional): Optional for domain_action "add". SSL validation method: "http" (default — Cloudflare auto-validates via an HTTP challenge served from our zone; convenient but cannot work for apex domains whose DNS is on Cloudflare) or "txt" (Cloudflare emits a TXT record the customer adds to DNS; works in every case including apex on a Cloudflare-proxied zone). Use "txt" whenever the target hostname is an apex on a Cloudflare-hosted zone. One of: `http`, `txt`.
- `vars` (`object`, optional): Required for "set_env": environment variables as key-value pairs

Example input: `{"action":"start_deployment","app_id":"<app_id>"}`

### `butterbasemcp_manage_function`

Manage Function · Write

Manage function lifecycle: list, get source, delete, get logs, and update environment variables.

Actions:
  - "list":       List all deployed functions with status, metrics, and invocation URLs
  - "get":        Retrieve a single function's full detail including its deployed source code
  - "delete":     Delete a deployed function permanently (IRREVERSIBLE)
  - "get_logs":   Retrieve recent invocation logs for debugging and monitoring
  - "update_env": Update environment variables for a deployed function without redeploying code
  - "update_settings": Toggle per-function settings (currently: allow_service_key_impersonation)

Parameters by action:
  list:       { app_id, action: "list" }
  get:        { app_id, action: "get", function_name }
  delete:     { app_id, action: "delete", function_name }
  get_logs:   { app_id, action: "get_logs", function_name, limit?, since?, level?, include_deleted? }
  update_env: { app_id, action: "update_env", function_name, env }
  update_settings: { app_id, action: "update_settings", function_name, allow_service_key_impersonation? }

Common errors:
  - RESOURCE_NOT_FOUND: Function doesn't exist
  - VALIDATION_INVALID_SCHEMA: Invalid parameter format

Idempotency: Safe to call anytime (list is read-only; delete is idempotent; update_env is safe to call multiple times).

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `list`, `get`, `delete`, `get_logs`, `update_env`, `update_settings`.
- `app_id` (`string`, required): The app ID
- `allow_service_key_impersonation` (`boolean`, optional): Per-function gate (Phase 2). When false, the platform 403s any call carrying X-Butterbase-As-User at the edge — use for admin-only or billing-webhook handlers that must never accept an as-user assertion.
- `env` (`object`, optional): Environment variables to set (string) or delete (null)
- `function_name` (`string`, optional): The function name (required for get, delete, get_logs, update_env, update_settings)
- `include_deleted` (`boolean`, optional): Include logs for soft-deleted functions (post-incident forensics). Default: false.
- `level` (`string`, optional): Filter by log level (default: all). One of: `error`, `all`.
- `limit` (`number`, optional): Maximum number of logs to return (default: 100)
- `since` (`string`, optional): ISO timestamp to filter logs after this time

Example input: `{"action":"list","app_id":"<app_id>"}`

### `butterbasemcp_manage_integrations`

Manage Integrations · Write

Manage third-party integrations for a Butterbase app (e.g., Gmail, Slack, Google Calendar).

Actions:
  - "configure":          Enable or manage a third-party integration toolkit for an app
  - "rotate_credentials": Swap in new BYO OAuth client_id/client_secret without dropping connected accounts
  - "disable":            Disable a configured integration toolkit
  - "list_available":     List available integrations that can be enabled (curated or full catalog)
  - "list_connected":     List connected integration accounts for an app
  - "list_tools":         List available tool actions for connected integrations
  - "execute_action":     Execute a tool action on a connected integration (e.g., send email, create event)

"execute_read" is the organization-scoped read path used by Office and is not available on this app-scoped tool.

Parameters by action:
  configure:          { app_id, action: "configure", toolkit, scopes?, display_name?, oauth_credentials? }
  rotate_credentials: { app_id, action: "rotate_credentials", toolkit, oauth_credentials }
  disable:            { app_id, action: "disable", toolkit }
  list_available:     { app_id, action: "list_available", search? }
  list_connected:     { app_id, action: "list_connected" }
  list_tools:         { app_id, action: "list_tools", toolkit? }
  execute_action:     { app_id, action: "execute_action", tool_name (or tool_slug), params?, user_id? }

Curated toolkits (first-class support, no BYO credentials needed):
  gmail, google-calendar, slack, google-sheets, notion, github, hubspot, outlook, google-drive, discord

Non-curated toolkits (Twitter, LinkedIn, Reddit, etc.) usually require BYO OAuth credentials.
Use list_available with search=<name> first to inspect requires_byo_credentials and auth_schemes.

Example — configure (curated, managed auth):
  Input:  { app_id: "app_abc123", action: "configure", toolkit: "gmail", scopes: ["gmail.send"] }
  Output: { id: "...", toolkit_slug: "gmail", enabled: true }

Example — configure (BYO OAuth credentials, e.g. Twitter/X):
  Input:  {
    app_id: "app_abc123", action: "configure", toolkit: "twitter",
    scopes: ["tweet.read", "tweet.write", "users.read", "offline.access"],
    oauth_credentials: {
      client_id: "...",
      client_secret: "...",
      generic_id: "",  // toolkit-specific extra field
      auth_scheme: "OAUTH2"
    }
  }
  Output: { id: "...", toolkit_slug: "twitter", enabled: true }

Example — rotate_credentials (after upstream OAuth client rotation):
  Input:  {
    app_id: "app_abc123", action: "rotate_credentials", toolkit: "twitter",
    oauth_credentials: { client_id: "new...", client_secret: "new..." }
  }
  Output: { id: "...", toolkit_slug: "twitter", enabled: true }

Example — list_available:
  Input:  { app_id: "app_abc123", action: "list_available", search: "twitter" }
  Output: { integrations: [{ toolkit: "twitter", displayName: "Twitter", curated: false, auth_schemes: ["OAUTH2"], requires_byo_credentials: true }, ...] }

Example — list_connected:
  Input:  { app_id: "app_abc123", action: "list_connected" }
  Output: { connections: [{ toolkit_slug: "gmail", status: "active", connected_at: "..." }, ...] }

Example — list_tools:
  Input:  { app_id: "app_abc123", action: "list_tools", toolkit: "gmail" }
  Output: { tools: [{ name: "GMAIL_SEND_EMAIL", description: "Send an email", parameters: {...} }, ...] }

Example — execute_action (send email):
  Input:  { app_id: "app_abc123", action: "execute_action", tool_name: "GMAIL_SEND_EMAIL", params: { to: "user@example.com", subject: "Hello", body: "World" } }
  Output: { successful: true, data: { messageId: "..." } }

Common errors:
  - INTEGRATIONS_NOT_CONFIGURED: Integration API key not set
  - INTEGRATIONS_BYO_CREDENTIALS_REQUIRED: Toolkit has no Composio-managed auth; pass oauth_credentials
  - INTEGRATIONS_UPSTREAM_ERROR: Composio rejected the auth config (bad slug or bad credentials)
  - INTEGRATIONS_NOT_CONNECTED: User hasn't connected this integration
  - INTEGRATIONS_EXECUTION_FAILED: Integration tool execution failed
  - RESOURCE_NOT_FOUND: App doesn't exist

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `configure`, `rotate_credentials`, `disable`, `list_available`, `list_connected`, `list_tools`, `execute_action`.
- `app_id` (`string`, required): The app ID (e.g. app_abc123def456)
- `display_name` (`string`, optional): Custom display name (configure only)
- `oauth_credentials` (`object`, optional): BYO OAuth credentials for non-curated toolkits (configure / rotate_credentials). Required fields vary by toolkit — pass any extras the toolkit needs (e.g. twitter requires `generic_id` as the App Bearer Token). Omit for curated toolkits.
- `params` (`object`, optional): Arguments for the tool action (execute_action only)
- `scopes` (`array`, optional): OAuth scopes to request (configure only)
- `search` (`string`, optional): Search query to find integrations by name (list_available only)
- `tool_name` (`string`, optional): Integration tool slug (e.g. GMAIL_SEND_EMAIL). Required for execute_action unless tool_slug is given.
- `tool_slug` (`string`, optional): Alias for tool_name (the Office vocabulary). Accepted for execute_action.
- `toolkit` (`string`, optional): Integration toolkit slug (e.g. gmail, slack). Required for configure and disable.
- `user_id` (`string`, optional): Execute on behalf of a specific user, service-level only (execute_action only)

Example input: `{"action":"configure","app_id":"<app_id>"}`

### `butterbasemcp_manage_kv`

Manage KV · Write

Manage app KV store: config rules (expose/unexpose namespaces) and data-plane operations (get/set/del/incr etc).

Actions — Config:
  - "list_rules":  List all KV namespace exposure rules for the app
  - "expose":      Expose a key pattern with read/write role access control
  - "unexpose":    Remove an exposure rule by pattern
  - "stats":       Get KV usage stats (key count, memory, etc.)
  - "scan":        Scan keys by prefix (cursor-based, params: prefix, limit, cursor)
  - "flush":       Delete ALL keys in the KV store (requires confirm: true; include_config?: true also wipes rules)

Actions — Data plane:
  - "get":    Get the value of a key (opts: raw?, touch?)
  - "set":    Set a key to a value (opts: ttl?, ephemeral?, raw?)
  - "del":    Delete one key
  - "incr":   Increment a key's integer value (opts: by?)
  - "decr":   Decrement a key's integer value (opts: by?)
  - "setnx":  Set a key only if it does not already exist (opts: value, ttl?)
  - "setex":  Set a key with an explicit TTL in seconds (same as set + ttl)
  - "cas":    Compare-and-swap: atomically set next only if current value matches expected
  - "exists": Check if a key exists
  - "ttl":    Get remaining TTL of a key in seconds
  - "expire": Set a TTL on an existing key
  - "mget":   Get values of multiple keys at once (uses batch op)
  - "mset":   Set multiple key-value pairs at once (uses batch op; entries: {key: value})

Parameters by action:
  list_rules:  { app_id, action: "list_rules" }
  expose:      { app_id, action: "expose", pattern, read, write }
  unexpose:    { app_id, action: "unexpose", pattern }
  stats:       { app_id, action: "stats" }
  scan:        { app_id, action: "scan", prefix?, limit?, cursor? }
  flush:       { app_id, action: "flush", confirm: true, include_config? }
  get:         { app_id, action: "get", key, raw?, touch? }
  set:         { app_id, action: "set", key, value, ttl?, ephemeral?, raw? }
  del:         { app_id, action: "del", key }
  incr:        { app_id, action: "incr", key, by? }
  decr:        { app_id, action: "decr", key, by? }
  setnx:       { app_id, action: "setnx", key, value, ttl? }
  setex:       { app_id, action: "setex", key, value, ttl }
  cas:         { app_id, action: "cas", key, expected, next }
  exists:      { app_id, action: "exists", key }
  ttl:         { app_id, action: "ttl", key }
  expire:      { app_id, action: "expire", key, ttl }
  mget:        { app_id, action: "mget", keys }
  mset:        { app_id, action: "mset", entries }

Warning: "flush" deletes ALL keys and cannot be undone. Always pass confirm: true explicitly.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `list_rules`, `expose`, `unexpose`, `stats`, `scan`, `flush`, `get`, `set`, `del`, `incr`, `decr`, `setnx`, `setex`, `cas`, `exists`, `ttl`, `expire`, `mget`, `mset`.
- `app_id` (`string`, required): The app ID (e.g. app_abc123def456)
- `by` (`number`, optional): Amount to increment/decrement (default 1, for incr/decr)
- `confirm` (`boolean`, optional): Must be true to execute flush
- `cursor` (`string`, optional): Pagination cursor for scan (omit for first page)
- `entries` (`object`, optional): Object of key→value pairs for mset (values can be any JSON)
- `ephemeral` (`boolean`, optional): Store in ephemeral DB (shorter-lived, for set/setnx)
- `expected` (`string`, optional): The expected current value for CAS (compare-and-swap)
- `include_config` (`boolean`, optional): Also wipe exposure rules when flushing (use with flush)
- `key` (`string`, optional): The KV key for single-key data-plane actions
- `keys` (`array`, optional): Array of keys for mget
- `limit` (`integer`, optional): Max keys to return per scan page (max 1000)
- `next` (`string`, optional): The next value to set for CAS if expected matches
- `pattern` (`string`, optional): Key pattern for expose/unexpose (e.g. "user:*")
- `prefix` (`string`, optional): Key prefix for scan (e.g. "user:")
- `raw` (`boolean`, optional): Return the stored JSON text without decoding (get/mget only). Writes reject raw=true.
- `read` (`string`, optional): Read access role for expose. One of: `public`, `authed`, `owner`, `deny`.
- `touch` (`boolean`, optional): Reset TTL to original on read (for get)
- `ttl` (`number`, optional): TTL in seconds (null = no expiry; for set, setex, expire, setnx)
- `value` (`string`, optional): The value to set — can be any JSON (object, array, string, number)
- `write` (`string`, optional): Write access role for expose. One of: `public`, `authed`, `owner`, `deny`.

Example input: `{"action":"list_rules","app_id":"<app_id>"}`

### `butterbasemcp_manage_oauth`

Manage OAuth Providers · Write

Manage OAuth providers for end-user authentication (configure, get, update, delete).

Actions:
  - "configure": Set up a new OAuth provider (idempotent upsert)
  - "get":       Read provider config (single provider or all). client_secret is redacted.
  - "update":    Patch existing provider — only supplied fields change
  - "delete":    Remove a provider. Existing sessions remain valid until expiry.

Built-in providers (URLs/scopes auto-filled — only client_id, client_secret, redirect_uris required):
  google, github, discord, facebook, linkedin, microsoft, apple, x
For any other provider name, supply authorization_url, token_url, userinfo_url manually.

Parameters by action:
  configure: { app_id, action: "configure", provider, client_id, client_secret, redirect_uris, scopes?, authorization_url?, token_url?, userinfo_url?, provider_metadata? }
  get:       { app_id, action: "get", provider? }   // omit provider to list all
  update:    { app_id, action: "update", provider, ...fields-to-change }
  delete:    { app_id, action: "delete", provider }

Example — configure (Google):
  Input: { app_id: "app_abc123", action: "configure", provider: "google",
           client_id: "...", client_secret: "GOCSPX-...",
           redirect_uris: ["https://api.butterbase.ai/auth/app_abc123/oauth/google/callback"] }

Example — configure (Apple, requires provider_metadata):
  Input: { ..., provider: "apple", provider_metadata: { teamId, keyId, privateKey } }

Provider notes:
  - X (Twitter): no email — synthetic {username}@users.noreply.x.local is used
  - Apple: only returns name on first auth; uses POST callback (handled automatically); requires provider_metadata { teamId, keyId, privateKey }
  - Facebook: default scopes email, public_profile

OAuth flow after configure:
  GET {api_base}/auth/{app_id}/oauth/{provider}?redirect_to=https://yourapp.com/auth/callback
  After successful authentication, user is redirected to redirect_to with tokens as query params.

Common errors:
  - RESOURCE_NOT_FOUND: app or provider doesn't exist
  - VALIDATION_INVALID_SCHEMA: empty client_id/client_secret, or invalid URL on a custom provider

Idempotency: configure/update/delete are safe to retry.

Warning (delete): prevents future sign-ins via that provider; existing sessions remain valid until they expire.

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `configure`, `get`, `update`, `delete`.
- `app_id` (`string`, required): The app ID (e.g. app_abc123def456)
- `authorization_url` (`string`, optional): Required for custom providers on configure.
- `client_id` (`string`, optional): OAuth client ID. Required for configure.
- `client_secret` (`string`, optional): OAuth client secret. Required for configure (Apple: placeholder OK; real secret derived from provider_metadata).
- `provider` (`string`, optional): Provider name. Required for configure/update/delete. Optional for get (omit to list all).
- `provider_metadata` (`object`, optional): Provider-specific metadata. Required for Apple: { teamId, keyId, privateKey }.
- `redirect_uris` (`array`, optional): Required for configure. Format: ["{api_base}/auth/{app_id}/oauth/{provider}/callback"]
- `scopes` (`array`, optional): OAuth scopes. Optional for built-in providers (sensible defaults).
- `token_url` (`string`, optional): Required for custom providers on configure.
- `userinfo_url` (`string`, optional): Required for custom providers on configure.

Example input: `{"action":"configure","app_id":"<app_id>"}`

### `butterbasemcp_manage_organizations`

Manage organizations · Write

Manage Butterbase organizations: list, create, rename, delete, and members.

Actions:
  - "list":                List every organization the caller is a member of.
  - "get":                 Detail + members. { org_id }
  - "create":              Create a non-personal org. { name }
  - "rename":              Rename an org (owner only). { org_id, name }
  - "delete":              Delete a non-personal org (owner only). { org_id }
  - "list_members":        List members of an org. { org_id }
  - "remove_member":       Remove a member (owner only). { org_id, user_id }

Invitations aren't in this tool yet — use the dashboard web UI for now.

Common errors:
  - 401 unauthorized: caller not logged in
  - 403 not a member / not an owner
  - 404 org not found
  - 409 cannot delete personal org

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The organization operation to perform. One of: `list`, `get`, `create`, `rename`, `delete`, `list_members`, `remove_member`.
- `name` (`string`, optional): Organization name. Required for create/rename.
- `org_id` (`string`, optional): The organization id. Required for get/rename/delete/list_members/remove_member.
- `user_id` (`string`, optional): Target user id. Required for remove_member.

Example input: `{"action":"list"}`

### `butterbasemcp_manage_people`

Manage People · Write

Use the app's People integration: search person/company, fetch profiles, queue email lookups, manage credits and BYOK keys.

Actions:
  - search_person       { app_id, current_role_title?, past_role_title?, current_company_name?, current_company_industry?, country?, region?, city?, education_school_name?, education_degree_name?, education_field_of_study?, page_size?, next_token?, enrich_profiles? }
                         Structured-filter search for a person. Boolean syntax works inside any field, e.g.
                           current_role_title: '(VP OR "Vice President") AND NOT assistant'
                           education_school_name: '(Harvard OR Stanford OR MIT OR Princeton OR Yale)'
                         Returns enriched person data and credit usage.
  - search_company      { app_id, industry?, country?, employee_count_max?, page_size?, next_token?, enrich_profiles? }
                         Structured-filter search for a company. Returns enriched company data and credit usage.
  - get_profile         { app_id, linkedin_profile_url, live_fetch? }
                         Fetch a full profile from a LinkedIn URL. live_fetch: "force" for live (not cached).
                         Returns cached flag and credit usage.
  - queue_email_lookup  { app_id, linkedin_profile_url }
                         Queue an async email lookup job. Returns lookup_id to poll with get_email_lookup.
  - get_email_lookup    { app_id, id }
                         Poll the status of an email lookup. Returns status, email (when complete), credits used.
  // BYOK actions are disabled — the platform manages the People key. If
  // your MCP client previously called set_byok_key/clear_byok_key they will
  // now return an error.
  // - set_byok_key   (disabled)
  // - clear_byok_key (disabled)

This tool wraps the app's /v1/:app_id/people/* routes (search, profile, email lookup, BYOK).

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `search_person`, `search_company`, `get_profile`, `queue_email_lookup`, `get_email_lookup`, `set_byok_key`, `clear_byok_key`.
- `app_id` (`string`, required): The app ID
- `api_key` (`string`, optional): People API key (required for set_byok_key)
- `city` (`string`, optional): City filter (for search_person)
- `country` (`string`, optional): Country filter (for search_person and search_company)
- `current_company_industry` (`string`, optional): Current company industry filter (for search_person)
- `current_company_name` (`string`, optional): Current company name filter (for search_person)
- `current_role_title` (`string`, optional): Current role/job title filter (for search_person)
- `education_degree_name` (`string`, optional): Education degree filter (e.g. "MBA", "PhD") — for search_person
- `education_field_of_study` (`string`, optional): Education field of study filter (e.g. "Computer Science") — for search_person
- `education_school_name` (`string`, optional): Education school name filter, supports boolean (e.g. "(Harvard OR Stanford OR MIT)") — for search_person
- `employee_count_max` (`number`, optional): Maximum employee count filter (for search_company)
- `enrich_profiles` (`boolean`, optional): Whether to enrich results with full profile data (for search_person and search_company)
- `id` (`string`, optional): Lookup ID (required for get_email_lookup)
- `industry` (`string`, optional): Industry filter (for search_company)
- `linkedin_profile_url` (`string`, optional): LinkedIn profile URL (required for get_profile and queue_email_lookup)
- `live_fetch` (`string`, optional): For get_profile: "force" to skip cache and fetch live. One of: `force`.
- `next_token` (`string`, optional): Pagination token from a previous search response (for search_person and search_company)
- `page_size` (`number`, optional): Number of results per page (for search_person and search_company)
- `past_role_title` (`string`, optional): Past role/job title filter (for search_person)
- `region` (`string`, optional): Region/state filter (for search_person)

Example input: `{"action":"search_person","app_id":"<app_id>"}`

### `butterbasemcp_manage_rag_content`

Manage RAG Content · Write

Manage RAG (Retrieval-Augmented Generation) collections and documents. Collections are named
containers for documents that are chunked, embedded, and indexed for semantic search.

Actions:
  Collection actions:
  - "create_collection": Create a new collection
  - "list_collections":  List all collections in an app
  - "get_collection":    Get details for a specific collection (includes document counts by status)
  - "delete_collection": Permanently delete a collection and all its documents/embeddings

  Document actions:
  - "ingest_document":     Add a document (raw text or uploaded file) to be chunked, embedded, and indexed
  - "list_documents":      List all documents in a collection with their status
  - "get_document_status": Check the processing status of a specific document
  - "delete_document":     Permanently delete a document and its chunks/embeddings

Parameters by action:
  create_collection:   { app_id, action: "create_collection", name, description?, access_mode?, chunk_size?, chunk_overlap? }
  list_collections:    { app_id, action: "list_collections" }
  get_collection:      { app_id, action: "get_collection", name }
  delete_collection:   { app_id, action: "delete_collection", name }
  ingest_document:     { app_id, collection, action: "ingest_document", text?, storage_object_id?, filename?, metadata? }
  list_documents:      { app_id, collection, action: "list_documents" }
  get_document_status: { app_id, collection, action: "get_document_status", document_id }
  delete_document:     { app_id, collection, action: "delete_document", document_id }

access_mode options (create_collection):
  - "private" (default): Only the app owner can query
  - "shared": All authenticated users can query
  - "custom": Use RLS policies for fine-grained access

Ingestion modes for ingest_document (provide one):
  1. Raw text: provide "text" directly
  2. File-based: upload via manage_storage (action: "upload_url") first, then provide "storage_object_id"

Supported file types: PDF, TXT, Markdown, CSV, HTML, DOCX, XLSX, PPTX.

Document statuses: "pending" → "processing" → "ready" (or "failed")

Workflow: create_collection → ingest_document → poll get_document_status until "ready" → query with rag_query.

Warning: "delete_collection" permanently removes the collection, all documents, and embeddings. Cannot be undone.
Warning: "delete_document" permanently removes the document and its embeddings. To replace, delete then re-ingest.

Common errors:
  - RESOURCE_NOT_FOUND: App, collection, or document doesn't exist
  - VALIDATION_DUPLICATE_NAME: Collection name already exists (create_collection)
  - VALIDATION_ERROR: Neither text nor storage_object_id provided (ingest_document)

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `create_collection`, `list_collections`, `get_collection`, `delete_collection`, `ingest_document`, `list_documents`, `get_document_status`, `delete_document`.
- `app_id` (`string`, required): The app ID (e.g. app_abc123def456)
- `access_mode` (`string`, optional): Access control mode (create_collection only, default: private). One of: `private`, `shared`, `custom`.
- `chunk_overlap` (`number`, optional): Overlap tokens between chunks (create_collection only, default: 50)
- `chunk_size` (`number`, optional): Max tokens per chunk (create_collection only, default: 512)
- `collection` (`string`, optional): The collection name (required for document actions)
- `description` (`string`, optional): Human-readable description (create_collection only)
- `document_id` (`string`, optional): Document ID (required for get_document_status/delete_document)
- `filename` (`string`, optional): Filename hint for display and format detection (ingest_document only)
- `metadata` (`object`, optional): Key-value metadata to attach (ingest_document only, e.g. { source: "wiki" })
- `name` (`string`, optional): Collection name (required for create_collection/get_collection/delete_collection). Lowercase alphanumeric, hyphens, underscores only.
- `storage_object_id` (`string`, optional): UUID of an uploaded storage object from manage_storage action: "upload_url" (ingest_document only)
- `text` (`string`, optional): Raw text content to ingest (ingest_document only)

Example input: `{"action":"create_collection","app_id":"<app_id>"}`

### `butterbasemcp_manage_realtime`

Manage Realtime · Write

Manage realtime WebSocket notifications for database tables.

Actions:
  - "configure": Enable realtime broadcasts (INSERT/UPDATE/DELETE) on the given tables.
                 Idempotent — already-enabled tables are skipped.
  - "get":       Return current realtime config (which tables, active LISTEN connection, websocket URL).

Parameters by action:
  configure: { app_id, action: "configure", tables: [...] }
  get:       { app_id, action: "get" }

After configuring, clients connect via WebSocket:
  ws://api.butterbase.local/v1/{app_id}/realtime
  Client sends:  { "type": "subscribe", "table": "messages" }
  Server sends:  { "type": "change", "table": "messages", "op": "INSERT", "record": {...} }

RLS enforcement:
  - End-user JWT connections only receive changes they have permission to see
  - API key / service connections receive all changes (RLS bypassed)
  - Anonymous connections use butterbase_anon role policies

Prerequisites:
  - Tables must already exist (use manage_schema action: "apply" first)
  - For user-scoped data, enable RLS on the table first (manage_rls action: "enable" / "create_user_isolation")

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `configure`, `get`.
- `app_id` (`string`, required): The app ID
- `tables` (`array`, optional): Required for configure. Table names to enable realtime on.

Example input: `{"action":"configure","app_id":"<app_id>"}`

### `butterbasemcp_manage_repo`

Manage Repo · Write

Manage an app's repo (content-addressed code snapshots).

Actions:
  - "push":             Push a small set of files (≤1 MB total over MCP — for larger repos shell out to `butterbase repo push`). Files are { path, content_base64 } pairs. Server computes sha and runs prepare → upload → commit.
  - "pull_latest":      Fetch the latest snapshot's manifest (does not write files locally). Returns { snapshot_id, files: [{ path, sha256, size, downloadUrl }] } — agents fetch each downloadUrl directly.
  - "pull_snapshot":    Fetch a SPECIFIC snapshot's manifest by snapshot_id (does not write files locally). Returns the same shape as "pull_latest" but for that snapshot.
  - "status":           Returns { app_id, pinned_snapshot_id?, remote_latest_snapshot_id?, file_count }. No working-tree comparison (the server has no working tree).
  - "list_snapshots":   List snapshot history newest-first.
  - "wipe":             Delete every snapshot and blob, then null repo_latest_snapshot. Irreversible.

Parameters by action:
  push:           { action: "push", app_id, files: [{ path, content_base64 }], message? }
  pull_latest:    { action: "pull_latest", app_id }
  pull_snapshot:  { action: "pull_snapshot", app_id, snapshot_id }
  status:         { action: "status", app_id }
  list_snapshots: { action: "list_snapshots", app_id }
  wipe:           { action: "wipe", app_id }

Auth matrix: writes (push, wipe) require app owner. Reads (pull_latest, pull_snapshot, status, list_snapshots) work for owner or anonymously on a public app; private+non-owner gets 404.

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): No description. One of: `push`, `pull_latest`, `pull_snapshot`, `status`, `list_snapshots`, `wipe`.
- `app_id` (`string`, required): App ID — required for every action.
- `files` (`array`, optional): Required for "push".
- `message` (`string`, optional): Optional snapshot message for "push".
- `snapshot_id` (`string`, optional): REQUIRED for "pull_snapshot". The specific snapshot_id to fetch (as returned by push/pull_latest/list_snapshots).

Example input: `{"action":"push","app_id":"<app_id>"}`

### `butterbasemcp_manage_rls`

Manage RLS · Write

Manage Row-Level Security (RLS): enable on tables, create/update/delete policies, list, and one-shot user isolation setup.

Actions:
  - "enable":               Enable RLS on a table (foundation — no policies yet)
  - "create_policy":        Create a custom RLS policy with USING / WITH CHECK expressions
  - "update_policy":        Atomically update an existing policy (drops and re-creates in one tx)
  - "create_user_isolation": One-shot — enable RLS, create policy so users see only their rows, install auto-populate trigger
  - "list":                 List all RLS policies for the app (and tables_with_rls without policies)
  - "delete":               Delete one policy (if policy_name set) or ALL policies on the table (and disable RLS)

Parameters by action:
  enable:                { app_id, action: "enable", table_name }
  create_policy:         { app_id, action: "create_policy", table_name, policy_name, command?, role?, using_expression?, with_check_expression?, restrictive?, user_column? }
  update_policy:         { app_id, action: "update_policy", table_name, policy_name, command?, role?, using_expression?, with_check_expression?, restrictive? }
  create_user_isolation: { app_id, action: "create_user_isolation", table_name, user_column, public_read_column? }
  list:                  { app_id, action: "list" }
  delete:                { app_id, action: "delete", table_name, policy_name? }

Built-in roles (assigned automatically by the platform — you never create them):
  - butterbase_anon:    no auth header → "anon" in policies
  - butterbase_user:    valid end-user JWT → "user" in policies; current_user_id() returns user id
  - butterbase_service: platform API key → automatic full-access bypass; no policy needed

create_policy guidance:
  - command defaults to ALL. SELECT/DELETE: only using_expression. INSERT: only with_check_expression. UPDATE/ALL: both.
  - role: omit to apply to all roles, or set "anon" / "user" to scope and prevent cross-role policy leaks.
  - restrictive: true → policy is AND'd with permissive ones; useful for cross-table checks that must always hold.
  - user_column: pass to install a BEFORE INSERT trigger that auto-fills the column from current_user_id() —
    without it, clients must include the column in POST bodies or insert is rejected with AUTH_RLS_POLICY_VIOLATION.
  - For UUID columns, cast: current_user_id()::uuid

Cross-table subqueries pitfall (ANY policy, USING or WITH CHECK):
  EXISTS/IN/scalar subqueries inside a policy expression read the referenced table THROUGH that table's own RLS,
  under the SAME calling role. Symptom: policy evaluates false and the write fails AUTH_RLS_POLICY_VIOLATION
  (or SELECT returns empty), even for expressions that look trivially true.
  Three flavors:
    1. Referenced table has RLS enabled but NO permissive SELECT policy for role "user"
       → subquery sees 0 rows → EXISTS false. Even a hardcoded EXISTS(SELECT 1 FROM t WHERE id='<real id>') fails.
    2. Referenced table has user_isolation → subquery only sees the CURRENT user's rows, never other users' rows
       (blocks the "user B interacts with user A's public row" pattern).
    3. Referenced table's user_isolation gates by column X, but the subquery keys by column Y
       (e.g. users isolated on "id = current_user_id()" but subquery joins on "auth_user_id = current_user_id()",
        or vice versa). The row exists but isn't visible via the subquery's join column.
  Fix — pick one:
    - Add a permissive SELECT policy for role "user" on the referenced table for the rows the subquery needs
      (create_policy with command: "SELECT", role: "user", using_expression: <predicate>).
    - Or use create_user_isolation with public_read_column to expose specific rows to everyone.
    - Or align the subquery join column with the referenced table's isolation column.

create_user_isolation does:
  1. Enables RLS on the table
  2. User isolation policy (rows where user_column = current_user_id())
  3. Auto-populate trigger for user_column on INSERT
  4. Auto service bypass policy
  5. If public_read_column set: extra SELECT policies for butterbase_user + butterbase_anon
     allowing reads where that boolean column is true ("own rows + public read" pattern in one call)

delete behavior:
  - With policy_name: removes that single policy (RLS stays enabled)
  - Without policy_name: removes ALL policies AND disables RLS — table becomes globally accessible

Common errors:
  - VALIDATION_TABLE_NOT_FOUND: create the table with manage_schema (action: "apply") first
  - VALIDATION_COLUMN_NOT_FOUND: user_column missing from the table
  - VALIDATION_INVALID_TYPE: user_column must be UUID or TEXT
  - RLS_TYPE_MISMATCH: cast types in expressions, e.g. current_user_id()::uuid
  - RLS_INVALID_EXPRESSION: SQL syntax error
  - RESOURCE_NOT_FOUND: policy doesn't exist (update_policy) — use create_policy first

Idempotency: enable, create_policy, update_policy, create_user_isolation, delete — all safe to retry.

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `enable`, `create_policy`, `update_policy`, `create_user_isolation`, `list`, `delete`.
- `app_id` (`string`, required): The app ID
- `command` (`string`, optional): create_policy/update_policy. Default: ALL. One of: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `ALL`.
- `policy_name` (`string`, optional): Required for create_policy/update_policy. Optional for delete (omit to remove ALL policies). Alphanumeric + underscores.
- `public_read_column` (`string`, optional): create_user_isolation only. Boolean column — adds SELECT policies for user/anon allowing reads where this is true.
- `restrictive` (`boolean`, optional): create_policy/update_policy. If true, AS RESTRICTIVE policy (AND with permissive).
- `role` (`string`, optional): create_policy/update_policy. Scope to butterbase_anon or butterbase_user. Recommended. One of: `anon`, `user`.
- `table_name` (`string`, optional): Required for enable/create_policy/update_policy/create_user_isolation/delete.
- `user_column` (`string`, optional): create_policy: install BEFORE INSERT auto-populate trigger. create_user_isolation: required, the user-id column.
- `using_expression` (`string`, optional): create_policy/update_policy. SQL for USING clause. Required for SELECT/DELETE/ALL/UPDATE.
- `with_check_expression` (`string`, optional): create_policy/update_policy. SQL for WITH CHECK clause. Required for INSERT.

Example input: `{"action":"enable","app_id":"<app_id>"}`

### `butterbasemcp_manage_schema`

Manage Schema · Write

Manage the database schema: read current schema, apply changes, preview changes, and audit migration history.

Actions:
  - "get":             Get the current schema (tables, columns, indexes) and api_base
  - "apply":           Apply a declarative schema. Diffs against current and runs the safe DDL.
  - "dry_run":         Preview the SQL that "apply" would run, without executing
  - "list_migrations": List applied migrations (most recent first)

Parameters by action:
  get:             { app_id, action: "get" }
  apply:           { app_id, action: "apply",   schema, name? }
  dry_run:         { app_id, action: "dry_run", schema }
  list_migrations: { app_id, action: "list_migrations" }

Schema example:
  {
    tables: {
      posts: {
        columns: {
          id: { type: "uuid", primaryKey: true, default: "gen_random_uuid()" },
          title: { type: "text", nullable: false },
          author_id: { type: "uuid", references: { table: "users", column: "id", onDelete: "CASCADE" } },
          created_at: { type: "timestamptz", default: "now()" }
        }
      }
    }
  }

Idempotency: "apply" is safe to call multiple times. If the schema is already up-to-date, returns "Schema is up to date".

Destructive operations: Require explicit opt-in via the _drop (table-level) or _dropColumns (column-level) fields.

Common errors:
  - VALIDATION_INVALID_SCHEMA: schema format does not match the DSL
  - STATE_PREREQUISITE_MISSING: add _drop / _dropColumns to authorize destructive ops
  - QUOTA_TABLE_LIMIT: max 50 tables per app
  - RESOURCE_NOT_FOUND: app_id does not exist

Read only: no. Destructive: no. Idempotent (safe to retry): yes.

Inputs:

- `action` (`string`, required): The action to perform. One of: `get`, `apply`, `dry_run`, `list_migrations`.
- `app_id` (`string`, required): The app ID
- `name` (`string`, optional): Optional for apply. Migration name (auto-generated if omitted).
- `schema` (`object`, optional): Required for apply/dry_run. The desired database schema.

Example input: `{"action":"get","app_id":"<app_id>"}`

### `butterbasemcp_manage_storage`

Manage Storage · Write

Manage app storage: presigned upload/download URLs, list/delete objects, update config.

Actions:
  - "upload_url":    Get a presigned PUT URL to upload a file (expires in 15 min)
  - "download_url":  Get a presigned GET URL for a stored file (expires in 1 hour)
  - "list":          List all objects in app storage with metadata
  - "delete":        Permanently delete an object from S3 + database
  - "update_config": Update storage config (publicReadEnabled, per-app storage cap, per-file size cap)

Parameters by action:
  upload_url:    { app_id, action: "upload_url", filename, content_type, size_bytes, public? }
  download_url:  { app_id, action: "download_url", object_id }
  list:          { app_id, action: "list" }
  delete:        { app_id, action: "delete", object_id }
  update_config: { app_id, action: "update_config", publicReadEnabled?, storageLimitBytes?, maxFileSizeMb? }

object_id is the UUID returned from upload or list. Do NOT pass the s3_key / bucket path
(e.g. app_id/user_id/uuid_file.jpg) — that is metadata only and is not a usable URL.

Upload workflow:
  1. action: "upload_url"  → returns { upload_url, object_id, expires_at }
  2. PUT the file to upload_url with the matching Content-Type header
  3. Persist object_id (e.g. users.avatar_id)
  4. Later: action: "download_url" with that object_id

Set public: true on upload_url to make the file downloadable by any authenticated user
(e.g. post images, avatars). Files are private by default.

publicReadEnabled (update_config):
  - true:  any authenticated user can download any file (uploads/deletes still user-scoped)
  - false (default): users can only download their own files; platform auth (API key) can still access any file

storageLimitBytes (update_config):
  - Per-app total storage cap in bytes. Defaults to 1 GB (1073741824) when unset.
  - Must be a positive integer and cannot exceed the org's plan cap
    (rejected with VALIDATION_INVALID_SCHEMA if it does). To raise it above the
    plan cap, upgrade the plan via manage_billing.

maxFileSizeMb (update_config):
  - Per-file size cap in megabytes. Defaults to 10 MB when unset.
  - Raise this to accept larger uploads (e.g. video/audio). Must be a positive integer.

At least one of publicReadEnabled, storageLimitBytes, or maxFileSizeMb must be supplied.

Limits & errors:
  - Files: default max 10 MB each; raise via maxFileSizeMb (QUOTA_FILE_SIZE_EXCEEDED)
  - QUOTA_STORAGE_EXCEEDED: delete unused files or upgrade plan
  - RESOURCE_NOT_FOUND: app or object doesn't exist (verify object_id, not s3_key)
  - delete is idempotent (no-op if already deleted); upload/download URL generation is not (new URL each call)

Warning: "delete" cannot be undone. Update DB references (e.g. users.avatar_id) first.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `upload_url`, `download_url`, `list`, `delete`, `update_config`.
- `app_id` (`string`, required): The app ID (e.g. app_abc123def456)
- `content_type` (`string`, optional): Required for upload_url. MIME type (e.g. "image/png", "application/pdf").
- `filename` (`string`, optional): Required for upload_url. The filename for the upload.
- `maxFileSizeMb` (`integer`, optional): Optional for update_config. Per-file size cap in megabytes. Default 10 MB.
- `object_id` (`string`, optional): Required for download_url and delete. Storage object UUID — not the s3_key path.
- `public` (`boolean`, optional): Optional for upload_url. Mark file as publicly downloadable. Default: false.
- `publicReadEnabled` (`boolean`, optional): Optional for update_config. Enable/disable app-wide public read.
- `size_bytes` (`integer`, optional): Required for upload_url. File size in bytes.
- `storageLimitBytes` (`integer`, optional): Optional for update_config. Per-app total storage cap in bytes. Cannot exceed the plan cap.

Example input: `{"action":"upload_url","app_id":"<app_id>"}`

### `butterbasemcp_manage_substrate`

Manage substrate · Write

Manage the entire Butterbase substrate: the action ledger, the entity graph,
source artifacts, institutional memory (decisions/commitments/learnings), outbox,
attention rules, snapshots, and settings.

Every substrate write goes through the action ledger via "propose". The ledger
captures the proposer, the policy verdict, and the result; nothing is written
to the substrate without an action_id you can trace.

Actions — Writes (action ledger):
  - "propose":          Propose a write. capability ∈ {upsert_entity, update_entity, patch_entity, record_decision, record_commitment, record_learning, upsert_source_artifact, merge_entities, delete_entity, bulk_revert_actions, record_principle, retire_principle, supersede_decision, send_email_draft, revert_action}. Body: { capability, payload, dangerously_skip_approval?, idempotency_key? }. A replay key binds the normalized capability, payload and trusted app/rule scope within the organization, without a TTL. Matched replays return the prior verdict, result and actual status with replay: true. Reusing the key for different work, or replaying an ambiguous historical key, returns 409 idempotency_intent_conflict; different work requires a fresh key and authorization.
  - "approve":          Approve a previously-proposed action that is awaiting review. { action_id }.
  - "reject":           Reject a previously-proposed action. { action_id, reason }.

Actions — Ledger reads:
  - "list_actions":     List action ledger rows. Optional: { status?, capability?, source_app_id?, source_rule_id?, limit?, before?, include_noops? }. Status: proposed, approved, rejected, executing, executed, failed, reverted; pending aliases proposed. status=approved means "a human approved this row" (approved_at is set), regardless of its current terminal state. Unsupported statuses are rejected. No-op rows (result.changed=false) are hidden unless include_noops is true.
  - "get_action":       Get one ledger row by id. { action_id }.
  - "list_removals":    List hard entity removals, newest first. Optional: { before?, limit? }. action_id is the ledger row for capability deletes and null for deletes outside the capability path.

Actions — Entity graph:
  - "find_entities":    List/fuzzy-search entities. Optional: { type?, q?, primary_email?, limit? (max 1000), cursor?, include_deleted? }. type ∈ person|company|fund|workspace|team|project|event|agent|self. Soft-deleted entities are excluded unless include_deleted is true. Returns { entities, next_cursor? }.
  - "get_entity":       Get one entity by id. { entity_id }.
  - (writes use "propose" with capability=upsert_entity / update_entity / patch_entity / merge_entities / delete_entity)

Actions — Source artifacts (meeting transcripts, email threads, call recordings, etc.):
  - "list_source_artifacts": List/search. Optional: { kind?, q?, limit?, count? }. q runs FTS over title+summary+content.
  - "get_source_artifact":   Get one artifact by id (incl. full content). { artifact_id }.
  - (writes use "propose" with capability=upsert_source_artifact)

Actions — Institutional memory:
  - "search_memory":    FTS across decisions, commitments, learnings, and source_artifacts. { q?, kinds?, limit? }. q is optional — omit to list recent items. kinds = comma-separated subset of {decisions,commitments,learnings,source_artifacts}.
  - "list_memory":      Browse memory chronologically (no FTS). Filter by source_artifact_id, kinds, superseded. { kinds?, source_artifact_id?, superseded?, before?, limit? }.
  - "list_agent_memories": Browse compact curated memory summaries. { cursor?, limit? }.
  - "search_agent_memories": Search only curated agent memories. { q, cursor?, limit? }.
  - "get_agent_memory": Read one complete curated memory and its evidence. { memory_id }.
  - Curated memory writes use "propose" with record_agent_memory, amend_agent_memory, or archive_agent_memory. Amend/archive require memory_id and expected_revision in payload. These records never change policy.
  - (writes use "propose" with capability=record_decision / record_commitment / record_learning / record_principle / supersede_decision / retire_principle)
  - record_decision with kind="policy_decision" and applies_to {"operator_sweep":"archive"} stops timer sweeps; event wakes and user turns still run. Supersede the decision to resume.

Actions — Outbox (side-effect dispatch queue):
  - "list_outbox":      List rows. Optional: { status?, limit? }. status ∈ pending|in_flight|succeeded|failed|dead_letter|cancelled.
  - "retry_outbox":     Manually re-queue a failed/dead-letter row. { outbox_id }.
  - "cancel_outbox":    Mark a not-yet-completed row as cancelled. { outbox_id }.

Actions — Attention rules (cron-triggered automation):
  - "list_rules":       List all attention rules. Optional: { enabled? }.
  - "get_rule":         Get one rule by id. { rule_id }.
  - "create_rule":      Create a new rule. { rule: {name, description?, trigger_cron, condition_mode, condition, action_capability, action_payload_template, enabled?, max_fires_per_day?} }.
  - "update_rule":      Full replace by id. { rule_id, rule }.
  - "delete_rule":      Delete by id. { rule_id }.
  - "enable_rule":      Set enabled=true. { rule_id }.
  - "disable_rule":     Set enabled=false. { rule_id }.
  - "list_rule_firings": History for one rule. { rule_id, status?, limit?, before? }.

Actions — Snapshots & settings:
  - "snapshots":        Daily aggregate snapshots. Optional: { days? }.
  - "get_settings":     Show substrate settings (e.g. yolo_mode).
  - "set_yolo":         Toggle YOLO mode. { yolo_mode: boolean }.

Actions — Policy (read the rules that govern you BEFORE proposing):
  - "list_capabilities":       The guardrail matrix — every capability's default_policy,
                               yolo_eligible, reversible. Read this to know what will gate.
  - "list_principles":         Active principles with their applies_to scope and
                               constraint_spec. Optional: { status?: 'active'|'expired'|'all',
                               limit?, before? }. Defaults to active.
  - "get_principle":           One principle + how many conflicts it has caused.
                               { principle_id }.
  - "list_policy_conflicts":   Conflicts your proposals hit. Optional:
                               { resolved?: 'true'|'false', principle_id?, limit?, before? }.
  - "get_policy_conflict":     One conflict with the principle and blocked action inlined.
                               { conflict_id }.
  - "resolve_policy_conflict": Record how a human disposed of a conflict.
                               { conflict_id, resolution, reason? } where resolution is one of
                               overridden | amended_principle | retired_principle |
                               confirmed_principle | cancelled_action.
                               NOTE: 'cancelled_action' also rejects the blocked action.
                               'overridden' does NOT approve it — call approve explicitly.

Capability default policies (what happens when you "propose" each capability):
  record_decision        → auto             (reversible)
  record_commitment      → auto             (reversible)
  record_learning        → auto             (reversible)
  upsert_entity          → auto             (reversible)
  update_entity          → auto             (reversible)
  patch_entity           → auto             (reversible)
  upsert_source_artifact → auto             (reversible)
  revert_action          → auto             (not reversible)
  record_principle       → approval_required (not reversible) ← mutates policy layer
  amend_principle        → approval_required (not reversible) ← mutates policy layer
  supersede_decision     → approval_required (not reversible) ← mutates policy layer
  retire_principle       → approval_required (not reversible) ← mutates policy layer
  delete_entity          → approval_required (not reversible)
  merge_entities         → approval_required (not reversible)
  bulk_revert_actions    → approval_required (not reversible)
  send_email_draft       → approval_required (not reversible; yolo_eligible but agent proposals always gate)

Note: record_decision auto-approves, but record_principle / amend_principle / supersede_decision /
retire_principle always require human approval — they mutate the policy-enforcement layer of the
substrate. They are also yolo_eligible=false, so neither yolo_mode nor dangerously_skip_approval
can auto-approve them. You cannot rewrite the rules that gate you.

To CHANGE a principle, propose one of:
  record_principle  { title, applies_to, constraint_spec, rationale?, salience? }  — create
  amend_principle   { principle_id, reason, title?/rationale?/applies_to?/constraint_spec?/salience? }
                    — edit in place, keeps the id stable so existing conflict rows still resolve
  retire_principle  { principle_id, reason }  — set status=expired (not a delete)

Use "list_principles" to read constraints BEFORE proposing, rather than discovering them by
being blocked.

Common returns: the raw substrate API JSON response. The agent has full CRUD over the
substrate via this single tool — there is no other substrate tool to call.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The substrate operation to perform. One of: `provision`, `propose`, `approve`, `reject`, `list_actions`, `get_action`, `list_removals`, `find_entities`, `get_entity`, `list_source_artifacts`, `get_source_artifact`, `search_memory`, `list_memory`, `list_agent_memories`, `search_agent_memories`, `get_agent_memory`, `list_outbox`, `retry_outbox`, `cancel_outbox`, `list_rules`, `get_rule`, `create_rule`, `update_rule`, `delete_rule`, `enable_rule`, `disable_rule`, `list_rule_firings`, `snapshots`, `get_settings`, `set_yolo`, `list_capabilities`, `list_principles`, `get_principle`, `list_policy_conflicts`, `get_policy_conflict`, `resolve_policy_conflict`.
- `action_id` (`string`, optional): Ledger action id (act_…) — for approve/reject/get_action.
- `artifact_id` (`string`, optional): Source artifact id (art_…) — for get_source_artifact.
- `before` (`string`, optional): ISO-8601 timestamp cursor — list_actions/list_rule_firings/list_memory.
- `capability` (`string`, optional): Capability name — required for "propose".
- `conflict_id` (`string`, optional): Policy conflict id (pcf_…) — for get_policy_conflict/resolve_policy_conflict.
- `count` (`boolean`, optional): Include precise total — list_source_artifacts.
- `cursor` (`string`, optional): Opaque cursor from a previous find_entities response.next_cursor.
- `dangerously_skip_approval` (`boolean`, optional): Owner-only YOLO override for propose. Principle conflicts still block.
- `days` (`integer`, optional): Number of days — snapshots.
- `display_name` (`string`, optional): Required for "provision" — display name for the substrate self-entity.
- `enabled` (`boolean`, optional): Filter rules by enabled — list_rules.
- `entity_id` (`string`, optional): Entity id (ent_…) — for get_entity.
- `idempotency_key` (`string`, optional): Optional client key to dedupe retries on propose.
- `include_deleted` (`boolean`, optional): Include soft-deleted entities — find_entities. Default false.
- `include_noops` (`boolean`, optional): Include ledger rows whose result.changed is false — list_actions. Default false.
- `kind` (`string`, optional): Kind filter — list_source_artifacts.
- `kinds` (`string`, optional): Comma-separated memory kinds — search_memory/list_memory.
- `limit` (`integer`, optional): Max rows (up to 1000 for find_entities).
- `match` (`string`, optional): Tsquery mode: 'and' (default — every word required), 'or' (any word), 'phrase' (adjacent). One of: `and`, `or`, `phrase`.
- `memory_id` (`string`, optional): Curated agent memory id — for get_agent_memory.
- `org_id` (`string`, optional): Optional target organization id (UUID). Sent as `x-organization-id` header. Use to operate on a shared org's substrate. Requires the caller's API key to be bound to the same org (mint one via manage_auth_config generate_service_key + organization_id). For JWT auth (dashboard), the auth layer validates membership against organization_members.
- `outbox_id` (`string`, optional): Outbox row id (obx_…) — for retry_outbox/cancel_outbox.
- `payload` (`object`, optional): Capability payload — required for "propose".
- `primary_email` (`string`, optional): Exact email filter (case-insensitive) — find_entities.
- `principle_id` (`string`, optional): Principle id (dec_…) — for get_principle / list_policy_conflicts filter.
- `q` (`string`, optional): Search query — find_entities (fuzzy), list_source_artifacts (FTS), search_memory.
- `reason` (`string`, optional): Required reason for "reject". Appended to the audit trail.
- `resolution` (`string`, optional): Required for "resolve_policy_conflict". How the human disposed of the conflict. One of: `overridden`, `amended_principle`, `retired_principle`, `confirmed_principle`, `cancelled_action`.
- `resolved` (`string`, optional): Filter conflicts by resolved state — list_policy_conflicts. One of: `true`, `false`.
- `rule` (`object`, optional): Rule body — create_rule/update_rule.
- `rule_id` (`string`, optional): Attention rule id (rule_…) — for *_rule actions.
- `source_app_id` (`string`, optional): Filter ledger by source app — list_actions.
- `source_artifact_id` (`string`, optional): Restrict memory rows / artifacts to those linked to this source_artifact_id — list_memory.
- `source_rule_id` (`string`, optional): Filter ledger by source rule — list_actions.
- `status` (`string`, optional): Status filter: list_actions accepts proposed, approved, rejected, executing, executed, failed, reverted; pending aliases proposed. Other actions use their own states (outbox pending is unchanged).
- `superseded` (`string`, optional): list_memory only: include superseded rows (default: include all). One of: `true`, `false`.
- `type` (`string`, optional): Entity type filter — find_entities.
- `yolo_mode` (`boolean`, optional): Required for "set_yolo".

Example input: `{"action":"provision"}`

### `butterbasemcp_prep_and_submit_hackathon_entry`

Prep and Submit Hackathon Entry · Write

Prep and submit your project to a Butterbase hackathon. Two-step flow.

The tool resolves which hackathon you mean from your submission_code (or, if
you've already submitted before, your existing binding). You do NOT pass a
slug — that's figured out for you. If resolution is ambiguous (no code, not
yet bound, and multiple hackathons are open), the tool returns the list of
open hackathons; ask the user which one they mean and re-run with a code.

  STEP 1 — action: "prep"
    Resolves the hackathon and returns its field_schema if exactly one is
    identified. Pass submission_code when you have it. Otherwise the tool
    will fall back to "user already bound" or "only one hackathon open".
    Use the schema to:
      • Show the user every field's "label" and "description" (never the internal "key").
      • Propose a value for each field and wait for the user's explicit
        confirmation before STEP 2.
    Do NOT auto-fill values from guesses, prior context, or app metadata without
    showing the user every field value first.

    If resolution returns multiple open hackathons with no match, present
    "open_hackathons" to the user and ask them to provide the submission_code.

  STEP 2 — action: "submit"
    Submits the confirmed "data" object. Pass hackathon_slug = matched.slug from
    the prep response so submit always targets the same hackathon prep resolved
    (matters when multiple are open). Re-running updates the existing submission
    and bumps version. Closes after the hackathon's submission_deadline.

App scoring:
  Always pass app_id on submit when you can. Hackathon scoring awards up to 50
  points for a demo URL on butterbase.dev and up to 50 additional points for
  Butterbase features measured on that specific app (database, functions,
  deployed frontend, auth users, storage, OAuth, realtime, integrations, etc.).
  Without app_id only the demo URL is scored, so entries without it almost
  always rank lower. Including app_id also ties the submission to a real app,
  which is much better for human judges.

Submission code:
  On the FIRST submission you must include the submission_code provided by
  the hackathon organizers. The same code is used to identify the hackathon
  during prep, so pass it on prep too. After the first successful submission
  the code is no longer required (the user is bound by user_id).

Recommended flow:
  1. Get the submission_code from the user (skip if they've already submitted before).
  2. Call with action: "prep", submission_code to resolve + retrieve the schema.
     • If matched is null and open_hackathons has multiple entries, ask the user
       which one and re-run with a code.
  3. Show the user each field's label / description, propose values, and wait
     for confirmation.
  4. Call with action: "submit", hackathon_slug = matched.slug from prep,
     data: {...confirmed values}, app_id, and submission_code (if provided in
     step 1).

Returns:
  prep   → { matched: { slug, name, submission_deadline, ends_at, field_schema } | null,
             match_reason, open_hackathons,
             next_call?: { tool, arguments, instructions } }
           When matched is non-null, next_call contains a fully-formed example
           submit invocation with placeholders for each field. Use it as the
           literal shape for STEP 2: replace each placeholder in arguments.data
           with the user-confirmed value, then call this tool with those args.
  submit → { submission: { id, hackathon_slug, version, data, app_id, ... }, participant_created }

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): "prep" resolves the hackathon and returns its schema. "submit" sends the confirmed data. One of: `prep`, `submit`.
- `app_id` (`string`, optional): Butterbase app id for the project being submitted (e.g. app_abc123). Strongly recommended on submit: scoring awards up to 50 extra points for Butterbase usage on this app. Ignored on prep.
- `data` (`string`, optional): Submission fields per the hackathon field_schema, as an object keyed by field "key". Required on submit; ignored on prep. Example: {"project_name":"My App","demo_url":"https://my-app.butterbase.app","description":"What it does"}. A JSON-encoded string is also accepted and will be parsed.
- `hackathon_slug` (`string`, optional): Slug of the hackathon. Optional. Pass on submit using the slug returned by prep (matched.slug) so submit targets the exact hackathon prep resolved. Ignored on prep.
- `submission_code` (`string`, optional): Per-hackathon code from the organizer. Used to identify which open hackathon the user means and to bind them on first submission. Required for prep when multiple hackathons are open and the user is not yet bound. Required on the FIRST submission. Ignored on submit after the user is already a participant.

Example input: `{"action":"prep"}`

### `butterbasemcp_seed_database`

Seed Database · Write

Insert multiple rows into a table in a single call. Useful for seeding sample data,
bootstrapping test fixtures, or populating lookup tables.

IMPORTANT: This tool authenticates with the platform API key (butterbase_service role),
which bypasses Row-Level Security. Inserts via this tool are not subject to RLS policies.

Rows are inserted sequentially. If a row fails (e.g., duplicate key, constraint violation),
the tool skips it and continues with the remaining rows. The response reports how many
rows were inserted vs failed, with error details for each failure.

Example:
  Input: {
    app_id: "app_abc123",
    table: "products",
    rows: [
      { "name": "Widget", "price": 999, "category": "tools" },
      { "name": "Gadget", "price": 1499, "category": "electronics" },
      { "name": "Doohickey", "price": 299, "category": "tools" }
    ]
  }
  Output: {
    inserted: 3,
    failed: 0,
    errors: [],
    rows: [ { id: "uuid-1", ... }, { id: "uuid-2", ... }, { id: "uuid-3", ... } ]
  }

Notes:
  - Columns with defaults (like id, created_at) can be omitted
  - Each row is an independent insert — failures don't roll back other rows
  - Maximum 100 rows per call

Common errors (per row):
  - VALIDATION_UNIQUE_CONSTRAINT_VIOLATION: Duplicate value in unique column
  - VALIDATION_FOREIGN_KEY_VIOLATION: Referenced record doesn't exist
  - VALIDATION_NOT_NULL_VIOLATION: Required field is missing

Idempotency: Not idempotent — creates new rows each time.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `app_id` (`string`, required): The app ID
- `rows` (`array`, required): Array of row objects to insert (max 100)
- `table` (`string`, required): The table name

Example input: `{"app_id":"<app_id>","rows":[],"table":"<table>"}`

### `butterbasemcp_submit_suggestion`

Submit Suggestion · Write

Submit feedback, bug reports, or feature suggestions to the Butterbase platform team.

Use this tool when you encounter issues with Butterbase tools, want to suggest improvements,
or when a user asks you to report something to the Butterbase team.

Categories:
  - bug_report: Something isn't working as expected or documented.
      Example: "apply_schema fails silently when adding an enum column with a default value"
  - feature_request: A capability that doesn't exist yet but would be useful.
      Example: "Support for composite unique constraints across multiple columns"
  - improvement: An existing feature works but could be better.
      Example: "get_schema should include index definitions in its output"
  - documentation: The docs are missing, unclear, or incorrect.
      Example: "The deploy_function tool description doesn't mention the 50MB size limit"

Source:
  - agent: You (the AI agent) are reporting this on your own initiative
  - human_prompted: The human user asked you to report this

Returns: The created suggestion with a unique ID and status.

Example output:
  {
    suggestion: {
      id: "a1b2c3d4-...",
      category: "bug_report",
      severity: "medium",
      description: "apply_schema returns success but...",
      affected_tool: "apply_schema",
      status: "new",
      created_at: "2026-04-05T10:00:00Z"
    }
  }

Recent tool calls are automatically captured as context — you don't need to
manually describe what you called. Just describe the issue or suggestion clearly.

Idempotency: Each call creates a new suggestion. Avoid submitting duplicates.

Read only: no. Destructive: no. Idempotent (safe to retry): no.

Inputs:

- `category` (`string`, required): Type of feedback. One of: `bug_report`, `feature_request`, `improvement`, `documentation`.
- `description` (`string`, required): Clear description of the issue, suggestion, or request. Be specific.
- `affected_tool` (`string`, optional): Name of the Butterbase tool affected (e.g. apply_schema, deploy_function)
- `agent_context` (`object`, optional): Any additional structured context you want to attach (error messages, parameters tried, etc.)
- `app_id` (`string`, optional): The app ID if this suggestion relates to a specific app
- `proposed_solution` (`string`, optional): Your suggested fix or approach, if you have one
- `severity` (`string`, optional): How impactful is this? critical = blocks work, high = significant pain, medium = annoying, low = minor. One of: `low`, `medium`, `high`, `critical`.
- `source` (`string`, optional): Whether you are reporting this on your own or at the user's request (default: agent). One of: `agent`, `human_prompted`.

Example input: `{"category":"bug_report","description":"<description>"}`

### `butterbasemcp_manage_agents`

Manage Agents · Destructive

Manage agents for a Butterbase app: list, get, create, update, delete, and validate specs.

Actions:
  - "list":     List all agents defined for an app.
  - "get":      Get a single agent by name (full record incl. graph_spec).
  - "create":   Create a new agent. Requires name + graph_spec.
  - "update":   Patch an existing agent (partial update). Requires name.
  - "delete":   Permanently delete an agent and all its runs. Requires name.
  - "validate": Validate a graph_spec without creating an agent. Requires graph_spec.

Parameters by action:
  list:     { action: "list", app_id }
  get:      { action: "get", app_id, name }
  create:   { action: "create", app_id, name, graph_spec, display_name?, description?, default_model?, ...access_fields? }
  update:   { action: "update", app_id, name, display_name?, description?, graph_spec?, default_model?, status?, ...access_fields? }
  delete:   { action: "delete", app_id, name }
  validate: { action: "validate", app_id, graph_spec }

Common errors:
  - 404: Agent or app not found
  - 403: Not authorized
  - 409: Agent name already exists (on create)
  - 400: Invalid body or unsafe public agent (pass safety_acknowledged=true to override)

Read only: no. Destructive: yes. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `list`, `get`, `create`, `update`, `delete`, `validate`.
- `app_id` (`string`, required): The app ID (e.g. app_abc123)
- `daily_budget_usd` (`number`, optional): Daily spend cap in USD (null = unlimited)
- `default_model` (`string`, optional): Default LLM model identifier. Optional for create/update.
- `description` (`string`, optional): What this agent does. Optional for create/update.
- `display_name` (`string`, optional): Human-readable name. Optional for create/update.
- `graph_spec` (`string`, optional): Graph spec object. Required for create and validate; optional for update. Validated by graphSpecSchema except on validate (which returns structured issues instead of rejecting).
- `max_concurrent_runs` (`integer`, optional): Maximum simultaneous active runs (null = unlimited)
- `max_runs_per_app_per_hour` (`integer`, optional): Rate limit across the whole app per hour (null = unlimited)
- `max_runs_per_ip_per_hour` (`integer`, optional): Rate limit per IP address per hour (null = unlimited)
- `max_runs_per_user_per_hour` (`integer`, optional): Rate limit per authenticated user per hour (null = unlimited)
- `name` (`string`, optional): Agent slug. Required for get/update/delete; required for create (must match /^[a-z0-9][a-z0-9_-]{0,63}$/)
- `safety_acknowledged` (`boolean`, optional): Set true to confirm you accept the risks of public write-capable agents
- `status` (`string`, optional): Optional for update. Set to "disabled" to stop the agent accepting new runs. One of: `active`, `disabled`.
- `visibility` (`string`, optional): Who can invoke the agent. One of: `private`, `authenticated`, `public`.

Example input: `{"action":"list","app_id":"<app_id>"}`

### `butterbasemcp_manage_api_keys`

Manage API Keys · Destructive

List and revoke API keys (service keys) for the Butterbase platform account.

This is a platform-scoped tool — it operates on the authenticated account, not on a specific app.
To generate a new API key, use manage_auth_config (action: "generate_service_key").

Actions:
  - "list":   List all active API keys on the account (key secrets are NOT returned — only metadata)
  - "revoke": Permanently revoke a specific key by its ID

Parameters by action:
  list:   { action: "list" }
  revoke: { action: "revoke", key_id: "<uuid>" }

Examples:

  List all keys:
    Input:  { action: "list" }
    Output: [
      {
        id: "uuid-1234",
        prefix: "bb_sk_a1b2c3",
        name: "CI/CD Pipeline Key",
        created_at: "2025-01-15T10:00:00Z",
        last_used_at: "2025-04-01T08:30:00Z"
      },
      ...
    ]

  Revoke a key:
    Input:  { action: "revoke", key_id: "uuid-1234" }
    Output: { message: "API key revoked", key_id: "uuid-1234" }

Workflow — rotate a key:
  1. Call "list" to identify the key by name or prefix
  2. Call manage_auth_config (action: "generate_service_key") to create the replacement key (store the new secret immediately)
  3. Update all consumers (CI, scripts, MCP config) with the new key
  4. Call "revoke" with the old key_id to invalidate it

Common errors:
  - AUTH_INSUFFICIENT_PERMISSIONS: Must be authenticated as the account owner
  - RESOURCE_NOT_FOUND: key_id does not exist or belongs to a different account

Security notes:
  - Revocation is immediate and irreversible
  - If a key is compromised, revoke it before generating a replacement to minimise exposure window

Read only: no. Destructive: yes. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform on API keys. One of: `list`, `revoke`.
- `key_id` (`string`, optional): The UUID of the API key to revoke (required for "revoke")

Example input: `{"action":"list"}`

### `butterbasemcp_manage_auth_users`

Manage Auth Users · Destructive

Manage end-user auth records for an app.

Actions:
  - "list":   Paginated list of app_users (id, email, provider, provider_uid, email_verified,
              last_sign_in_at, created_at). Pass the next_cursor from a prior response to page.
  - "delete": Hard-delete an app user by id. Cascades to refresh tokens and verification codes.
              Use this to unblock OAuth migrations when an existing email/password row collides.

Parameters by action:
  list:   { app_id, action: "list", limit?, cursor? }
  delete: { app_id, action: "delete", user_id }

Tips:
  - Looking for a user by email? Call list and filter client-side; this tool does not search by email.
  - To switch a user from email/password to Google OAuth without deleting, just have them sign in
    with Google — the OAuth callback now links the existing email row in place automatically.

Read only: no. Destructive: yes. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `list`, `delete`.
- `app_id` (`string`, required): The app ID
- `cursor` (`string`, optional): Optional for list. Pass next_cursor from a prior response.
- `limit` (`integer`, optional): Optional for list. 1–200, default 50.
- `user_id` (`string`, optional): Required for delete. The app_user UUID to delete.

Example input: `{"action":"list","app_id":"<app_id>"}`

### `butterbasemcp_manage_migrations`

Manage Migrations · Destructive

Read and control in-flight app migrations.

This complements manage_app (actions: move / move_status / teardown_source_replica) with the four
operational routes those actions don't cover.

Actions:
  - get_active             : { app_id, action: "get_active" }
                             Returns the running migration for this app, or { migration: null }.
  - abort                  : { app_id, migration_id, action: "abort" }
                             Cancel a migration that has NOT yet reached "flipping_routing".
                             Returns 409 if already past cutover; use "reverse" instead.
  - reverse                : { app_id, migration_id, action: "reverse" }
                             Roll a COMPLETED migration back to source. Only works while the
                             source replica is still retained (see list_source_replicas).
  - list_source_replicas   : { action: "list_source_replicas" }
                             Lists active retained source replicas for the caller's apps.
                             Use this before tearing down to discover what's still around.

Use list_regions + manage_app (action: "move") to start a move; manage_app (action: "move_status") to watch progress;
manage_app (action: "teardown_source_replica") when you're confident the move is stable.

Read only: no. Destructive: yes. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): The action to perform. One of: `get_active`, `abort`, `reverse`, `list_source_replicas`.
- `app_id` (`string`, optional): Required for get_active / abort / reverse.
- `migration_id` (`string`, optional): Required for abort / reverse.

Example input: `{"action":"get_active"}`

### `butterbasemcp_manage_office`

Manage Office · Destructive

Use Butterbase Office for one explicit organization (org_id, UUID). Actions: status, enable, pause, resume, list_decisions, get_decision, get_decision_context, approve, deny, create_conversation, list_conversations, get_conversation, ask, list_memory, search_memory, get_source_artifact, record_decision, record_commitment, record_learning, list_principles, get_principle, record_principle, list_agent_memories, search_agent_memories, get_agent_memory, record_agent_memory, amend_agent_memory, archive_agent_memory, list_skills, get_skill, list_skill_versions, list_skill_runs, draft_skill, test_skill, reject_skill, approve_skill, set_skill_enabled, rollback_skill, run_skill, amend_principle, retire_principle, get_office_settings, update_office_settings, list_office_records.
Protected enable/pause/resume/approve, update_office_settings and skill approve/set-enabled/rollback/run return a confirmation receipt and browser URL; show the URL to the initiating owner and never resolve it yourself. status with confirmation_id observes that receipt. Settings updates require the revision returned by get_office_settings.
Skill draft submits once through Ask; observe its conversation_id and turn_id. Skill test pins one read-only job: retry with the same idempotency_key to observe that run, never create a fresh test just to poll. Skills and their versions/runs are separately paginated. API-key/member reads show approved content; private drafts/tests are inspected by the owner in the browser.
Ask submits once with message and idempotency_key, waits at most 25 seconds, and may return a running or awaiting_approval turn. Observe it with get_conversation using its exact conversation_id and turn_id; never submit another Ask just to poll.
Agent-memory list/search return compact summaries; get_agent_memory reads the full entry. Agent-memory amend/archive require expected_revision in the payload. Legacy memory actions explicitly access business records rather than curated agent context.
Use a fresh idempotency_key for each intended write, reuse it for retries. Decisions use decision_id and decision_source (office or substrate). Memory payloads follow the matching native record schema. Members may read/deny; new Ask, conversation creation and memory writes require the owner. Managed background keys cannot mutate through this facade.

Read only: no. Destructive: yes. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): No description. One of: `status`, `enable`, `pause`, `resume`, `list_decisions`, `get_decision`, `get_decision_context`, `approve`, `deny`, `create_conversation`, `list_conversations`, `get_conversation`, `ask`, `list_memory`, `search_memory`, `get_source_artifact`, `record_decision`, `record_commitment`, `record_learning`, `list_principles`, `get_principle`, `record_principle`, `list_agent_memories`, `search_agent_memories`, `get_agent_memory`, `record_agent_memory`, `amend_agent_memory`, `archive_agent_memory`, `list_skills`, `get_skill`, `list_skill_versions`, `list_skill_runs`, `draft_skill`, `test_skill`, `reject_skill`, `approve_skill`, `set_skill_enabled`, `rollback_skill`, `run_skill`, `amend_principle`, `retire_principle`, `get_office_settings`, `update_office_settings`, `list_office_records`.
- `org_id` (`string`, required): No description.
- `after_sequence` (`integer`, optional): No description.
- `confirmation_id` (`string`, optional): No description.
- `conversation_id` (`string`, optional): No description.
- `cursor` (`string`, optional): No description.
- `decision_id` (`string`, optional): No description.
- `decision_source` (`string`, optional): No description.
- `enabled` (`boolean`, optional): No description.
- `expected_revision` (`integer`, optional): No description.
- `idempotency_key` (`string`, optional): No description.
- `interval_minutes` (`number`, optional): No description. One of: `10`, `15`, `30`, `60`.
- `kind` (`string`, optional): No description.
- `limit` (`string`, optional): No description.
- `memory_id` (`string`, optional): No description.
- `message` (`string`, optional): No description.
- `payload` (`string`, optional): No description.
- `principle_id` (`string`, optional): No description.
- `query` (`string`, optional): No description.
- `reason` (`string`, optional): No description.
- `resource` (`string`, optional): No description. One of: `entities`, `business_memory`, `source_artifacts`, `actions`, `principles`, `skills`, `skill_versions`, `skill_runs`.
- `skill_id` (`string`, optional): No description.
- `source_artifact_id` (`string`, optional): No description.
- `status` (`string`, optional): No description.
- `title` (`string`, optional): No description.
- `turn_id` (`string`, optional): No description.
- `version` (`string`, optional): No description.
- `wait_ms` (`integer`, optional): No description. Default: `15000`.

Example input: `{"action":"status","org_id":"<org_id>"}`

### `butterbasemcp_manage_preview`

Manage Preview Deployment · Destructive

Create, inspect, or re-copy an app's preview deployment.

A preview deployment is a copy of the live app that the user can safely break.
It is a separate app with its own database, its own files and its own sign-ups,
so nothing done in it reaches the app real users are on. Note this is a whole
second app, not a frontend deploy.

WHAT IT CONTAINS. A preview starts as a copy of the live app: its tables, its
functions, its access rules, and the live app's DATA — every table's rows, every
signed-up account (with password hashes, so people sign in with the passwords
they already have), and every uploaded file. That includes any personal data the
live app holds about its users. TELL THE USER THIS BEFORE CREATING ONE.

What deliberately does NOT travel:
  - The VALUES of app-level secrets and API keys (see SECRETS below).
  - Connected third-party accounts. The records are copied and then disconnected,
    and integrations and scheduled jobs are switched off, so a preview cannot act
    as the user against Stripe, Google or anything else. Reconnect on the preview
    if needed.
  - Billing and analytics history (subscriptions, orders, daily activity): that
    belongs to the live app.
  - Uploaded file IDs change. If the app stores a file id inside one of its own
    tables, that column still points at the live app's copy of the file.
  - Files whose contents are already missing in the live app (a file record with
    no data, e.g. an upload that never finished). They are skipped, and the
    create/reset job's warnings name them by their live object ids.
On a self-hosted deployment without the app-copy engine, a preview is populated
from tables marked _seed:true only; the create/reset job says so in its warnings.

Actions:
  "create" — provision a preview deployment for this app. Needs Launch or a paid
             plan above it (the free Playground plan is not allowed). A preview
             counts as one project against the plan's max_projects limit, so
             creating when the limit is reached fails with 403 (the message names
             the current count and the limit). A plan without previews at all also
             fails with 403. Returns a job_id; poll GET /v1/clone-jobs/{job_id}
             for progress. One per app.
             The job reaches status "copying_data" once the app exists and the
             data copy is running, and only reports "completed" once that copy has
             finished — a job that is not "completed" is not a usable preview, and
             the preview app is not linked or listed by "status" until it is.
  "status" — return the linked preview app id and the last push-live/re-copy
             times, or { staging_app_id: null } if there is none.
  "reset"  — throw away the preview's data and copy it fresh from the live app.
             Destructive to the preview only; the live app is never written to.
             Same "copying_data" then "completed" progression as create.
  "get_env_overrides" — list the KEY NAMES of the preview's secret overrides.
             Values are never returned.
  "set_env_overrides" — replace this app's preview secret overrides with
             `env_overrides`. Pass {} to clear them all. Can be called BEFORE
             "create" — overrides are stored against the live app, so setting them
             first makes the preview come up working rather than with every key
             empty. They also survive deleting and re-creating the preview.

SECRETS: the live app's secret VALUES are deliberately NOT copied into a preview.
Each key is created on the preview app with an EMPTY value, so a function that
needs it fails with a missing-key error instead of quietly running against live
credentials (a preview holding the live Stripe key charges real cards). The
create job's warnings name every key that was withheld. Supply test values with
"set_env_overrides" — ideally BEFORE "create", so the preview is usable the
moment it finishes.

To push a preview's changes onto the live app, use the separate promote_preview
tool.

Note on naming: the REST paths and some response fields still say "staging" —
that is the internal name for the same thing. What the user reads says "preview".

Idempotency: "status" and "get_env_overrides" are read-only. "create" refuses
with 409 if a preview already exists. "reset" is destructive to the preview app.
"set_env_overrides" is idempotent (it replaces the whole set).

Read only: no. Destructive: yes. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): No description. One of: `create`, `status`, `reset`, `get_env_overrides`, `set_env_overrides`.
- `app_id` (`string`, required): The live app id.
- `env_overrides` (`object`, optional): For action="set_env_overrides": the preview app's secret values, as a flat string->string object. Replaces the whole override set; pass {} to clear.

Example input: `{"action":"create","app_id":"<app_id>"}`

### `butterbasemcp_promote_preview`

Push Preview Deployment Live · Destructive

Push a preview deployment's changes onto the live app.

Copies the preview's STRUCTURE onto its live app — tables, access rules,
functions, Durable Objects, config, the code snapshot — AND DEPLOYS THE
FRONTEND. This is a real deploy to the app real users are on.

Data is never pushed. The live app's rows are never overwritten and never
removed. Only the shape of the database and the application code change. A table
added in the preview is created on the live app EMPTY.

Anything that would throw away live data is refused outright: if pushing would
require deleting a field, deleting a table, changing a field's type, or making an
existing field required, the WHOLE push is blocked and none of it is applied. The
refusal names exactly which statements are stuck, so the user can apply them to
the live app themselves (if they actually intend them) and push again. This is
all-or-nothing on purpose — one blocked statement cancels the five safe ones
alongside it, because a half-updated app is worse off than an un-updated one.

Deletions made only in the preview are KEPT on the live app (a push never deletes
anything) — the "check" action lists these separately as informational, not as
something blocking the push.

Actions:
  "check" — read-only, no side effects. Returns
            { can_promote, additive[], blocked[], ignored_removals[] }.
            `additive` is what will be applied; `blocked` is what is stuck.
  "run"   — performs the push for real, including a frontend deploy to the live
            app. Returns a job_id; poll GET /v1/clone-jobs/{job_id} for progress.

Always call "check" before "run" so both you and the user know whether the push
will be blocked and what will not carry over. Because "run" deploys to the live
app, CONFIRM WITH THE USER BEFORE CALLING IT — never call "run" unprompted.

Note on naming: the REST paths and some response fields still say "staging" and
"promote" — those are the internal names for the same thing. What the user reads
says "preview" and "push live".

Read only: no. Destructive: yes. Idempotent (safe to retry): no.

Inputs:

- `action` (`string`, required): No description. One of: `check`, `run`.
- `app_id` (`string`, required): The live app id (not the preview app id).

Example input: `{"action":"check","app_id":"<app_id>"}`

## Related

Other Developer Tools connectors ([all 130](/agentkit/connectors/?category=developer-tools)):

- [GitHub](/agentkit/connectors/github/): Scalekit connector, OAuth, 217 tools
- [Microsoft Entra ID](/agentkit/connectors/microsoftentraid/): Scalekit connector, OAuth, 82 tools
- [Datadog](/agentkit/connectors/datadog/): Scalekit connector, API key, 105 tools


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
