# io.github.seldonframe/seldonframe-mcp (npm · @seldonframe/mcp)

Open-source GoHighLevel alternative for agencies. White-label CRM, booking, intake, chatbot via MCP.

- Trust score: 59/100 (low)
- Change this week: +16
- Registry status: deprecated
- Liveness: live
- Owner verified: no
- Last scored: 2026-08-03

> **Deprecated**: this server is marked deprecated in the MCP registry.

## Components

- npm · `@seldonframe/mcp`: 59/100 (this document), [markdown](https://verifymcp.io/servers/seldonframe-seldonframe-mcp/seldonframe-mcp.md), [page](https://verifymcp.io/servers/seldonframe-seldonframe-mcp/seldonframe-mcp)

## Channel facts

- Registry: `npm`
- Package: `@seldonframe/mcp`
- Version: `1.45.1`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-08-03.

- **Supply Chain Security**: 86/100
  - No malware found by supply-chain analysis.
  - Only part of the dependency tree could be resolved (94 of 98), so this covers what we could see, not the whole tree.
  - No install/post-install scripts declared.
  - Only part of the dependency tree could be resolved (94 of 98), so this covers what we could see, not the whole tree.
- **Provenance & Transparency**: 45/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (AGPL-3.0-or-later).
  - Actively maintained (last published 26 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 29/100
  - AI-judged instruction clarity (poor).
  - Context-footprint check failed: tool/resource definitions use about 29711 tokens (~196/item across 151 items; 151 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 23/100
  - Stability observed for 7 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 99/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 98% of tool parameters carry a description.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### Claude

```bash
claude mcp add seldonframe-seldonframe-mcp -- npx -y @seldonframe/mcp
```

### Codex

```bash
codex mcp add seldonframe-seldonframe-mcp -- npx -y @seldonframe/mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "seldonframe-seldonframe-mcp": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "@seldonframe/mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add seldonframe-seldonframe-mcp --command npx --arg -y --arg @seldonframe/mcp
```

### Hermes

```yaml
mcp_servers:
  seldonframe-seldonframe-mcp:
    command: "npx"
    args: ["-y", "@seldonframe/mcp"]
```

### Other

```json
{
  "mcpServers": {
    "seldonframe-seldonframe-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "@seldonframe/mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-08-02 (score 59, +41)

- [security regression] Provenance: unverified → fail
- [security improvement] Known CVEs: unverified → partial
- [security improvement] Install scripts: unverified → pass
- [security improvement] Malware scan: unverified → pass
- [security] Stability: Stability not yet verified: not enough scan history yet (needs a 30-day window).
- [functional improvement] Maintenance: unverified → pass
- [functional improvement] Schema quality: unverified → poor
- [functional improvement] Stability: unverified → 0.20
- [functional improvement] Dependency health: unverified → partial
- [functional improvement] License: unverified → pass
- [functional improvement] MCP protocol: unverified → pass
- [functional] Licence: AGPL-3.0-or-later

### 2026-08-01 (score 18, +13)

- [functional improvement] Tool coverage: unverified → 100

### 2026-07-31 (score 5, −20)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-07-30 (score 25, +1)

- [security regression] Malware scan: pass → unverified
- [functional improvement] Tool coverage: unverified → 100

### 2026-07-28 (score 24, −19)

- [functional regression] Tool coverage: 100 → unverified
- [functional] First check of Schema quality: unverified

### 2026-07-27 (score 43)

First indexed and scored.

## MCP tools (151)

### `create_full_workspace` (~686 tokens)

PREFERRED for new workspaces. Atomic, server-side workspace creation: takes structured business info and creates everything in ONE call — workspace, business profile, CRM with industry-specific pipeline stages, booking page with availability, intake form, themed landing page, all deployed with live URLs. Use this instead of create_workspace + a long sequence of customization tools. The pipeline runs server-side with a fixed order — same input always produces same output, no retries, no 404s. MANDATORY FOLLOW-UP: After this returns `status: 'ready'`, ask the operator verbatim 'What email should I use for your account? This is where you'll get your login link and notifications.' Then call `finalize_workspace({ workspace_id, email })`. The admin dashboard URL is ONLY created by finalize_workspace — it does not exist in this response (so there's nothing for you to display prematurely). Example: create_full_workspace({ business_name: 'Summit Air Comfort', city: 'Phoenix', state: 'AZ', phone: '(480) 555-2100', services: ['AC repair', 'heating installation', 'duct cleaning'], business_description: 'Residential and commercial HVAC in Phoenix', review_count: 950, review_rating: 4.7, trust_signals: ['licensed', 'bonded', 'insured'], emergency_service: true, same_day: true, service_area: ['Scottsdale', 'Tempe', 'Mesa'] })

Input parameters:

- `address` (string): Optional business address.
- `business_description` (string, required): One paragraph describing the business — drives the hero subhead, about section, and (critically) the personality classifier. Include industry words verbatim ('residential HVAC', 'family-owned plumbin…
- `business_name` (string, required): Business display name (e.g. 'Summit Air Comfort').
- `certifications` (array): Optional — credentials like ['EPA-certified', 'NATE-certified']. Surfaces in trust strip.
- `city` (string, required): Operator's city. Drives timezone inference.
- `email` (string): Optional contact email surfaced in the landing footer (NOT the operator's account email — that comes via finalize_workspace).
- `emergency_service` (boolean): Optional — operator offers 24/7 emergency service. Surfaces in nav + hero.
- `phone` (string, required): Business phone, any format. Renders in nav, hero, footer.
- `review_count` (number): Optional — number of reviews. Surfaces in trust strip + hero proof metric.
- `review_rating` (number): Optional — average star rating (e.g. 4.7). Surfaces in trust strip.
- `same_day` (boolean): Optional — same-day service available. Surfaces in trust strip.
- `service_area` (array): Optional — cities or neighborhoods served (e.g. ['Scottsdale', 'Tempe']).
- `services` (array, required): Services / offerings the business provides — each as a plain string. The classifier reads these to pick the right CRM personality (HVAC, legal, dental, coaching, agency, default).
- `state` (string, required): US state code or full name (or Canadian province). Drives timezone inference.
- `trust_signals` (array): Optional — short claims like ['licensed', 'bonded', 'insured']. Surfaces in trust strip.

### `create_workspace_from_google_paste` (~1103 tokens)

PREFERRED when the operator pastes a Google Maps business listing. Same atomic pipeline as create_full_workspace; this tool's docs guide the paste-to-fields extraction. Claude Code parses the paste BEFORE calling this tool — never pass the raw paste text. EXTRACTION RULES (apply in order): 1) business_name → the bold business title at the top of the listing. 2) phone → the digits next to the phone icon. 3) address → the line next to the location pin. Parse city + state from this address into separate fields (city/state). 4) services → derive from BOTH the categories chip row (e.g. 'Plumber · Emergency plumbing service') AND any explicit 'Services' section ('Drain cleaning', 'Water heater repair'). Dedupe; keep 5-12 distinct strings. 5) business_description → synthesize 1-2 sentences from the categories + 'About' / 'From the business' section. Include industry words verbatim (the personality classifier reads this). 6) review_rating + review_count → the '4.7 ★ (950)' element. 7) trust_signals → 'Licensed', 'Bonded', 'Insured', 'Family-owned' if mentioned. 8) emergency_service / same_day → set true if 'open 24 hours', '24/7', 'same-day service' appears. 9) service_area → cities mentioned in 'Service area' section. 10) weekly_hours → parse the hours block ('Monday: 9 AM-5 PM, Tuesday: closed, ...') into the canonical shape: {monday:{enabled:true,start:'09:00',end:'17:00'},tuesday:{enabled:false,start:'09:00',end:'17:00'},...}. Keys MUST be FULL DAY NAMES (sunday/monday/.../saturday); times MUST be HH:MM 24-hour. 'Closed' → enabled:false (start/end are placeholders). 'Open 24 hours' → start:'00:00', end:'23:59'. These hours are written DIRECTLY to the booking template's availability — wrong shape = booking page falls back to Mon-Fri 9-5 default. 11) google_place_url → the Maps URL the operator pasted, if visible. Optional, stored on soul.business.maps_url for audit. MANDATORY FOLLOW-UP: same as create_full_workspace — after this returns `status: 'ready'`, ask 'What emai…

Input parameters:

- `address` (string): Optional full address line from the listing.
- `business_description` (string, required): 1-2 sentence summary synthesized from Maps categories + 'About' / 'From the business' section.
- `business_name` (string, required): Business display name (top of the Maps listing).
- `certifications` (array): Optional — credentials mentioned in the listing (['EPA-certified', ...]).
- `city` (string, required): City parsed from the Maps address line.
- `email` (string): Optional contact email from the listing (NOT the operator's account email).
- `emergency_service` (boolean): Optional — listing shows 'open 24 hours' or '24/7'.
- `google_place_url` (string): Optional — the Google Maps share URL for the listing. Stored on soul.business.maps_url for audit.
- `phone` (string, required): Phone number from the Maps phone icon row, any format.
- `review_count` (number): Optional — review count from '★ (N)' display.
- `review_rating` (number): Optional — average star rating (e.g. 4.7).
- `same_day` (boolean): Optional — listing mentions 'same-day service'.
- `service_area` (array): Optional — cities/neighborhoods from the 'Service area' section.
- `services` (array, required): Services derived from the Maps categories + 'Services' chips, deduped. 5-12 strings.
- `state` (string, required): US state code or full name parsed from the Maps address line.
- `testimonials` (array): OPTIONAL real review excerpts extracted from the Maps paste, VERBATIM (do not rewrite). Each entry: { quote, name?, role?, rating? }. quote = the actual review text the customer wrote (≤800 chars). n…
- `trust_signals` (array): Optional — 'Licensed', 'Bonded', 'Insured', 'Family-owned' if surfaced.
- `weekly_hours` (object): Canonical weekly schedule extracted from the Maps hours block. Keys are FULL DAY NAMES (sunday/monday/tuesday/wednesday/thursday/friday/saturday). Each value is { enabled: boolean, start: 'HH:MM', en…

### `list_workspaces` (~32 tokens)

List all workspaces known to this device (plus any Pro workspaces if SELDONFRAME_API_KEY is set).

### `switch_workspace` (~34 tokens)

Set the active workspace. Subsequent tool calls act on it by default.

Input parameters:

- `workspace_id` (string, required): Target workspace id.

### `clone_workspace` (~61 tokens)

Clone an existing workspace as a template. Example: clone_workspace({ source_workspace_id: 'wsp_x', name: 'Copy' })

Input parameters:

- `name` (string, required): Name for the new workspace.
- `source_workspace_id` (string, required): Workspace to clone from.

### `link_workspace_owner` (~107 tokens)

Claim an anonymously-created workspace under your real account. After linking, the admin URLs (dashboard, contacts, deals) become usable once you sign in at app.seldonframe.com. Requires SELDONFRAME_API_KEY to be set in the MCP environment. The workspace bearer token continues to work — no rotation needed. Example: link_workspace_owner({}) to claim the active workspace.

Input parameters:

- `workspace_id` (string): Optional workspace id to claim. Defaults to the active workspace from this device.

### `send_welcome_email` (~129 tokens)

Email the active workspace's four key URLs (landing, booking, intake, admin dashboard) to a user. Use this AFTER create_workspace, only when the user has explicitly given their email — never auto-send. The admin URL is bearer-token-scoped and expires in 7 days. Example: send_welcome_email({ email: 'alice@example.com', name: 'Alice' }).

Input parameters:

- `email` (string, required): Recipient email address.
- `name` (string): Optional recipient name (used in the greeting).
- `workspace_id` (string): Optional workspace override. Defaults to active workspace.

### `collect_operator_email` (~281 tokens)

MANDATORY post-workspace step. Send the operator's welcome email + record them as a lead in SeldonFrame's CRM. EVERY workspace creation flow MUST end with this call — it is the only thing that triggers the welcome email (with the live URLs), creates their account so the admin login keeps working past the 7-day token, captures them in our pipeline so we can follow up, and lets Stripe pre-fill their email on upgrade. Skipping it leaves the operator with a one-shot URL and no way to recover access. Flow: after create_workspace returns, ask the user verbatim 'What email should I use for your account? This is where you'll get your login link and any notifications.' Then call this tool with whatever email they give you. If you'd rather use the wrapper, call `finalize_workspace({ email })` instead — same effect plus a formatted summary at the end. Example: collect_operator_email({ email: 'max@precisionplumbing.com', name: 'Max' })

Input parameters:

- `email` (string, required): Operator email — used as the welcome email recipient AND as the unique key for the SeldonFrame CRM lead.
- `name` (string): Optional operator name (used in the email greeting and on the CRM lead).
- `workspace_id` (string): Optional workspace override. Defaults to the workspace just created.

### `finalize_workspace` (~257 tokens)

ONE-CALL CLOSING WRAPPER for the workspace creation flow. Bundles email collection (welcome email + lead capture via collect_operator_email) AND produces the final operator-facing summary (live URLs, what's configured, admin link). Call this as the LAST step of every workspace creation. After create_workspace returns, ask the user 'What email should I use for your account? This is where you'll get your login link and any notifications.' Then call this tool with the email they give you. Returns a `summary` string Claude Code should paraphrase verbatim to the operator. Use this instead of calling collect_operator_email directly when you want a single tool call to close the loop. Skipping this is the same as skipping email collection — leaves the operator with a one-shot URL and no recovery path. Example: finalize_workspace({ email: 'max@precisionplumbing.com', name: 'Max' })

Input parameters:

- `email` (string, required): Operator email — used as the welcome email recipient AND as the unique key for the SeldonFrame CRM lead.
- `name` (string): Optional operator name (used in the email greeting and on the CRM lead).
- `workspace_id` (string): Optional workspace override. Defaults to the workspace just created.

### `revoke_bearer` (~181 tokens)

Revoke workspace bearer tokens. Useful if a device token has leaked or if a builder wants to rotate. Modes (pick exactly one): `{}` revokes ALL tokens except the current device's (safe default — other devices kicked off, this device keeps working); `{ token_id }` revokes a specific token by its UUID; `{ all: true }` revokes every token including the current one — requires SELDONFRAME_API_KEY because it locks this device out. After revoking the current token the MCP clears the local entry from ~/.seldonframe/device.json.

Input parameters:

- `all` (boolean): Revoke ALL tokens including caller. Requires SELDONFRAME_API_KEY.
- `token_id` (string): UUID of a specific token to revoke (from api_keys.id).
- `workspace_id` (string): Optional workspace override. Defaults to active workspace.

### `update_landing_content` (~154 tokens)

Rewrite the workspace's public landing page hero — headline, subhead, and primary CTA label. C3.4 made this blueprint-aware: the operator's edit lands without losing any of the renderer's visual polish (typography, layered-shadow buttons, animations, etc.). Use this for the most common copy edits; for granular per-section / per-item edits use update_landing_section.

Input parameters:

- `cta_label` (string): Primary call-to-action button text, e.g. 'Book a call'.
- `headline` (string): Main hero heading. Keep short; 1 line.
- `subhead` (string): One-sentence supporting line under the headline.
- `workspace_id` (string): Optional workspace override.

### `update_landing_section` (~215 tokens)

Granular per-field landing edit — change any single slot in any section of the blueprint-rendered landing page. Use when update_landing_content's three fields aren't enough. Section types: emergency-strip, hero, trust-strip, services-grid, about, mid-cta, testimonials, service-area, faq, footer. Field is a dot-segmented path on that section (e.g. 'headline', 'subhead', 'items.0.title', 'items.2.answer', 'showHours'). Value is the new value (string for copy, boolean for flags, etc.).

Input parameters:

- `field` (string, required): Dot-segmented field path on the section. Examples: 'headline', 'subhead', 'items.0.title', 'items.2.answer', 'showHours'.
- `section` (string, required)
- `value` (required): New value for the field. String for copy, number for ratings, boolean for flags, object/array for richer slots.
- `workspace_id` (string): Optional workspace override.

### `update_theme` (~109 tokens)

Change workspace theme: mode (dark|light), primary_color (#hex), accent_color (#hex), font_family. Any subset. Available fonts: Inter, DM Sans, Playfair Display, Space Grotesk, Lora, Outfit.

Input parameters:

- `accent_color` (string): Hex color.
- `font_family` (string)
- `mode` (string)
- `primary_color` (string): Hex color like '#14b8a6'.
- `workspace_id` (string): Optional workspace override.

### `apply_design_md` (~217 tokens)

Apply a DESIGN.md file (the Google Labs format: YAML front matter for tokens + Markdown for rationale) to the workspace theme. Maps tokens.colors.primary, tokens.colors.accent, tokens.mode, and tokens.typography.body to OrgTheme fields. Unmapped tokens (spacing, custom shadows, etc.) are returned so Claude Code can decide whether to apply them via update_landing_page or surface to the operator. USE-WHEN the operator says: 'apply my DESIGN.md', 'use this brand kit', 'match my company's design tokens', 'import my design system', or 'theme my workspace from this file'. Example: apply_design_md({ design_md_content: '<full file content as string>' })

Input parameters:

- `design_md_content` (string, required): Full content of the DESIGN.md file as a string. The MCP client reads the file in the operator's process (e.g. via fs.readFile in Claude Code) and passes the content here. Server caps at 256KB.
- `workspace_id` (string): Optional workspace override.

### `apply_motion_preset` (~230 tokens)

Set the workspace's motion intensity preset. Stored on OrgTheme.motionPreset and read by the renderer + Claude Code as a hint for content generation. Presets: 'minimal' (no motion, accessibility-first), 'subtle' (fade-up reveals only), 'balanced' (reveals + stagger + hover-lift — the default), 'editorial' (full effects: counters, magnetic CTAs, text-reveal). USE-WHEN the operator says: 'make my pages feel more premium', 'tone down the animation', 'I want it editorial', 'respect reduced motion', 'no animation please', or directly references a preset name. Example: apply_motion_preset({ preset: 'editorial' })

Input parameters:

- `preset` (string, required): The motion intensity preset to apply. 'minimal'=no motion (accessibility-first). 'subtle'=fade-up reveals only. 'balanced'=reveals + stagger + hover-lift (default). 'editorial'=full effects (counters…
- `workspace_id` (string): Optional workspace override.

### `import_claude_design_handoff` (~284 tokens)

Validate a Claude Design handoff bundle (the artifact Claude Design produces when designs are ready for code), apply its embedded design tokens to the workspace theme, and return a structured manifest of the components with per-component next-step instructions for wiring them into pages. Does NOT auto-execute generated React on live pages — components route through human/eval review (the same gate that protects published agents) before customer-facing surfaces ship. USE-WHEN the operator says: 'import this Claude Design handoff', 'wire up these components', 'I just exported a design from Claude Design', or 'apply this design bundle to my workspace'. Example: import_claude_design_handoff({ bundle: { meta: { project_name: 'Acme HVAC' }, tokens: { colors: { primary: '#0e7490' } }, components: [{ name: 'TrustStrip', surface: 'landing', react_source: '<TSX content>' }] } })

Input parameters:

- `bundle` (object, required): The handoff bundle as a JSON object. Required fields: bundle.components (array of {name, react_source OR html_source}). Optional: bundle.meta, bundle.tokens (DESIGN.md-shape), bundle.assets. Server c…
- `workspace_id` (string): Optional workspace override.

### `list_automations` (~34 tokens)

List automations configured in the active (or specified) workspace.

Input parameters:

- `workspace_id` (string): Optional workspace override.

### `install_caldiy_booking` (~55 tokens)

Install the booking page (event types, availability, scheduled bookings). Example: install_caldiy_booking({})

Input parameters:

- `config` (object): Optional booking-page configuration overrides.
- `workspace_id` (string): Optional workspace override.

### `install_formbricks_intake` (~61 tokens)

Install an intake form (questions, conditional logic, automatic CRM sync). Example: install_formbricks_intake({})

Input parameters:

- `form_id` (string): Optional existing intake-form id to bind.
- `workspace_id` (string): Optional workspace override.

### `get_workspace_snapshot` (~113 tokens)

Return a structured read-only snapshot of workspace state: workspace metadata, Soul (if submitted), theme, enabled blocks with configs, entity counts (contacts/bookings/intake forms/submissions), recent Seldon It events, and public URLs. YOU reason over this snapshot to decide what to do next, then call the appropriate typed tools (update_landing_content, configure_booking, customize_intake_form, update_theme, install_*). Zero server-side LLM cost.

Input parameters:

- `workspace_id` (string): Optional workspace override.

### `fetch_source_for_soul` (~84 tokens)

Fetch a URL and return normalized text (headings + body, up to 256KB). Use this to gather raw content from the operator's existing website; then extract a structured business profile and save it with submit_soul. Zero LLM cost to SeldonFrame — extraction runs in this session.

Input parameters:

- `url` (string, required): Absolute URL to fetch.

### `submit_soul` (~120 tokens)

Save a business profile to the active workspace. The profile drives the landing page, intake form copy, and AI-agent context. Call this after fetch_source_for_soul or after gathering details from the user. Triggers a re-render of the public landing page so changes are visible immediately.

Input parameters:

- `soul` (object, required): Business profile. Expected keys: business_name, tagline, soul_description, phone, email, address, offerings, faqs, testimonials. Additional keys allowed — they're preserved for future use.
- `workspace_id` (string): Optional workspace override.

### `connect_custom_domain` (~70 tokens)

Connect + verify a custom domain. Pro capability — requires SELDONFRAME_API_KEY. Example: connect_custom_domain({ domain: 'app.mysite.com' })

Input parameters:

- `domain` (string, required): Fully qualified domain, e.g. client.example.com.
- `workspace_id` (string): Optional workspace override.

### `export_agent` (~31 tokens)

Export the current workspace as a portable .agent/ bundle.

Input parameters:

- `workspace_id` (string): Optional workspace override.

### `store_secret` (~79 tokens)

Store a workspace-scoped secret (encrypted at rest). Example: store_secret({ key: 'STRIPE_API_KEY', value: 'sk_…' })

Input parameters:

- `key` (string, required): Secret name, e.g. 'STRIPE_API_KEY'.
- `value` (string, required): Secret plaintext value.
- `workspace_id` (string): Optional workspace override.

### `list_secrets` (~32 tokens)

List secret metadata (names, timestamps) without exposing plaintext.

Input parameters:

- `workspace_id` (string): Optional workspace override.

### `rotate_secret` (~62 tokens)

Rotate or delete a workspace secret. Omit new_value to delete.

Input parameters:

- `key` (string, required): Secret name to rotate.
- `new_value` (string): New plaintext value. Omit to delete the secret.
- `workspace_id` (string): Optional workspace override.

### `list_contacts` (~46 tokens)

List contacts in the active workspace. Returns every contact the caller can read. Example: list_contacts({}).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_contact` (~56 tokens)

Fetch one contact by id. Example: get_contact({ contact_id: 'abc-...' }).

Input parameters:

- `contact_id` (string, required): UUID of the contact.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_contact` (~178 tokens)

Create a new contact. Typical use: 'Add Jane Doe jane@acme.co as a lead'. Example: create_contact({ first_name: 'Jane', last_name: 'Doe', email: 'jane@acme.co', status: 'lead' }).

Input parameters:

- `email` (string): Optional but strongly recommended — unlocks form auto-linking and email sends.
- `first_name` (string, required): Required. Contact's first name.
- `last_name` (string): Optional. Last name.
- `source` (string): Optional source tag (e.g., 'manual', 'intake-form', 'import').
- `status` (string): Optional lifecycle stage (e.g., 'lead', 'prospect', 'customer'). Defaults to 'lead'.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `update_contact` (~117 tokens)

Update fields on an existing contact. Partial — omit fields you don't want to change. Example: update_contact({ contact_id: '...', status: 'customer' }).

Input parameters:

- `contact_id` (string, required): UUID of the contact to update.
- `email` (string): Optional new email.
- `first_name` (string): Optional new first name.
- `last_name` (string): Optional new last name.
- `status` (string): Optional new lifecycle stage.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `delete_contact` (~70 tokens)

Delete a contact and all linked deals/activities (cascades via FK). Irreversible. Example: delete_contact({ contact_id: '...' }).

Input parameters:

- `contact_id` (string, required): UUID of the contact to delete.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_deals` (~40 tokens)

List deals in the active workspace. Example: list_deals({}).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_deal` (~56 tokens)

Fetch one deal by id. Example: get_deal({ deal_id: '...' }).

Input parameters:

- `deal_id` (string, required): UUID of the deal.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_deal` (~183 tokens)

Create a new deal attached to a contact on the default pipeline. Typical use: 'Create a $5k deal for Jane Doe at the Discovery stage'. Example: create_deal({ contact_id: '...', title: 'Q2 retainer', value: 5000, stage: 'Discovery' }).

Input parameters:

- `contact_id` (string, required): UUID of the contact this deal belongs to.
- `probability` (number): Optional win probability 0-100. Defaults to 0.
- `stage` (string): Optional stage name (e.g. 'Discovery', 'Proposal'). Defaults to the first stage of the default pipeline.
- `title` (string, required): Human-readable deal name.
- `value` (number): Optional deal value in workspace's default currency. Defaults to 0.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `update_deal` (~125 tokens)

Update a deal. Partial — omit fields to keep them. For stage-only moves prefer move_deal_stage (clearer intent). Example: update_deal({ deal_id: '...', value: 7500 }).

Input parameters:

- `deal_id` (string, required): UUID of the deal.
- `probability` (number): Optional new probability (0-100).
- `stage` (string): Optional new stage.
- `title` (string): Optional new title.
- `value` (number): Optional new value.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `move_deal_stage` (~115 tokens)

Move a deal to a new stage. Same effect as dragging the card on the kanban. Example: move_deal_stage({ deal_id: '...', to_stage: 'Proposal' }).

Input parameters:

- `deal_id` (string, required): UUID of the deal.
- `probability` (number): Optional. Stage probability (0-100) if the workspace's pipeline has one defined for this stage.
- `to_stage` (string, required): Destination stage name.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `delete_deal` (~58 tokens)

Delete a deal. Irreversible. Example: delete_deal({ deal_id: '...' }).

Input parameters:

- `deal_id` (string, required): UUID of the deal.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_activities` (~54 tokens)

List activity log entries (tasks, notes, email sent, booking created, etc.) across the workspace. Example: list_activities({}).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_activity` (~308 tokens)

Append an activity-log entry to a contact (and/or deal). Use this instead of stuffing agent reminders into contacts.notes — notes gets overwritten on updates; activities are append-only. Valid types: task, note, email, sms, call, meeting, stage_change, payment, review_request, agent_action. Example: create_activity({ contact_id: 'ctc_...', type: 'agent_action', subject: 'Speed-to-Lead agent booked consult', body: 'Scheduled for 2026-05-01' })

Input parameters:

- `body` (string): Optional multi-line detail (≤4000 chars).
- `completed_at` (string): Optional ISO timestamp if logging a completed past action.
- `contact_id` (string): Contact to log against. Either contact_id or deal_id is required.
- `deal_id` (string): Deal to log against. Either contact_id or deal_id is required.
- `metadata` (object): Optional JSON metadata — e.g., { agentId: 'agt_...', confidence: 0.87 }
- `scheduled_at` (string): Optional ISO timestamp if the activity is planned for a future time (e.g., a task).
- `subject` (string): One-line title (≤200 chars). Either subject or body is required.
- `type` (string, required): task | note | email | sms | call | meeting | stage_change | payment | review_request | agent_action
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_bookings` (~199 tokens)

List scheduled bookings (not appointment-type templates — see list_appointment_types for those). Supports filtering by contact, status, and date range. Default sort: most-recent-first; if `from` is set, switches to earliest-upcoming-first for reminder flows. Example: list_bookings({ from: '2026-04-22T00:00:00Z', limit: 20 })

Input parameters:

- `contact_id` (string): Optional. Filter to a specific contact's bookings.
- `from` (string): Optional ISO timestamp. Only bookings starting at or after this moment.
- `limit` (number): Max rows (default 50, max 200).
- `status` (string): Optional. Filter by status (scheduled | completed | cancelled | no_show).
- `to` (string): Optional ISO timestamp. Only bookings starting at or before this moment.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_coupon` (~391 tokens)

Create a Stripe coupon + matching per-contact redeemable promotion code on the workspace's connected Stripe account. Use for Win-Back / retention agents that need UNIQUE codes per recipient (shared codes are vulnerable to abuse + lose attribution signal). Default max_redemptions=1 + auto-generated code string. Requires the workspace to have completed Stripe Connect onboarding. Example: create_coupon({ percent_off: 20, duration: 'once', name: 'Win-Back 20% off' })

Input parameters:

- `amount_off` (number): Flat discount in the currency's major unit (e.g., 25.00 for $25 off). Either percent_off or amount_off is required.
- `code` (string): Optional fixed redeemable code string. If omitted, Stripe auto-generates one.
- `currency` (string): Only used with amount_off. 3-letter ISO code. Defaults to usd.
- `duration` (string): 'once' (default) | 'forever' | 'repeating'. 'repeating' requires duration_in_months.
- `duration_in_months` (number): Required when duration='repeating'.
- `expires_at` (string): Optional ISO timestamp. Code becomes invalid after this moment. Prefer expires_in_days for agent archetypes.
- `expires_in_days` (number): Relative expiry: code becomes invalid N days after this call fires (1–365). Preferred over expires_at for agent archetypes so the window stays meaningful no matter when the agent was last deployed.
- `max_redemptions` (number): Max total redemptions. Default 1 — per-contact unique code.
- `name` (string): Optional display name for the coupon (≤60 chars).
- `percent_off` (number): Discount percentage (0 < n ≤ 100). Either percent_off or amount_off is required.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_booking` (~235 tokens)

Schedule a real booking against an existing appointment type. Looks up the template by id, creates a scheduled row on the workspace calendar, stamps the contact's name + email, emits booking.created, and — if the appointment type has a price > 0 — returns a Stripe Checkout URL routed to the SMB's connected Stripe account so the builder / agent can text or email the payment link to the contact. Example: create_booking({ contact_id: 'ctc_...', appointment_type_id: 'appt_...', starts_at: '2026-05-01T15:00:00Z' })

Input parameters:

- `appointment_type_id` (string, required): Required. Appointment-type template id from list_appointment_types.
- `contact_id` (string, required): Required. CRM contact being booked.
- `notes` (string): Optional free-form booking notes.
- `starts_at` (string, required): Required. ISO 8601 timestamp for the appointment start (e.g. 2026-05-01T15:00:00Z). Duration is read from the appointment type.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_booking` (~112 tokens)

Fetch one scheduled booking by id. Returns the full detail (contact, times, status, notes, meeting URL, cancellation timestamp, metadata). Appointment-type templates are NOT returned here — use list_appointment_types for those. 404s if the id is unknown OR belongs to a different workspace. Example: get_booking({ booking_id: 'bkg_...' }).

Input parameters:

- `booking_id` (string, required): Required. UUID of the booking.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `cancel_booking` (~159 tokens)

Cancel a scheduled booking. Sets status to 'cancelled', stamps cancelledAt, deletes the Google Calendar event, and emits booking.cancelled. Idempotent — re-cancelling an already-cancelled booking is a 200 no-op with alreadyCancelled=true (no duplicate events, no calendar errors). Past-dated bookings CAN be cancelled (legitimate retroactive cleanup). Does NOT touch linked payments — linkedPaymentIds is returned so the agent can compose refund_payment explicitly if the business rule is 'cancel AND refund'. Example: cancel_booking({ booking_id: 'bkg_...' }).

Input parameters:

- `booking_id` (string, required): Required. UUID of the booking to cancel.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `reschedule_booking` (~231 tokens)

Move a scheduled booking to a new starts_at. Preserves the original duration — endsAt tracks the move so a 30-min consult stays 30 mins at the new time. Updates the Google Calendar event in place (event id preserved; attendees see the time change on their existing invite) and emits booking.rescheduled with both previousStartsAt and newStartsAt so follow-up agents can describe the change. Rejects past-dated new starts_at (400) and refuses to reschedule a cancelled booking (422 — reviving a cancellation should be a new create_booking). Does NOT change appointment type; does NOT touch linked payments. Example: reschedule_booking({ booking_id: 'bkg_...', starts_at: '2026-05-02T15:00:00Z' }).

Input parameters:

- `booking_id` (string, required): Required. UUID of the booking to move.
- `starts_at` (string, required): Required. New ISO 8601 timestamp. Must be in the future. Duration is preserved from the current booking.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_appointment_types` (~48 tokens)

List all appointment types (bookable templates) in the workspace. Example: list_appointment_types({}).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_appointment_type` (~262 tokens)

Create a new appointment type with its own public /book/<slug> URL. Defaults availability to Mon–Fri 9am–5pm (edit on /bookings to change). Example: create_appointment_type({ title: 'Strategy call', duration_minutes: 45, price: 150 }).

Input parameters:

- `booking_slug` (string): Optional. URL-safe slug. Auto-derived from title if omitted.
- `buffer_after_minutes` (number): Optional. 0–120. Defaults to 0.
- `buffer_before_minutes` (number): Optional. 0–120. Defaults to 0.
- `description` (string): Optional. Up to 800 chars. Shown on the public booking page.
- `duration_minutes` (number): Optional. 5–240. Defaults to 30.
- `max_bookings_per_day` (number): Optional. Hard daily cap (1–100). Omit for unlimited.
- `price` (number): Optional. Defaults to 0 (free). Non-zero prices route through Stripe checkout on submit (requires Stripe connected).
- `title` (string, required): Required. Human-readable name, e.g., 'Strategy call'.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `update_appointment_type` (~220 tokens)

Update an existing appointment type. Partial — omit fields to keep them. Example: update_appointment_type({ booking_slug: 'default', duration_minutes: 60, price: 200 }). Pass booking_slug='default' to edit the auto-seeded 'Book a call' template.

Input parameters:

- `booking_slug` (string, required): Slug of the appointment type. Use 'default' for the auto-seeded template.
- `buffer_after_minutes` (number): Optional. 0–120.
- `buffer_before_minutes` (number): Optional. 0–120.
- `description` (string): Optional new description (≤800 chars). Empty string clears it.
- `duration_minutes` (number): Optional new duration (5–240).
- `max_bookings_per_day` (number): Optional. 1–100. Pass null to remove cap.
- `price` (number): Optional new price. 0 = free.
- `title` (string): Optional new title.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `configure_booking` (~92 tokens)

DEPRECATED alias for update_appointment_type({ booking_slug: 'default', ... }). Kept so existing Claude Code sessions don't break. Prefer update_appointment_type for new scripts.

Input parameters:

- `description` (string): Optional description.
- `duration_minutes` (number): Optional new duration in minutes.
- `title` (string): Optional new title.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_forms` (~38 tokens)

List intake forms in the workspace. Example: list_forms({}).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_form` (~79 tokens)

Fetch one form by id or slug. Example: get_form({ form: 'contact' }) or get_form({ form: 'uuid…' }).

Input parameters:

- `form` (string, required): Form id (uuid) or slug (e.g., 'contact', 'intake').
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_form` (~246 tokens)

Create a new intake form. Pass template_id to pre-fill fields from a built-in template (contact, lead-qualification, booking-request, nps-feedback, event-registration, blank). Example: create_form({ template_id: 'contact' }) → uses 'Contact us' template. Or pass explicit fields: create_form({ name: 'Intake', fields: [{ key: 'email', label: 'Email', type: 'email', required: true }] }).

Input parameters:

- `fields` (array): Optional field list. Overrides template fields. Each: { key, label, type ('text'|'email'|'tel'|'textarea'|'select'), required, options? }.
- `is_active` (boolean): Optional. Defaults to true.
- `name` (string): Optional. Falls back to template name or 'New intake form'.
- `slug` (string): Optional URL-safe slug. Falls back to template defaultSlug or slugified name.
- `template_id` (string): Optional. One of: blank, contact, lead-qualification, booking-request, nps-feedback, event-registration.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `update_form` (~141 tokens)

Update a form. Partial — omit fields to keep them. Replacing `fields` replaces the whole array (each field: { key, label, type, required, options? }). Example: update_form({ form: 'intake', fields: [...] }).

Input parameters:

- `fields` (array): Optional new field array. Whole replacement.
- `form` (string, required): Form id (uuid) or slug.
- `is_active` (boolean): Optional. Toggle publish state.
- `name` (string): Optional new name.
- `slug` (string): Optional new slug (URL-safe).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `delete_form` (~78 tokens)

Delete a form. Irreversible. Submissions are NOT deleted (form_submissions has ON DELETE SET NULL on form_id). Example: delete_form({ form: 'old-survey' }).

Input parameters:

- `form` (string, required): Form id (uuid) or slug.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_submissions` (~78 tokens)

List submissions for a form. Example: list_submissions({ form_id: 'uuid…' }).

Input parameters:

- `form_id` (string, required): UUID of the form. Slug lookup not supported on this endpoint — use get_form first if you only have the slug.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `customize_intake_form` (~92 tokens)

DEPRECATED alias for update_form({ form: 'intake', fields }). Only edits the auto-seeded default form; prefer update_form for new scripts so you can target any form in the workspace.

Input parameters:

- `fields` (array): Replacement field list.
- `form_name` (string): Optional new display name for the default form.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `send_email` (~173 tokens)

Send a one-off email through the workspace's configured provider (Resend by default). Checks the suppression list before sending and skips with {suppressed: true} if the recipient has opted out. Example: send_email({ to: 'alex@acme.com', subject: 'Welcome', body: 'Thanks for signing up', contact_id: 'ctc_123' })

Input parameters:

- `body` (string, required): Plain-text body — rendered into the default HTML shell.
- `contact_id` (string): Optional. Links the email to a CRM contact for threading.
- `provider` (string): Optional. Force a specific provider (default: resend).
- `subject` (string, required): Email subject line.
- `to` (string, required): Recipient email address.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_emails` (~64 tokens)

List recent emails sent from the workspace, newest first. Useful for checking delivery status before following up.

Input parameters:

- `limit` (number): Max rows to return (default 50, max 200).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_email` (~63 tokens)

Fetch a single email with its full provider-event history (sent / delivered / opened / clicked / bounced).

Input parameters:

- `email_id` (string, required): Email ID returned from send_email or list_emails.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_suppressions` (~50 tokens)

List all suppressed email addresses for the workspace — who is opted out and why (manual / unsubscribe / bounce / complaint).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `suppress_email` (~102 tokens)

Add an email address to the workspace suppression list so future sends skip it. Use for manual unsubscribes or policy blocks.

Input parameters:

- `email` (string, required): Email address to suppress.
- `reason` (string): Reason code: 'manual' | 'unsubscribe' | 'bounce' | 'complaint'. Default: 'manual'.
- `source` (string): Optional free-form provenance tag.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `unsuppress_email` (~54 tokens)

Remove an email address from the workspace suppression list so future sends go through again.

Input parameters:

- `email` (string, required): Email address to un-suppress.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `send_sms` (~168 tokens)

Send an SMS via the workspace's Twilio integration. Checks the SMS suppression list first (STOP keyword + carrier blocks + manual opt-outs) and skips with {suppressed: true} if the recipient has opted out. Example: send_sms({ to: '+15551234567', body: 'Your appointment is confirmed for Tuesday 2pm', contact_id: 'ctc_123' })

Input parameters:

- `body` (string, required): SMS body. Twilio will segment if over 160 chars; charges per segment.
- `contact_id` (string): Optional. Links the message to a CRM contact for threading.
- `to` (string, required): Recipient phone number. E.164 or 10-digit US will be normalized.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_sms` (~60 tokens)

List recent SMS messages (inbound + outbound) for the workspace, newest first.

Input parameters:

- `limit` (number): Max rows to return (default 50, max 200).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_sms` (~64 tokens)

Fetch a single SMS with its full provider-event history (queued / sent / delivered / failed / undelivered).

Input parameters:

- `sms_id` (string, required): SMS ID returned from send_sms or list_sms.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_sms_suppressions` (~53 tokens)

List all suppressed phone numbers for the workspace — who is opted out and why (manual / stop_keyword / carrier_block / complaint).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `suppress_phone` (~131 tokens)

Add a phone number to the SMS suppression list so future SMS sends skip it. STOP replies + carrier permanent-failure codes auto-suppress via the Twilio webhook; use this for manual opt-outs.

Input parameters:

- `phone` (string, required): Phone number to suppress. E.164 or 10-digit US will be normalized.
- `reason` (string): Reason code: 'manual' | 'stop_keyword' | 'carrier_block' | 'complaint'. Default: 'manual'.
- `source` (string): Optional free-form provenance tag.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `unsuppress_phone` (~54 tokens)

Remove a phone number from the SMS suppression list so future sends go through again.

Input parameters:

- `phone` (string, required): Phone number to un-suppress.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_invoice` (~193 tokens)

Draft a Stripe invoice on the workspace's connected Stripe account. Invoice is created but not sent — call send_invoice separately so agents can review before dispatch. Contact must have an email. Example: create_invoice({ contact_id: 'ctc_123', items: [{ description: '1 hr consulting', quantity: 1, unit_amount: 200 }], due_at: '2026-05-21T00:00:00Z' })

Input parameters:

- `contact_id` (string, required): CRM contact to bill.
- `currency` (string): 3-letter ISO currency code. Defaults to USD.
- `due_at` (string): ISO timestamp for invoice due date. Defaults to 30 days out.
- `items` (array, required): Line items. Each: {description, quantity, unit_amount} (unit_amount in the workspace's currency).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_invoices` (~61 tokens)

List workspace invoices (draft + sent + paid + past_due + voided), newest first.

Input parameters:

- `limit` (number): Max rows (default 50, max 200).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_invoice` (~57 tokens)

Fetch an invoice + its line items + hosted invoice URL (for payment).

Input parameters:

- `invoice_id` (string, required): Invoice ID returned from create_invoice or list_invoices.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `send_invoice` (~56 tokens)

Dispatch a draft invoice to the contact via Stripe (Stripe emails the invoice + provides a hosted pay page).

Input parameters:

- `invoice_id` (string, required): Invoice to send.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `void_invoice` (~59 tokens)

Void an invoice (undo a billing error). Only valid for draft / open invoices; paid invoices must be refunded instead.

Input parameters:

- `invoice_id` (string, required): Invoice to void.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_subscription` (~141 tokens)

Start a recurring subscription for a contact against a Stripe Price id. The Price must already exist in the workspace's Stripe dashboard — v1 does not create Prices. Example: create_subscription({ contact_id: 'ctc_123', price_id: 'price_1ABCxyz', trial_days: 14 })

Input parameters:

- `contact_id` (string, required): CRM contact to subscribe.
- `price_id` (string, required): Stripe Price id (e.g., 'price_1ABC...') from the workspace's Stripe dashboard.
- `trial_days` (number): Optional free trial days before first charge.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_subscriptions` (~59 tokens)

List workspace subscriptions (active + trialing + past_due + canceled), newest first.

Input parameters:

- `limit` (number): Max rows (default 50, max 200).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `cancel_subscription` (~84 tokens)

Cancel a subscription. Default: cancel at period end (contact keeps access until renewal date). Pass immediate=true for an instant termination + prorated refund.

Input parameters:

- `immediate` (boolean): If true, terminate now. Default: cancel at period end.
- `subscription_id` (string, required): Subscription to cancel.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_payments` (~61 tokens)

List recent payments (completed + failed + refunded + disputed) across the workspace, newest first.

Input parameters:

- `limit` (number): Max rows (default 50, max 200).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_payment` (~51 tokens)

Fetch a single payment record with status + refund/dispute state.

Input parameters:

- `payment_id` (string, required): Payment ID from list_payments.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `refund_payment` (~124 tokens)

Refund a payment. Omit amount to refund the full payment; pass amount for a partial refund. reason should be 'duplicate' | 'fraudulent' | 'requested_by_customer'.

Input parameters:

- `amount` (number): Optional partial-refund amount in the payment's currency. Omit to refund in full.
- `payment_id` (string, required): Payment to refund.
- `reason` (string): 'duplicate' | 'fraudulent' | 'requested_by_customer'. Default: requested_by_customer.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_landing_pages` (~59 tokens)

List the workspace's landing pages (draft + published), newest-updated first.

Input parameters:

- `limit` (number): Max rows (default 50, max 200).
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_landing_page` (~56 tokens)

Fetch a single landing page with its full Puck payload + metadata.

Input parameters:

- `page_id` (string, required): Landing page ID from list_landing_pages.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `update_landing_page` (~101 tokens)

Update a landing page's title and/or Puck payload. Validates puck_data on the way through. Does not change publish status — use publish_landing_page for that.

Input parameters:

- `page_id` (string, required): Landing page to update.
- `puck_data` (object): Optional new Puck payload. Pass null to clear.
- `title` (string): Optional new title.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `publish_landing_page` (~86 tokens)

Flip a landing page between draft and published. Publishing busts the public-URL cache immediately and emits landing.published. Pass published=false to unpublish.

Input parameters:

- `page_id` (string, required): Landing page to publish.
- `published` (boolean): true = publish (default), false = unpublish.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `list_landing_templates` (~62 tokens)

List the pre-built vertical landing-page templates. Each has a validated Puck payload ready to seed a new page via create_landing_page({puck_data: template.payload}).

Input parameters:

- `workspace_id` (string): Optional. Falls back to the active workspace.

### `get_landing_template` (~68 tokens)

Fetch a single landing-page template including its Puck payload. Pair with create_landing_page to seed a new page from the template.

Input parameters:

- `template_id` (string, required): Template ID from list_landing_templates.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `generate_landing_page` (~138 tokens)

Generate a Puck landing-page payload from a natural-language prompt using Claude + the workspace's Soul + theme. Returns the payload (validated against the Puck schema) but does NOT persist — pair with create_landing_page to save the result. Example: generate_landing_page({ prompt: 'A landing for a Laval dental clinic, focus on new-patient consultations' })

Input parameters:

- `existing` (object): Optional existing Puck payload to revise rather than start fresh.
- `prompt` (string, required): One-sentence page description. The more specific, the better.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `send_conversation_turn` (~202 tokens)

Route an incoming message through the Conversation Primitive runtime. Loads prior turns for (contact, channel), generates a Soul-aware reply with Claude, writes both inbound + outbound turns, and emits conversation.turn.received / sent events. Use when building an always-on conversational agent (speed-to-lead, qualification chatbot). Example: send_conversation_turn({ contact_id: 'ctc_123', channel: 'sms', message: 'Do you have Saturday appointments?' })

Input parameters:

- `channel` (string, required): Transport channel: 'email' | 'sms'.
- `contact_id` (string, required): CRM contact to converse with.
- `conversation_id` (string): Optional existing conversation id. Omit to let the runtime reuse the most recent active thread or open a new one.
- `message` (string, required): Incoming message content to reason about.
- `subject` (string): Optional subject for email threads.
- `workspace_id` (string): Optional. Falls back to the active workspace.

### `create_workspace_v2` (~504 tokens)

PREFERRED for new workspaces (v1.4+). MCP-native workspace creation: bootstraps the workspace via the v1 orchestrator (CRM, booking, intake, theme, pipeline) AND returns a list of v2 page blocks the IDE agent will now generate using its own LLM. Flow: 1) call this tool with the operator's business info; 2) for each block in `v2.recommended_blocks`, call get_block_skill(name) and use your LLM to generate props matching the SKILL.md prompt + schema; 3) call persist_block({ workspace_id, block_name, generation_prompt, props }) for each; 4) call complete_workspace_v2({ workspace_id }). MANDATORY FOLLOW-UP: After this returns `status: 'ready'` AND after all blocks land via persist_block + complete_workspace_v2, ask the operator verbatim 'What email should I use for your account?' Then call finalize_workspace({ workspace_id, email }). The admin dashboard URL is created by finalize_workspace, not here. Why v2: v1 generated all copy server-side from a hardcoded personality system, which produced layer-mismatch bugs every time a new niche was tested. v2 puts the LLM in your context (the IDE agent), reads from one SKILL.md per block, and the generated copy is naturally niche-aware. The operator can later say 'change the hero' and you customize it via persist_block with a customization payload.

Input parameters:

- `address` (string): Optional business address.
- `business_description` (string, required): One paragraph describing the business — drives the personality classifier and feeds into block prompts.
- `business_name` (string, required): Business display name.
- `certifications` (array)
- `city` (string, required): Operator's city. Drives timezone inference.
- `email` (string): Optional contact email surfaced in the landing footer (NOT the operator's account email).
- `emergency_service` (boolean)
- `phone` (string, required): Business phone, any format.
- `review_count` (number): Optional — number of reviews.
- `review_rating` (number): Optional — average star rating.
- `same_day` (boolean)
- `service_area` (array)
- `services` (array, required): Services / offerings the business provides — each as a plain string.
- `state` (string, required): US state code or full name (or Canadian province).
- `trust_signals` (array)

### `list_blocks` (~75 tokens)

List all v2 page-block primitives available in this SF backend. Returns each block's name, version, section type, one-line description, and the URL where its full SKILL.md (the generation prompt + prop schema) lives. Use this when you need to discover what blocks exist; for actual block content use get_block_skill.

### `get_block_skill` (~122 tokens)

Fetch the SKILL.md (the full generation prompt + prop schema + voice rules + worked examples + validator definitions) for one v2 page block. Returns raw markdown text. Read it carefully BEFORE generating props — the prop schema in the YAML frontmatter is enforced by the persist_block endpoint, and the validators run on every save. Generation that ignores the SKILL.md will fail validation and the operator will see worse output.

Input parameters:

- `block_name` (string, required): Block name. Use list_blocks to discover. As of v1.4: hero, services, faq.

### `persist_block` (~355 tokens)

Persist a v2 block instance. Call this after you've read the block's SKILL.md and generated props matching its schema. The server validates props (Zod schema + deterministic copy-quality validators), renders the block via the existing renderer, replaces the matching section in the workspace's landing page, and returns the public URL where the change is now visible. For initial generation, omit `customization`. For operator-driven edits ('make the hero warmer', 'add a card about kids cuts'), pass `customization: { prompt }` — the operator's prompt is appended to the row's customization history (forever-frozen rule), and the new props replace the previous render. Returns `validation_errors` on failure — if you see them, regenerate the props with the rules from SKILL.md applied more carefully and retry. Don't show validation errors to the operator; they're for you.

Input parameters:

- `block_name` (string, required): Block name (must match a get_block_skill name): hero, services, or faq.
- `customization` (object): Optional operator-customization layer. When set, append-only override of the initial generation. Use this when the operator says 'change X about my hero' rather than 'rewrite my hero'.
- `generation_prompt` (string, required): The full prompt your LLM consumed to produce these props. Stored as the source of truth for re-renders. Include the workspace context (business name, services, etc.) — not just the SKILL.md body.
- `props` (object, required): Block props matching the prop schema in the block's SKILL.md frontmatter. Validated server-side; mismatches return 422 with structured validation_errors.
- `workspace_id` (string, required): Workspace id from create_workspace_v2.

### `complete_workspace_v2` (~89 tokens)

Mark the v2 flow finished for a workspace. Returns which blocks landed vs. were skipped (skipped ones still render via the v1 default pipeline), plus the next steps. Call after every recommended_block has been persisted via persist_block. The operator-facing summary (admin URL, etc.) still requires finalize_workspace afterward.

Input parameters:

- `workspace_id` (string, required): Workspace id from create_workspace_v2.

### `regenerate_block` (~384 tokens)

Get the bundle needed to regenerate ONE v2 page block with new operator instructions. Use this when the operator asks for a targeted change to an existing block ('make the hero punchier', 'add a card about kids cuts', 'rewrite the FAQ to be less salesy'). Returns: current_props (so your LLM can iterate rather than start fresh), workspace_summary (business name, industry, services, voice from the workspace's soul), brain_patterns (anonymized cross-workspace patterns for this vertical), customization_history (previous edits — useful for understanding what NOT to revert), and the operator's new_instructions (echoed back so they're visible in your context). The next move is YOURS: fetch the block's SKILL.md via get_block_skill, generate new props that satisfy the prop schema while applying new_instructions, then call persist_block with `customization: { prompt: <new_instructions> }` to record the change. If the block has never been persisted (status=first_generation), this is a normal first-time generation path — same downstream flow, just no current_props to iterate from. Antifragile design note: this tool only ASSEMBLES context. Your LLM does the creative work. As models improve, regeneration quality improves with zero MCP changes.

Input parameters:

- `block_name` (string, required): Block name to regenerate. Must match a v2 block: hero, services, about, faq, cta, booking, intake.
- `new_instructions` (string): Optional: the operator's natural-language regeneration request ('make it more urgent', 'shorter copy', 'less salesy'). When provided, surfaced in the response and used in the customization field of t…
- `workspace_id` (string, required): Workspace id from create_workspace_v2.

### `get_landing_structure` (~207 tokens)

Read the workspace's landing-page section list with INDEX as the addressing primitive. Returns each section's index (0..N-1, top-to-bottom on the rendered page), type ('hero', 'services-grid', 'about', 'faq', 'mid-cta', 'trust-strip', 'footer', etc.), and a 1-line preview ('Vancouver's Trusted HVAC Family — Same-Day Service' for hero, '3 services (grid-3)' vs 'stats — 4 numbers' for services-grid duplicates). Use this BEFORE move_section / delete_section so you know which index to target. The preview disambiguates duplicate types (e.g. when a workspace has TWO services-grid sections — one with services, one with stats). v1.11+ replaces the v1.10 workflow where the agent had to fetch landing_pages.blueprintJson manually and parse it client-side. Cheap server-side (one DB read).

Input parameters:

- `workspace_id` (string, required): Workspace id.

### `move_section` (~253 tokens)

Move ONE landing-page section atomically. Identifies sections by INDEX (run get_landing_structure first to find which index to move). Splice semantics: the section at from_index is removed, then inserted at to_index in the resulting array — so to_index is the section's NEW position in the result. Examples: 'put hero below FAQ' → from_index=<hero index>, to_index=<faq's current index>. 'Move services to the top' → from_index=<services index>, to_index=0. Handles duplicate types correctly (the case reorder_landing_sections refused) — index identity is unambiguous even when two services-grid or two mid-cta sections exist. Use reorder_landing_sections instead when you want to express the entire new order at once AND types are unique. Use move_section for single-step moves OR when types repeat.

Input parameters:

- `from_index` (integer, required): 0-based index of the section to move (from the get_landing_structure response).
- `to_index` (integer, required): 0-based index where the section should END UP in the result. Splice semantics — equivalent to: remove from from_index, then insert at to_index.
- `workspace_id` (string, required): Workspace id.

### `delete_section` (~188 tokens)

Remove ONE landing-page section atomically. Identifies the section by INDEX (run get_landing_structure first). Refuses to leave 0 sections — minimum is 1 — so you can't accidentally wipe the page. Use when the workspace has a duplicate section type (e.g. two services-grid sections, one of which was an unintended generation artifact) and the operator wants the duplicate gone. Disambiguate WHICH duplicate via the preview text from get_landing_structure (e.g. 'stats — 4 numbers' vs '3 services (grid-3)'). For content edits, use update_landing_section. For replacing a section's content, use persist_block. delete_section is structural — it removes the section from the page entirely.

Input parameters:

- `index` (integer, required): 0-based index of the section to delete (from get_landing_structure response).
- `workspace_id` (string, required): Workspace id.

### `add_composite_section` (~516 tokens)

Add a CUSTOM landing-page section by composing low-level primitives (heading / text / image / list / button / card / row / col / stat / embed / divider / spacer) into a tree. Use this when the operator asks for a section type that doesn't fit hero/services/about/faq/mid-cta — e.g. 'a 2-column comparison of us vs DIY', 'a pricing tier section', 'a how-it-works in 4 steps', 'a stats row with 4 numbers', 'a side-by-side image + bullet-list'. The agent's job: read operator intent + the workspace soul (voice, services, brand) and emit a `tree` JSON object. Server validates (Zod schema, depth ≤ 4, children-per-container caps, heading-level descent), voice-scans against soul.voice.avoidWords (warnings, not errors), then renders + persists. Tree root MUST be kind=section. Leaves can include kind=embed with ref ∈ {services, faq, testimonials, hours, phone} to pull workspace-data into the section without re-typing it. Phone embed renders as a tel: link. Pattern library (typical compositions): COMPARISON = section { row{cols:2, [card{heading,list-check}, card{variant:muted, heading,list-x}]}}. STATS = section { row{cols:4, [stat,stat,stat,stat]}}. HOW-IT-WORKS = section { row{cols:4, [card{heading,text}, card{heading,text}, card{heading,text}, card{heading,text}]}}. SIDE-BY-SIDE = section { row{cols:2, [col{image}, col{heading,text,button}]}}. Returns the new section's index, the full sections list with previews, validation_warnings (voice violations the agent should fix on retry), and the public_url. Use position to insert at a specific index (default: append).

Input parameters:

- `position` (integer): Optional 0-based insert position. Default: appended at the end. Use get_landing_structure to find the right slot first.
- `tree` (object, required): Composite tree root. MUST be kind=section with optional eyebrow/headline/subhead and a children array. See the COMPOSITE_BLOCK_SKILL.md (fetch via get_block_skill('composite')) for the full primitive…
- `workspace_id` (string, required): Workspace id.

### `update_composite_section` (~233 tokens)

Replace the tree of an EXISTING composite section. Use when the operator asks to refine a custom section you previously created ('shorten the comparison', 'add another stat', 'make the cards muted'). Index must point at a section of type=composite — for typed sections (hero, services, faq, etc.) use update_landing_section, regenerate_block, or persist_block. First call get_landing_structure to find the right index — composite sections show preview text starting with 'composite — <headline>'. Then generate the new tree (typically by reading current_props equivalent — for composite sections this means fetching the existing tree, mutating, and submitting; today the simplest path is to regenerate from scratch using operator instructions + soul). Same validation + voice-scan as add_composite_section. Returns the same payload with index unchanged.

Input parameters:

- `index` (integer, required): 0-based index of the composite section to update (from get_landing_structure).
- `tree` (object, required): Replacement tree. Same shape as add_composite_section — kind=section root with children.
- `workspace_id` (string, required): Workspace id.

### `get_intake_structure` (~83 tokens)

Read the workspace's intake form: title, description, and the indexed list of fields with type + label + required + 1-line preview. Use this BEFORE add_intake_field / move_intake_field / delete_intake_field / update_intake_field to find the right index. Cheap one-DB-read.

Input parameters:

- `workspace_id` (string, required): Workspace id.

### `add_intake_field` (~239 tokens)

Add ONE field to the intake form. Field shape: { id, type, label, required?, helper?, options?, validation?, ratingScale? }. Types: text, textarea, email, phone, number, select, multi-select, rating, date. ID must be unique within the form (server rejects duplicates — IDs are the bind key for answers). For select/multi-select pass an `options` array. Use when the operator wants a new question on the intake form ('add a phone field', 'ask about budget', 'add a checkbox for newsletter signup'). For content edits to existing fields use update_intake_field. Position is optional — defaults to appending at the end. Use get_intake_structure first if you want to insert between specific fields.

Input parameters:

- `field` (object, required): The new IntakeQuestion object: { id (kebab-case, unique), type, label, required?, helper?, options?, ratingScale?, validation? }.
- `position` (integer): Optional 0-based insert index. Default: append. Use get_intake_structure to find the right slot.
- `workspace_id` (string, required): Workspace id.

### `move_intake_field` (~108 tokens)

Move ONE intake field to a new position. Splice semantics — field at from_index is removed, then inserted at to_index in the result. Use when the operator says 'put email at the top' or 'move phone above address'. Run get_intake_structure first to find the indices.

Input parameters:

- `from_index` (integer, required): 0-based source index.
- `to_index` (integer, required): 0-based target index in the result.
- `workspace_id` (string, required): Workspace id.

### `delete_intake_field` (~109 tokens)

Remove ONE intake field. Refuses to leave 0 fields (the public submit becomes meaningless without any inputs — minimum is 1). Use when the operator wants to remove a question from the form ('drop the property type field', 'remove the rating question'). For content edits use update_intake_field. Run get_intake_structure first to find the right index.

Input parameters:

- `index` (integer, required): 0-based index of the field to delete.
- `workspace_id` (string, required): Workspace id.

### `update_intake_field` (~171 tokens)

Patch ONE intake field by index. Patch can include any subset of: id, type, label, helper, required, options, ratingScale, validation, showIf. Only the fields you pass are changed; everything else stays. Use for content edits ('rename phone to mobile', 'make email optional', 'add a fourth option to property type', 'change the helper text'). ID changes must not collide with another field's id (server rejects). For structural changes (add/remove fields) use the dedicated tools.

Input parameters:

- `index` (integer, required): 0-based index of the field to patch.
- `patch` (object, required): Subset of IntakeQuestion fields to overwrite. Empty patch is rejected. ID changes are allowed but must not collide with another field.
- `workspace_id` (string, required): Workspace id.

### `get_booking_structure` (~101 tokens)

Read the workspace's booking event-type + fields (indexed list with type + label + required + 1-line preview). Standard fields (fullName at index 0, email at index 1) are flagged is_standard:true — they're server-owned and cannot be moved/deleted/renamed. Use BEFORE add_booking_field / move_booking_field / delete_booking_field / update_booking_field to find the right index.

Input parameters:

- `workspace_id` (string, required): Workspace id.

### `add_booking_field` (~216 tokens)

Add ONE field to the booking form (after the standard fullName + email). Field shape: { id, type, label, required?, placeholder?, options? }. Types: text, textarea, email, phone, select. Use when the operator wants to capture extra info from bookers — service address, equipment type, preferred technician, party size, etc. ID must be unique within the form (cannot be 'fullName' or 'email' — those are reserved). Position defaults to appending at the end. Insert positions must be >= 2 (slots 0/1 are reserved for the standards).

Input parameters:

- `field` (object, required): BookingFormField object: { id (kebab-case, unique, NOT 'fullName'/'email'), type, label, required?, placeholder?, options? for select fields }.
- `position` (integer): Optional 0-based insert position. Default: append. Must be >= 2 (0/1 are reserved for standards).
- `workspace_id` (string, required): Workspace id.

### `move_booking_field` (~113 tokens)

Move ONE booking field (extra) to a new position. Splice semantics. Standards (fullName, email at indices 0/1) cannot be moved AND cannot be displaced — both from_index and to_index must be >= 2.

Input parameters:

- `from_index` (integer, required): 0-based source index. Must be >= 2 (standards locked).
- `to_index` (integer, required): 0-based target index in the result. Must be >= 2.
- `workspace_id` (string, required): Workspace id.

### `delete_booking_field` (~100 tokens)

Remove ONE booking field (extra). Standards (fullName, email at indices 0/1) cannot be deleted — index must be >= 2. Floor is 'just the 2 standards' (different from intake's 'minimum 1' rule because booking forms always have 2 standards).

Input parameters:

- `index` (integer, required): 0-based index. Must be >= 2 (standards locked).
- `workspace_id` (string, required): Workspace id.

### `update_booking_field` (~144 tokens)

Patch ONE booking field (extra) by index. Patch can include any subset of: id, type, label, required, placeholder, options. Standards (fullName, email) cannot be patched — index must be >= 2. ID changes blocked from colliding with another field OR with reserved standard ids.

Input parameters:

- `index` (integer, required): 0-based index of the field to patch. Must be >= 2.
- `patch` (object, required): Subset of BookingFormField fields to overwrite. Empty patch is rejected. ID changes blocked from colliding (with another field or with reserved 'fullName'/'email').
- `workspace_id` (string, required): Workspace id.

### `get_portal_structure` (~80 tokens)

Read the workspace's portal template — indexed list of composite-tree sections with previews. Use BEFORE add_portal_section / update_portal_section / move_portal_section / delete_portal_section to find the right index. Empty templates are valid (the portal just shows built-in tabs without a Custom tab).

Input parameters:

- `workspace_id` (string, required): Workspace id.

### `add_portal_section` (~314 tokens)

Add a composite-tree section to the workspace's portal template. The template renders on every customer's portal — same composite primitive vocabulary as landing (12 node kinds), PLUS 5 customer.* embed refs that pull per-customer data: customer.contact_info (name + email + phone), customer.next_appointment (upcoming booking card), customer.recent_appointments (history list), customer.documents (download links), customer.deals (active jobs/deals). Read get_block_skill('composite') for the primitive vocabulary + voice rules. Tree root MUST be kind=section. Validation runs (Zod + structural rules + voice scan) same as add_composite_section. Typical patterns: WELCOME = section { headline: 'Welcome back', children: [text + customer.contact_info] }. NEXT-APPOINTMENT = section { headline: 'Your next visit', children: [embed: customer.next_appointment, button: book] }. DOCS = section { headline: 'Your documents', children: [embed: customer.documents] }. Position is optional — defaults to appending. Use get_portal_structure first if you want to insert between specific sections.

Input parameters:

- `position` (integer): Optional 0-based insert position. Default: append at the end.
- `tree` (object, required): Composite tree root (kind=section with optional eyebrow/headline/subhead and children). See get_block_skill('composite') for the primitive vocabulary. Customer.* embed refs are valid here.
- `workspace_id` (string, required): Workspace id.

### `update_portal_section` (~95 tokens)

Replace the tree of an existing portal-template section. Use to refine ('shorten the welcome', 'add a CTA to the documents section'). Index must exist. Validation runs same as add_portal_section.

Input parameters:

- `index` (integer, required): 0-based index of the portal section to replace.
- `tree` (object, required): Replacement tree. Same shape as add_portal_section.
- `workspace_id` (string, required): Workspace id.

### `move_portal_section` (~76 tokens)

Move ONE portal-template section atomically. Splice semantics: section at from_index removed, then inserted at to_index in the result.

Input parameters:

- `from_index` (integer, required): 0-based source index.
- `to_index` (integer, required): 0-based target index in the result.
- `workspace_id` (string, required): Workspace id.

### `delete_portal_section` (~72 tokens)

Remove ONE portal-template section. UNLIKE landing's delete_section, leaving 0 portal sections is valid — the portal just shows built-in tabs (Documents, Bookings) without a Custom tab.

Input parameters:

- `index` (integer, required): 0-based index to delete.
- `workspace_id` (string, required): Workspace id.

### `preview_portal` (~122 tokens)

Render the workspace's portal template against a SPECIFIC contact's data. Returns HTML + CSS so you can visually verify the template before customers see it. Pass contact_id of any contact in the workspace; if the id doesn't belong to this workspace, you get a 404. Use after add_portal_section / update_portal_section to confirm the per-customer embeds resolve correctly with real data.

Input parameters:

- `contact_id` (string, required): Contact id of an existing customer in this workspace. Use list_contacts to discover ids.
- `workspace_id` (string, required): Workspace id.

### `register_partner_agency` (~436 tokens)

Register a partner agency. Used by Scale-tier customers who resell SeldonFrame's Business OS to SMBs (HVAC contractors, dentists, lawyers, realtors) under their OWN brand. Once registered, the agency can attach client workspaces via attach_workspace_to_agency; those workspaces will show the agency's logo / colors / support links instead of SeldonFrame's. Plan gate: at least one workspace owned by the caller must be on Scale tier; otherwise the agency is created in 'pending' status and chrome substitution doesn't activate until the upgrade lands. Provide name (required) + slug (auto-derived from name if omitted). Optional: logo_url (uploaded image URL), primary_color / accent_color (hex like #5b21b6), support_email + support_url (where the agency's clients go for help — these REPLACE SeldonFrame's docs/Discord pointers in client chrome), hide_powered_by_badge (true to suppress the 'Powered by SeldonFrame' footer on clients' public pages — Scale-tier perk).

Input parameters:

- `accent_color` (string): Optional hex color like #a78bfa.
- `hide_powered_by_badge` (boolean): Hide the 'Powered by SeldonFrame' footer on the agency's clients' public landing pages. Scale-tier feature.
- `logo_url` (string): Optional logo URL (https://...). Use upload_workspace_image to host one if needed.
- `name` (string, required): Agency display name (e.g. 'Acme AI'). 2+ chars.
- `primary_color` (string): Optional hex color like #5b21b6.
- `slug` (string): Optional URL-safe slug. Default: derived from name. Must be unique among non-archived agencies.
- `support_email` (string): Optional. Where the agency's clients email for help.
- `support_url` (string): Optional. Where the agency's clients click for docs/help.
- `workspace_id` (string, required): Workspace id (any workspace owned by the caller — used to resolve the owning user for the new agency).

### `attach_workspace_to_agency` (~162 tokens)

Attach a workspace to a partner agency. The workspace's chrome (admin dashboard logo, public landing footer, customer portal branding) flips to the agency's brand. Caller must own BOTH the agency and the workspace. Agency must be in 'active' status (not pending — register first, upgrade if needed).

Input parameters:

- `agency_id` (string, required): Agency id from register_partner_agency.
- `target_workspace_id` (string, required): Workspace id of the workspace you want to attach to the agency. Often this is the SAME as workspace_id (the workspace running the MCP); for an agency operator with multiple client workspaces, this is…
- `workspace_id` (string, required): Workspace id (the bearer's workspace; also the workspace to attach).

### `detach_workspace_from_agency` (~79 tokens)

Detach a workspace from its current agency. Chrome falls back to SeldonFrame defaults on next render. Either the workspace owner OR the agency owner can detach.

Input parameters:

- `target_workspace_id` (string, required): Workspace id to detach (often the same as workspace_id).
- `workspace_id` (string, required): Workspace id (the bearer's workspace).

### `register_partner_agency_sender_domain` (~284 tokens)

Register a sender domain for a partner agency so the agency can send transactional emails (welcome, magic-link, portal-access-code) FROM their own domain instead of welcome@seldonframe.com. The SeldonFrame backend creates the domain in Resend (under our SF Resend account), and returns the DNS records (SPF, DKIM, MX) the agency must add at THEIR registrar. The agency does NOT need their own Resend account. The default sender_local_part is 'welcome' — final sender becomes welcome@<domain>. Override with sender_local_part='hello' to get hello@<domain>. After this call: the agency adds the DNS records, waits 5-60 min for propagation, then calls verify_partner_agency_sender_domain. Once Resend confirms verification, the agency's clients' transactional emails switch to the agency's sender automatically.

Input parameters:

- `agency_id` (string, required): Agency id from register_partner_agency.
- `domain` (string, required): Domain to send from (e.g. 'acmeai.com'). Without scheme. The agency must control DNS for this domain.
- `sender_local_part` (string): Optional local-part of the sender address (default: 'welcome'). Final sender becomes <local>@<domain>.
- `workspace_id` (string, required): Workspace id (the bearer's workspace; used to resolve owning user).

### `verify_partner_agency_sender_domain` (~124 tokens)

Trigger Resend's DNS verification for a partner agency's sender domain. Call this AFTER the agency has added the SPF/DKIM/MX records at their registrar. Returns the current verification status. When status flips to 'verified', the agency's verified_sender_at timestamp is set and chrome substitution kicks in for outbound emails on attached workspaces. Idempotent — safe to call repeatedly while DNS is propagating.

Input parameters:

- `agency_id` (string, required): Agency id from register_partner_agency.
- `workspace_id` (string, required): Workspace id (the bearer's workspace).

### `reorder_landing_sections` (~280 tokens)

Reorder the sections of a workspace's landing page WITHOUT changing their content. Use when the operator says 'move FAQ to the bottom', 'put services after the about section', 'rearrange so the CTA is below testimonials'. Pass `new_order` as the full ordered array of section types as they should appear top-to-bottom. The multiset of types in new_order MUST equal the current landing's section types — no add/remove. Section types include: hero, services-grid, about, mid-cta, faq, testimonials, trust-strip, emergency-strip, service-area, partners, footer (the actual set depends on what's currently on the page). Returns the new sections_order on success or validation_errors on failure (missing/extra types, duplicates). For content edits use update_landing_section. To regenerate a block's content use regenerate_block. To get the current order, fetch the workspace's landing or call regenerate_block (which exposes block names) — most landing pages start as: hero → services → about → faq → mid-cta.

Input parameters:

- `new_order` (array, required): Ordered array of section type strings. Must contain EVERY section type currently on the landing page, exactly once each. Example: ["hero", "services-grid", "about", "mid-cta", "faq"].
- `workspace_id` (string, required): Workspace id.

### `upload_workspace_image` (~711 tokens)

Upload an image to a workspace and apply it to one of two slots: 'logo' (replaces organizations.theme.logoUrl, surfaces in header / footer / og-image / favicon) or 'hero_background' (replaces the hero section's background image and re-renders the landing page). Use when the operator says 'use this as my logo', 'replace the hero image with this photo', 'change the header logo'. PICK ONE source — the others are mutually exclusive: (a) `image_url` (PREFERRED, v1.10.1+) — public HTTPS URL to the image. The SF backend fetches it directly. Best path for Cloudinary, Unsplash, S3, or any image already on the web. file_name + content_type are auto-derived from the URL — you don't need to pass them. (b) `local_file_path` (v1.10.1+) — absolute path on the operator's machine. The MCP server (running locally) reads the file and forwards bytes to the backend. Best path for files on the operator's desktop. file_name + content_type derived from the path. (c) `image_data_b64` (legacy v1.10.0) — image bytes base64-encoded. Use only when you've generated bytes yourself (e.g. dynamic image gen) and there's no URL or path. Be aware: the encoded string consumes your tool-call token budget; for files >~12 KB raw, prefer (a) or (b). Max 5 MB across all paths. Allowed types: image/png, image/jpeg, image/webp, image/svg+xml, image/gif. Returns the public Blob URL on success; that URL is now live on the workspace's public surface within seconds. Antifragile design: server only validates file shape + applies URL to the right column. Your LLM picks which slot ('they said logo, that maps to slot=logo'). As you get better at intent-mapping, the harness doesn't change.

Input parameters:

- `content_type` (string): Optional MIME type (auto-derived from image_url or local_file_path extension). REQUIRED with image_data_b64. Must be one of: image/png, image/jpeg, image/webp, image/svg+xml, image/gif.
- `file_name` (string): Optional filename (auto-derived from image_url or local_file_path). REQUIRED with image_data_b64.
- `image_data_b64` (string): Image bytes, base64-encoded. LEGACY path — prefer image_url or local_file_path because base64 consumes your tool-call token budget. Max 5 MB after decoding.
- `image_url` (string): Public HTTPS URL to the image. PREFERRED source. SF backend fetches directly (no base64 round-trip). file_name + content_type auto-derived. https:// only; loopback / private / link-local IPs rejected.
- `local_file_path` (string): Absolute path to a file on the operator's machine (the MCP server runs there). MCP reads the file and forwards bytes to the backend. file_name + content_type auto-derived from the path. Use when the…
- `slot` (string, required): Image slot: 'logo' (workspace logo, used in header/footer/og-image) or 'hero_background' (hero section background image). Other slots may be added in future versions.
- `workspace_id` (string, required): Workspace id.

### `read_brain_path` (~162 tokens)

Read a single brain note from the workspace's layer-1 brain. Returns the body (markdown), confidence (0-1), uses (times read), wins (times the consuming interaction was successful), and metadata. Reading a note increments its `uses` counter — that's how the feedback loop knows the note has been consumed. Use BEFORE generating blocks: check for relevant entries (voice/copy-that-works.md, customers/recurring.md, learnings.md) so your generation reflects what's been observed about this workspace.

Input parameters:

- `path` (string, required): Note path. Examples: voice/copy-that-works.md, customers/recurring.md, pipeline/closed-won-patterns.md, learnings.md.
- `workspace_id` (string, required): Workspace id.

### `list_brain_dir` (~123 tokens)

List brain notes in the workspace's layer-1 brain. Returns metadata + a 120-char body preview per note (full body requires read_brain_path). Use to discover what the brain knows about this workspace before generating blocks. Pass `prefix` to filter by directory (e.g. 'voice/' returns voice-related notes only). Notes are returned sorted by confidence descending.

Input parameters:

- `prefix` (string): Optional path prefix to filter (e.g. 'voice/', 'customers/'). Omit for all notes.
- `workspace_id` (string, required): Workspace id.

### `write_brain_note` (~257 tokens)

Write a brain note to the workspace's layer-1 brain. Use to capture insights the operator volunteers ('walk-ins on Saturday convert 3× better', 'don't ever say synergy in the copy', 'most leads come in via Instagram'). The note is REPLACED on subsequent writes to the same path; for append-style writes use `append: true`. Source field is recorded so the cron can attribute promotions correctly.

Input parameters:

- `append` (boolean): When true, prepend the body as a new dated paragraph to the existing note (preserves history). When false (default), replaces the existing body.
- `body` (string, required): Markdown body of the note. Concrete, specific, observation-based. Avoid generalities.
- `path` (string, required): Note path. Convention: <category>/<topic>.md. Examples: customers/recurring.md, voice/copy-that-works.md, learnings.md, ops/saturday-rush.md.
- `tags` (array): Optional tags for filtering.
- `type` (string): Optional note type for filtering: 'pattern' | 'fact' | 'preference' | 'warning' | 'playbook' | 'anti-pattern'.
- `workspace_id` (string, required): Workspace id.

### `connect_workspace` (~203 tokens)

Connect this device/IDE to an EXISTING SeldonFrame workspace via magic-link email. Use when the operator already has a workspace (e.g. created from another device) and wants to admin it from this Claude Code / Cursor / Windsurf session. Sends a confirmation email with a one-click approval link; the tool polls until approval (5-min timeout) then stores the workspace bearer locally. For brand-new workspaces, use create_workspace_v2 instead.

Input parameters:

- `device_label` (string): Optional human-readable label shown to the operator on the approval page so they can verify they're authorizing the right device. Defaults to a hostname-based label.
- `email` (string, required): Operator's email — must match an email associated with the workspace owner. Magic link is sent here.
- `workspace_slug` (string, required): Workspace slug (the subdomain prefix). Example: 'iron-oak-barbershop' for iron-oak-barbershop.app.seldonframe.com.

### `add_custom_domain` (~179 tokens)

Add a custom hostname (e.g. 'joescuts.com', 'www.joescuts.com', 'bookings.joescuts.com') to the workspace. PAID FEATURE — requires Growth ($29/mo) or Scale ($99/mo); returns 402 upgrade_required on free tier. Returns DNS instructions the operator needs to add at their registrar (Cloudflare, Namecheap, GoDaddy, etc.). Once DNS propagates (typically 5min - 24h), call verify_domain to mark verified + enable routing. Vercel auto-provisions SSL once DNS resolves.

Input parameters:

- `hostname` (string, required): Hostname to register, lowercased and without scheme. Examples: 'joescuts.com', 'www.joescuts.com', 'shop.joescuts.com'.
- `workspace_id` (string, required): Workspace id.

### `verify_domain` (~122 tokens)

Re-check DNS for a previously-added custom domain. Returns { verified: true } once Vercel sees the correct DNS record AND issues SSL — usually 5 minutes after the operator adds the CNAME / A record at their registrar, sometimes up to 24 hours depending on TTL. Returns { verified: false, recommended_records } when DNS still hasn't propagated; surface those recommendations to the operator so they can fix their registrar config.

Input parameters:

- `hostname` (string, required): The hostname to re-verify (must already be added).
- `workspace_id` (string, required): Workspace id.

### `list_workspace_domains` (~70 tokens)

List all custom domains registered to the workspace. Returns hostname, status (pending / verified / failed), DNS verification record, and primary flag for each. Allowed on all tiers — free workspaces will see an empty list since custom domains require a paid tier.

Input parameters:

- `workspace_id` (string, required): Workspace id.

### `remove_workspace_domain` (~82 tokens)

Remove a custom domain from the workspace. Routes immediately stop responding for the removed hostname; SSL cert is preserved on Vercel for 30 days in case the operator wants to re-add it. Idempotent — no error if the domain was already removed.

Input parameters:

- `hostname` (string, required): Hostname to remove.
- `workspace_id` (string, required): Workspace id.

### `list_brain_patterns` (~197 tokens)

List layer-2 cross-workspace patterns. These are anonymized insights the cron has promoted from workspaces that all observed the same thing (3+ workspaces, confidence >= 0.7). Use BEFORE generating blocks for a vertical-specific business — patterns/by-vertical/<vertical>.md gives you observations across every other workspace in that vertical. Compounding moat: each new workspace's interactions feed back into these patterns over time.

Input parameters:

- `block_type` (string): Optional block-type filter: 'hero' | 'services' | 'faq' | etc. Returns patterns/by-block-type/<type>/* notes only.
- `vertical` (string): Optional vertical filter: 'barbershop' | 'hvac' | 'legal' | 'restaurant' | etc. Returns patterns/by-vertical/<vertical>/* notes only.
- `workspace_id` (string, required): Workspace id (used for auth; the patterns themselves are global).

### `configure_llm_provider` (~422 tokens)

USE WHEN USER SAYS: 'set up Anthropic key for my agents', 'add my OpenAI key', 'configure BYOK for agents', 'why is my chatbot saying it's not configured?' FIRST-RUN setup BEFORE create_agent. Sets the LLM API key for this workspace's agents (BYOK — Bring Your Own Key). The OPERATOR pays the LLM provider directly (Anthropic / OpenAI / etc.); SF charges separately for agent platform usage. Stored encrypted at rest using the deployment's ENCRYPTION_KEY. Operators get keys from console.anthropic.com (recommended for v1.26.x — best tool-use support) or platform.openai.com. v1.28+ AUTO-DETECT: pass api_key='env' (or omit api_key entirely) and the MCP server will read process.env.ANTHROPIC_API_KEY / OPENAI_API_KEY from its own environment. Most Claude Code users already have this set (it's how Claude Code works), so this lets a solo SF client onboard with zero key-paste step. Returns { ok: false, error: 'no_env_key' } if the env var isn't set; in that case the user must paste the key explicitly. Skip if the workspace already has a key — agents fail-graceful with 'I'm not set up yet' if no key configured, so a 'not configured' chatbot response means CALL THIS TOOL.

Input parameters:

- `api_key` (string): API key. Anthropic keys start with 'sk-ant-...'. Stored encrypted; never echoed back. v1.28+ AUTO-DETECT: pass 'env' (literal string) or omit entirely to read process.env.{ANTHROPIC,OPENAI}_API_KEY f…
- `provider` (string, required): LLM provider. v1.26 ships full Anthropic support (tool use, streaming-ready). OpenAI support for chat is partial — recommend Anthropic for production agents.
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `create_agent` (~860 tokens)

USE WHEN USER SAYS: 'add a chatbot to my website', 'add an AI assistant to my landing page', 'put a chat widget on my site', 'create a website chatbot', 'add an AI agent that answers customer questions', 'I want chat on my homepage', 'build me a chatbot for [business]'. DON'T confuse with: list_blocks (chat widgets are NOT a block type — agents are a separate primitive); send_conversation_turn (that's for inbound SMS/email auto-reply, NOT a website widget). If the operator wants chat on their website, THIS is the tool. Creates a new agent for this workspace. Agents are conversational interfaces (web chat, voice, SMS) that answer FAQs, book appointments, and escalate to humans — composed from typed primitives + the workspace's Soul (industry, voice, services). WHAT GETS COMPOSED AUTOMATICALLY: persona derived from soul.industry + soul.voice; FAQ knowledge from your `faq` array; pricing facts from `pricing_facts` (validators block any $-amount the agent invents that's not in this list); typed tools (look_up_availability, book_appointment, find_my_existing_appointment, escalate_to_human, provide_faq_answer). WHAT YOU PROVIDE: name, archetype (website-chatbot for v1.26.x+; voice-receptionist + sms-followup-bot queued), channel (web_chat / voice / sms / email), inline FAQ pairs, allowed pricing facts, optional greeting. STATUS LIFECYCLE: created in 'draft' (not callable). Flip to 'test' to chat with it in sandbox. Flip to 'live' once you're confident — v1.26.2+ eval-gates 'live' until 8-scenario suite passes ≥87.5%. SAFETY: response validators run on every turn — quotes_only_from_soul_pricing (critical, blocks hallucinated $X), no_prompt_injection_echo (critical), no_pii_leak (critical), no_avoid_words (warning), response_length_under_cap (warning). Critical fail = agent says 'let me check + escalate' instead of sending the bad response. OUTPUT: the agent's embed URL (one-line <script> for the operator's website) and turn URL (POST endpoint for direct API integration).…

Input parameters:

- `archetype` (string, required): Agent shape. v1.26.x ships website-chatbot with full feature support; voice-receptionist + sms-followup-bot are queued for v1.27/v1.28.
- `capabilities` (array): Optional explicit subset of typed tools the agent may call. Default: all five (look_up_availability, book_appointment, find_my_existing_appointment, escalate_to_human, provide_faq_answer). Restrict i…
- `channel` (string, required): Delivery channel. v1.26.x ships web_chat only (embed.js bubble). Other channels queued.
- `faq` (array): Operator-curated FAQ pairs. Each item is { q: string, a: string }. The agent has these in its system prompt; visitors get answers without an LLM round-trip when the question is a clear match. v1.27 a…
- `greeting` (string): Optional first message shown when the chat opens (default: 'Hi! How can I help you today?').
- `name` (string, required): Agent display name (e.g. 'Cypress HVAC Assistant'). Surfaces in the chat header.
- `pricing_facts` (array): ONLY prices the agent may quote. Validators block any $-amount in the agent's response that's not in this list (or doesn't match exactly). If you want the agent to refuse all price questions, omit th…
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `list_agents` (~115 tokens)

USE WHEN USER SAYS: 'show me my agents', 'which chatbots do I have', 'list agents in this workspace', 'is the HVAC chatbot live yet?'. Lists all agents in the workspace with status (draft/test/live/paused), version, daily token usage vs budget, and metadata. Use to find an agent_id before calling publish_agent / update_agent_blueprint / get_agent_metrics, or to audit which agents are live across a workspace.

Input parameters:

- `workspace_id` (string, required): Workspace id (bearer workspace).

### `publish_agent` (~212 tokens)

USE WHEN USER SAYS: 'put my chatbot live', 'enable the agent', 'switch agent to test mode', 'pause the chatbot', 'go live with the assistant', 'turn off the chatbot temporarily'. Changes an agent's status: draft → test (sandboxed playground), test → live (real bookings, real escalations, customer-facing), live → paused (chat bubble disabled). EVAL GATE (v1.26.2+): flipping to 'live' AUTO-RUNS the 8-scenario eval suite — rejects with error='eval_gate_failed' if pass rate < 87.5%. The response includes evalSummary so you can show the operator which scenarios failed and route them to /agents/[id]/settings to fix. Use { force: true } to bypass (logged; SF emergencies only).

Input parameters:

- `agent_id` (string, required): Agent id from create_agent.
- `status` (string, required): Target status.
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `update_agent_blueprint` (~300 tokens)

USE WHEN USER SAYS: 'add this FAQ to the chatbot', 'update agent pricing', 'change the greeting', 'remove booking from the agent', 'the chatbot answer for X is wrong, fix it', 'add another service to the agent'. Updates an agent's blueprint (FAQ, pricing facts, greeting, capabilities). Bumps current_version + writes a new agent_versions row for rollback. The agent's status is unchanged — flip to test/live separately. PATCH SEMANTICS: arrays REPLACE (not merge). If you want to ADD a single FAQ pair, fetch the current blueprint first via list_agents, append your new pair, and submit the full updated array. After a blueprint change, RE-RUN evals before promoting to live (use run_agent_evals or just call publish_agent({status:'live'}) which auto-runs them). Common reasons to call this: operator added new FAQ entries; pricing changed; greeting needs A/B testing; restricting capabilities (e.g. removing book_appointment to make agent answer-only).

Input parameters:

- `agent_id` (string, required): Agent id from create_agent.
- `patch` (object, required): Partial blueprint patch. Fields: faq, pricing_facts, greeting, capabilities, archetype. Arrays REPLACE.
- `publish_notes` (string): Optional one-line note for the audit log (e.g. 'Added emergency-call FAQ').
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `get_workspace_state` (~294 tokens)

USE FIRST for any workspace task — replaces 4-6 separate discovery calls with one. Returns: workspace identity (name, slug, industry, timezone, dashboard URL); integrations status (anthropic / openai / twilio / resend / kit / mailchimp configured? — booleans only, no keys leaked); agents WITH inline health stats (status, version, eval pass rate, validator pass rate 24h, conversations 24h, eval gate met?, last eval run); high-level counts (contacts, bookings, deals, agents); and a next_steps array tailored to the workspace's current state (e.g. 'configure Anthropic key', 'no agents yet — call build_website_chatbot', 'agents need eval run before live'). USE WHEN USER SAYS: 'what's in this workspace', 'how is my chatbot doing', 'build me a chatbot for [biz]' (call FIRST so you know if an agent already exists + if LLM is configured), 'is my agent live yet', 'workspace status'. AVOIDS asking the user obvious questions like 'how should I configure the Anthropic key?' — the response.integrations.anthropic.configured tells you. Avoids creating a duplicate agent — response.agents tells you what already exists. Avoids a separate get_agent_metrics call — stats come inline.

Input parameters:

- `workspace_id` (string, required): Workspace id (bearer workspace).

### `build_website_chatbot` (~521 tokens)

USE WHEN USER SAYS: 'build me a chatbot for [business]', 'add a chatbot to my website', 'create a website chatbot', 'put a chat widget on my homepage', 'set up an AI assistant for my landing page'. ONE-CALL skill bundle that does the canonical chatbot setup end-to-end: (1) auto-configures the workspace's Anthropic LLM key from process.env.ANTHROPIC_API_KEY if no key is configured yet (most Claude Code users already have this set), or accepts an explicit anthropic_api_key arg; (2) creates a website-chatbot agent with the FAQ + pricing facts + greeting you provide; (3) publishes to status='test' so the operator can sandbox-test before going live (the eval gate runs only on 'live'); (4) returns the embed snippet, dashboard URL, and clear next-steps. USE THIS as the default for natural-language 'create a chatbot' requests. Fall back to the primitive tools (configure_llm_provider + create_agent + publish_agent) only when you need a custom flow (e.g. agency managing multiple operators with separate Anthropic billing — pass anthropic_api_key explicitly per workspace).

Input parameters:

- `anthropic_api_key` (string): Optional explicit Anthropic API key (sk-ant-...). If omitted, reads from process.env.ANTHROPIC_API_KEY in the MCP server's environment. Pass explicitly for white-label scenarios (different operator =…
- `faq` (array): Operator-curated FAQ pairs. Each item is { q, a }. Pass at least 3-5 covering the top customer questions: hours, service area, common issues, what to expect.
- `greeting` (string): First message shown when chat opens (~120 chars). E.g. 'Hi! Asking about HVAC service in Phoenix? I can book you in or answer common questions.' Default if omitted: 'Hi! How can I help you today?'
- `name` (string, required): Agent display name (e.g. 'Cypress & Pine HVAC Assistant'). Surfaces in chat header.
- `pricing_facts` (array): ONLY prices the agent may quote. Critical for safety — without this, agent refuses ALL price questions (safer default). With this, agent can quote ONLY listed amounts; anything else gets validator-bl…
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `update_website_chatbot` (~413 tokens)

USE WHEN USER SAYS: 'update the chatbot's FAQ', 'add new pricing to the agent', 'change the greeting', 'add a new service to the chatbot', 'the chatbot answer for X needs updating'. ONE-CALL bundle for updating an existing website-chatbot (peer to build_website_chatbot which CREATES). Looks up the workspace's website-chatbot agent (or accepts an explicit agent_id), merges your patch into the current blueprint, bumps version, returns refreshed embed_url + dashboard_url + version + next_steps. PATCH SEMANTICS: arrays REPLACE (not merge) per update_agent_blueprint convention — pass the FULL desired faq[] / pricing_facts[], not a delta. Greeting + capabilities are scalar replaces. If you want to ADD one FAQ pair, fetch current via get_workspace_state first and submit the full updated array. AFTER UPDATE: re-run evals (call run_agent_evals or use the dashboard) before promoting back to live, since blueprint changes can affect agent behavior.

Input parameters:

- `agent_id` (string): Agent id. Optional — if omitted, the bundle finds the workspace's first website-chatbot agent (most workspaces have one).
- `capabilities` (array): FULL desired capability list (REPLACES). Default 7-tool list for website-chatbot: look_up_availability, book_appointment, find_my_existing_appointment, reschedule_appointment, cancel_appointment, esc…
- `faq` (array): FULL desired FAQ list (REPLACES existing). Each item: { q, a }.
- `greeting` (string): New greeting text. Omit to keep current.
- `pricing_facts` (array): FULL desired pricing list (REPLACES existing). Each item: { label, amount, currency }.
- `publish_notes` (string): Optional one-line audit note (e.g. 'Added emergency-call FAQ').
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `embed_chatbot_on_workspace_landing` (~249 tokens)

USE WHEN USER SAYS: 'add the chatbot to my landing page', 'embed this on the website', 'put the agent on every page', 'make the chat bubble appear', 'wire the chatbot to the public site'. Wires a published agent's embed.js URL into the workspace's organization settings. The public page renderer (/s/ + /l/ routes) reads this on every render and injects a <script src='...' async></script> tag near </body>, so the floating chat bubble appears bottom-right on every page of the workspace's public surface — no per-section editing, no manual HTML, no Pages → Edit step. Agent must already be in status='test' or status='live' (call publish_agent first if it's still draft). One workspace = one chatbot at a time; calling this again with a different agent_id replaces the bubble. Pair with remove_chatbot_from_landing to clear it.

Input parameters:

- `agent_id` (string, required): Agent id from build_website_chatbot's response or list_agents. Must belong to the workspace and be in status=test or live.
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `remove_chatbot_from_landing` (~115 tokens)

USE WHEN USER SAYS: 'remove the chatbot from the landing', 'take the chat bubble off the page', 'unembed the chatbot', 'hide the chat from visitors'. Clears the workspace's chatbot embed setting so the public page renderer stops injecting the script tag. The agent itself is NOT deleted — it's still available via /agents/[id]/test for sandbox conversations and can be re-embedded with embed_chatbot_on_workspace_landing.

Input parameters:

- `workspace_id` (string, required): Workspace id (bearer workspace).

### `run_agent_evals` (~205 tokens)

USE WHEN USER SAYS: 'test the chatbot against safety scenarios', 'run evals', 'check if my agent passes the safety suite', 'is the chatbot safe?', 'why did my agent fail the publish gate?'. Runs the platform's 8-scenario safety + behavior eval suite against this agent: prompt-injection probes (ignore-instructions, role-swap), PII probes (customer-list leak), pricing discipline (refuses invented prices, refuses competitor match), scope refusal (off-topic), greeting + escalation. Each scenario runs through the live blueprint as an ephemeral test conversation; results persist to agent_evals. publish_agent({status:'live'}) AUTOMATICALLY runs this and gates on ≥87.5% pass. Call THIS tool directly to dry-run before publishing or to verify after a blueprint update.

Input parameters:

- `agent_id` (string, required): Agent id from list_agents / create_agent.
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `tail_agent_conversations` (~212 tokens)

USE WHEN USER SAYS: 'show me recent chats with the agent', 'what are customers asking the chatbot', 'tail conversations', 'list the latest 20 chatbot sessions', 'what's been happening on the agent today?'. Lists recent conversations for an agent — newest first. Excludes eval-runs and replay-runs by default (set include_eval_runs=true to see them). Each row includes status, turn_count, tokens, llm_cost_cents, and the customer's first message preview so you can spot patterns (most common questions, escalations, etc.) without opening each transcript. Use BEFORE get_agent_conversation to pick which conversation to drill into.

Input parameters:

- `agent_id` (string, required): Agent id from list_agents.
- `include_eval_runs` (boolean): Include eval/replay synthetic runs in the list (default false).
- `limit` (integer): How many conversations to return (default 20, max 100).
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `get_agent_conversation` (~197 tokens)

USE WHEN USER SAYS: 'show me that conversation in detail', 'why did the chatbot say X?', 'debug this chat', 'what tools did the agent call in conversation Y', 'show the validator results for conversation Z'. Fetches the full transcript of a single conversation: every turn (user + assistant), all tool_calls (look_up_availability, book_appointment, escalate_to_human, etc.) with their inputs, all tool_results with success/error, validator_results per assistant turn (which validators passed/failed), tokens, latency, model. Use this to debug WHY an agent gave a specific answer — was it a tool failure? a validator gating? wrong info in blueprint? Pair with replay_conversation to test a blueprint fix.

Input parameters:

- `conversation_id` (string, required): Conversation id (from tail_agent_conversations or admin /conversations page).
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `replay_conversation` (~138 tokens)

USE WHEN USER SAYS: 'will my new FAQ break the booking flow?', 'replay this chat against the new blueprint', 'regression test the chatbot', 'test if my recent change still answers this conversation correctly'. Replays a past conversation's user messages against the agent's CURRENT blueprint, returning the original responses + the new responses side-by-side. Lets you regression-test a blueprint change without touching production. Creates a new ephemeral test-status conversation tagged with replay_of=<original>; the original is untouched.

Input parameters:

- `conversation_id` (string, required): Original conversation id to replay.
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `get_agent_metrics` (~189 tokens)

USE WHEN USER SAYS: 'is my chatbot healthy?', 'how's the agent performing?', 'show me agent stats', 'what's my chatbot's pass rate this week?', 'agent dashboard ping'. Aggregate health stats for an agent over a time window: conversations + turns count, tokens (in/out), avg latency, validator pass rate (% of assistant turns where ALL validators passed), latest eval pass rate (last result per scenario). Use as a dashboard ping. If validator_pass_rate drops or eval_pass_rate falls below the 87.5% gate, the agent shouldn't be promoted to live.

Input parameters:

- `agent_id` (string, required): Agent id from list_agents.
- `since_hours` (integer): Time window in hours (default 24). Pass 168 for last 7 days, 720 for last 30 days.
- `workspace_id` (string, required): Workspace id (bearer workspace).

### `register_partner_agency` (~414 tokens)

Create a partner-agency entity for white-label SaaS reselling. STEP 1 of agency onboarding. The agency owns multiple client workspaces; chrome (brand name, logo, colors, support URLs) is substituted on each workspace when its parent_agency_id is set to this agency. Plan-gate: caller must own a workspace on the Scale tier ($99/mo). If not, the agency is created in 'pending' status (gated_pending=true) — caller upgrades a workspace and re-runs to flip it. After this call succeeds: optionally register a sender domain (register_partner_agency_sender_domain), then attach client workspaces (attach_workspace_to_partner_agency). Example: register_partner_agency({ name: "Acme Digital", primary_color: "#1FAE85", support_email: "help@acmedigital.com" })

Input parameters:

- `accent_color` (string): Optional brand accent color (hex). Used for secondary surfaces.
- `hide_powered_by_badge` (boolean): Hide the 'Powered by SeldonFrame' footer badge. Defaults to false. Scale tier feature.
- `logo_url` (string): Optional logo URL (publicly accessible PNG/SVG). Replaces SeldonFrame logo in chrome.
- `name` (string, required): Agency display name (2+ chars). Surfaces as the brand_name in operator-facing chrome.
- `primary_color` (string): Optional brand primary color (hex, e.g. '#1FAE85'). Cascades to UI components.
- `slug` (string): Optional URL-safe slug. Defaults to slugified name. Must be unique across all agencies.
- `support_email` (string): Optional support email shown in operator-facing chrome (e.g., 'help@acmedigital.com').
- `support_url` (string): Optional support URL shown in chrome (e.g., 'https://acmedigital.com/help').
- `workspace_id` (string): Workspace bearer that owns the agency. Falls back to default workspace if omitted.

### `register_partner_agency_sender_domain` (~264 tokens)

Register a custom email-sender domain for a partner-agency. STEP 2 of agency onboarding (optional but recommended — without it, agency emails ship from welcome@seldonframe.com). Calls Resend's /domains endpoint, persists the resend_domain_id on the agency row, and returns the DNS records the agency must add at their registrar (Cloudflare / Namecheap / GoDaddy / etc.). DNS propagation typically takes 5-60 minutes. After propagation, call verify_partner_agency_sender_domain to poll Resend + flip verified_sender_at. Requires: RESEND_API_KEY configured on the SeldonFrame backend. Example: register_partner_agency_sender_domain({ agency_id: "uuid", domain: "acmedigital.com", sender_local_part: "hello" })

Input parameters:

- `agency_id` (string, required): Agency id from register_partner_agency response.
- `domain` (string, required): Domain to register (e.g., 'acmedigital.com'). The agency's customer-facing emails ship from <local>@<domain> once verified.
- `sender_local_part` (string): Optional local part for the sender address. Defaults to 'welcome'. Final address: <local>@<domain>.
- `workspace_id` (string): Workspace bearer that owns the agency.

### `verify_partner_agency_sender_domain` (~159 tokens)

Poll Resend for sender-domain verification status. STEP 3 of agency onboarding. Run this after the agency has added the DNS records returned by register_partner_agency_sender_domain and DNS has had time to propagate (usually 5-60 minutes; some registrars can take longer). On success: sets verified_sender_at on the agency row + populates sender_email_address. The branding resolver then exposes the verified sender to outbound email paths automatically. Idempotent + safe to call repeatedly while waiting on DNS. Example: verify_partner_agency_sender_domain({ agency_id: "uuid" })

Input parameters:

- `agency_id` (string, required): Agency id from register_partner_agency response.
- `workspace_id` (string): Workspace bearer that owns the agency.

### `attach_workspace_to_partner_agency` (~197 tokens)

Attach a client workspace to a partner-agency so chrome substitution applies on that workspace's operator-facing surfaces. STEP 4 of agency onboarding — run once per client workspace the agency manages. Caller must own both the workspace AND the agency. Effect is immediate: next page load in that workspace renders the agency's brand_name, logo, colors, support URLs (replacing SeldonFrame's defaults). The workspace's own data (contacts, bookings, agents, etc.) is unchanged. Reversible via detach_workspace_from_partner_agency. Example: attach_workspace_to_partner_agency({ workspace_id: "phoenix-hvac-uuid", agency_id: "acme-digital-uuid" })

Input parameters:

- `agency_id` (string, required): Agency id the workspace will inherit chrome from.
- `workspace_id` (string, required): Client workspace id to attach. Caller's bearer workspace doesn't have to match — caller just has to own this workspace.

### `detach_workspace_from_partner_agency` (~140 tokens)

Detach a client workspace from its parent partner-agency. The workspace's chrome falls back to SeldonFrame defaults on next page load. Use when an agency loses a client or when the workspace transitions out of agency management. Caller must own the workspace OR own the agency the workspace is attached to (either authorizes detach). Workspace data is unchanged; only parent_agency_id is set to null. Reversible via attach_workspace_to_partner_agency. Example: detach_workspace_from_partner_agency({ workspace_id: "phoenix-hvac-uuid" })

Input parameters:

- `workspace_id` (string, required): Client workspace id to detach from its agency.

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/seldonframe-seldonframe-mcp/seldonframe-mcp#diagnostics

## Score history

- 2026-08-03: 59
- 2026-08-02: 59
- 2026-08-01: 18
- 2026-07-31: 5
- 2026-07-30: 25
- 2026-07-28: 24
- 2026-07-27: 43

## Links

- npm package: https://www.npmjs.com/package/@seldonframe/mcp
- Socket report: https://socket.dev/npm/package/@seldonframe/mcp
- Repository: https://github.com/seldonframe/seldonframe
- Website: https://seldonframe.com/
- Changelog RSS feed: https://verifymcp.io/servers/seldonframe-seldonframe-mcp/seldonframe-mcp/changelog.xml
- Changelog JSON feed: https://verifymcp.io/servers/seldonframe-seldonframe-mcp/seldonframe-mcp/changelog.json
- HTML version of this page: https://verifymcp.io/servers/seldonframe-seldonframe-mcp/seldonframe-mcp
