{"baseUrl":"https://plinth.tools","endpoints":{"mcp":"https://plinth.tools/mcp","rest":"https://plinth.tools/api/v1/call","toolsCatalog":"https://plinth.tools/api/v1/tools","bootstrap":"https://plinth.tools/api/v1/agent/bootstrap","health":"https://plinth.tools/api/health"},"mcpClientConfig":{"cursor":{"mcpServers":{"plinth":{"url":"https://plinth.tools/mcp","headers":{"Authorization":"Bearer ${PLINTH_API_KEY}"}}}},"claudeDesktop":{"mcpServers":{"plinth":{"command":"npx","args":["-y","@modelcontextprotocol/server-fetch","https://plinth.tools/mcp"],"env":{"PLINTH_API_KEY":"${PLINTH_API_KEY}"}}}}},"registrationFlow":[{"step":1,"tool":"onboarding.discover_organization","summary":"Check whether the work email domain can claim a workspace."},{"step":2,"tool":"onboarding.request_email_verification","summary":"Email a one-time verification code (purpose domain_claim)."},{"step":3,"tool":"onboarding.verify_email","summary":"Consume the inbox token."},{"step":4,"tool":"onboarding.register_workspace","summary":"Create the workspace (or request_access if already claimed)."},{"step":5,"tool":"onboarding.select_stack","summary":"Pick app modules and provider brands."},{"step":6,"tool":"provider_connections.connect_workspace","summary":"Seal shared Brevo / Resend keys once at workspace level (use connect per-app only for overrides)."},{"step":7,"tool":"mcp.create_app_connection","summary":"Mint an app + scoped API key (shown once)."},{"step":8,"tool":"apps.set_email_routing","summary":"Optional: round-robin / failover across email providers."},{"step":9,"tool":"regulation.apply_presets","summary":"Apply TOS rate-limit presets (also auto on connect)."},{"step":10,"tool":"workspace.whoami","summary":"Smoke-test the key before sending mail."}],"dailyOperations":[{"tool":"emails.send","summary":"Send transactional mail (html/text/body)."},{"tool":"emails.send_template","summary":"Send using a saved template key."},{"tool":"emails.ensure_template","summary":"Fetch-or-create a template (agent recovery primitive)."},{"tool":"emails.get_message_status","summary":"Delivery / engagement for one message id."},{"tool":"apps.get_email_routing","summary":"Read pool, strategy, failover settings."},{"tool":"regulation.preview","summary":"Remaining quota before a blast."},{"tool":"governance.export_audit_log","summary":"Export recent audited actions."}],"toolGroups":[{"id":"agent","label":"Agent bootstrap","tools":[{"name":"agent.get_bootstrap","description":"Return everything an autonomous agent needs to wire Plinth MCP: endpoint URLs, Cursor/Claude config snippets, the workspace registration flow, daily-operation shortcuts, and the full tool catalog grouped by vertical. Call this first when onboarding a new agent or CI job. Pass `baseUrl` only when the public origin differs from the deployment default. Requires a Bearer API key; unauthenticated callers should use GET /api/v1/agent/bootstrap for the static half."}]},{"id":"onboarding","label":"Onboarding & workspace","tools":[{"name":"onboarding.get_catalog","description":"Show what apps and providers a tenant can pick during setup. Use when answering 'what can Plinth do?' or 'which providers are supported?' before kicking off registration."},{"name":"onboarding.discover_organization","description":"Look up an email's domain and tell me whether it's free for a new workspace, already claimed (the user should request access), or a personal/free-mail domain (rejected). Use before calling register_workspace; the discovery decides which path to take."},{"name":"onboarding.request_email_verification","description":"Send a one-time code to an email so the user can prove ownership. Use before claiming a domain (purpose=domain_claim), requesting access to an existing org (purpose=access_request), or signing in passwordless (purpose=magic_link). The token is delivered out-of-band; this call just returns the TTL."},{"name":"onboarding.verify_email","description":"Consume a verification token returned from the user's inbox. After this succeeds, register_workspace / request_access / login can use the same (email, token) within the freshness window."},{"name":"onboarding.register_workspace","description":"Create a new tenant workspace and atomically claim the owner's email domain. Use when the user says 'create my workspace', 'sign up our company', or 'set up Acme Inc on Plinth'. Pass a fresh verificationToken from verify_email. Errors: DOMAIN_ALREADY_CLAIMED if someone beat you to it, PERSONAL_EMAIL_REJECTED for free-mail domains."},{"name":"onboarding.request_access","description":"Ask to join an existing workspace already registered for this email's domain. Use when discover_organization returned 'claimed' and the user wants in. Requires a fresh email verification."},{"name":"onboarding.select_stack","description":"Pick which app modules and provider stack the tenant wants Plinth to manage (e.g. 'we want auth + email through Brevo'). Use after register_workspace, when the user is choosing capabilities."},{"name":"onboarding.get_setup_status","description":"Where am I in the onboarding flow? Returns registration, provider, and app-connection completeness. Use to drive a checklist UI or to answer 'what's left to set up?'."}]},{"id":"workspace","label":"Workspace identity","tools":[{"name":"workspace.whoami","description":"Tell me which workspace, user, and roles this API key is bound to. Use this when the agent needs to confirm context before doing anything destructive, when the user asks 'who am I?' or 'which workspace?', or as a smoke test that the key works."}]},{"id":"team","label":"Team","tools":[{"name":"team.list_members","description":"List human members of the authenticated workspace (email, role, status, last seen). Use when an agent needs the org roster, when reconciling access, or before inviting someone who might already be a member. Requires a workspace API key with this tool allowed."},{"name":"team.invite_member","description":"Invite a teammate by email with role admin or member (owner cannot be assigned via invite). Sends the same invite email as the dashboard when mail is configured. The API key must have been minted with the inviter's Plinth account email as actorId (the dashboard does this automatically when you create an app key while signed in)."}]},{"id":"apps","label":"Apps & routing","tools":[{"name":"apps.get_email_routing","description":"Show how this app is currently routing email sends — which connected providers it may use (the pool), the rotation strategy, optional rate-limit failover across the pool, and what the workspace default would be if the pool is empty. Use to answer 'is this app on Brevo or Resend?' or before flipping with apps.set_email_routing."},{"name":"apps.set_email_routing","description":"Configure how this app picks an email provider per send. Strategies: 'single' uses providers[0] (or the workspace default when providers is empty); 'round_robin' atomically cycles through providers[] using a per-app cursor (even split across providers per send, deterministic); 'random' picks uniformly per send. When `failoverOnRateLimit` is true and at least two providers are in the pool, a send that hits a Plinth `RATE_LIMITED` gate (strict daily quota / token bucket) on one provider automatically retries the next pool member before failing the call. Validates each brand is actually connected for this workspace and resets the rotation cursor on save. Use when the user says 'split this app's email between Brevo and Resend' or 'move this app to Resend only'. Audited as mcp.app_connection.email_routing_changed."},{"name":"apps.get_email_allocation","description":"Show workspace-wide per-app email allocation: whether the UTC-day pool split is enabled, the effective daily pool cap (explicit override or sum of workspace-wide regulation send limits), each app's share percent, today's accepted send count vs that app's cap, and optional per-recipient reservations inside each app's share. Use before changing shares or when debugging `app_email_allocation_exceeded` / `app_recipient_allocation_exceeded` declines."},{"name":"apps.set_email_allocation","description":"Reserve a percentage of the workspace's UTC-day email pool for each app. Shares must sum to at most 100%. When enabled, sends that would push an app past floor(pool × sharePercent / 100) accepted messages today are politely declined with `declineReason='app_email_allocation_exceeded'` (whole batch when no per-recipient reservations exist). Optionally reserve sub-caps inside an app's share for specific recipient addresses via `recipientReservations`; reserved caps must sum to at most that app's daily cap, and remaining capacity is shared by all other recipients. Per-recipient over-cap sends are declined with `declineReason='app_recipient_allocation_exceeded'` (partial batch allowed). `dailyPoolCap` overrides the auto-derived pool (sum of workspace-wide regulation `sends` burst capacities); omit or null to use auto. Audited as email.app_allocation.updated."},{"name":"apps.set_provider_credential_mode","description":"Choose whether an app inherits workspace-level provider credentials (default) or uses only its own sealed keys. When inheriting, sends use workspace keys for any brand the app has not connected itself. Audited as mcp.app_connection.provider_inheritance_changed."}]},{"id":"providers","label":"Provider connections","tools":[{"name":"provider_connections.connect","description":"Connect a third-party provider (Brevo, Stripe, Resend, …) by sealing its API key onto a SPECIFIC app. Use when the user says 'connect Brevo to my marketing app' or 'add Stripe keys to the checkout app'. Apps that inherit workspace providers use `provider_connections.connect_workspace` for shared keys instead. Per-app credentials — every credential pair belongs to exactly one app, so two apps in the same workspace can each carry a distinct Brevo / Stripe / Clerk subscription when not inheriting. Credentials are encrypted with XChaCha20-Poly1305 + per-workspace HKDF; only redacted previews are echoed back. The owning `appId` is REQUIRED and must be one this workspace already owns; pass the calling app's id when in doubt. For Resend, optional `resendAccountTier` (`free` default, or `paid`) controls which regulation presets are auto-applied — Plinth cannot read Resend billing from the API."},{"name":"provider_connections.connect_workspace","description":"Connect a provider at workspace level so every app that inherits workspace keys can send through it without pasting the same API key again. Seals credentials on the hidden workspace self-app; visible apps with `inheritWorkspaceProviders=true` (the default) resolve these keys automatically unless they define their own override for that brand. Use before creating apps when the operator says 'connect Brevo once for the whole org'. Same validation and audit as `provider_connections.connect`."}]},{"id":"emails","label":"Email send & templates","tools":[{"name":"emails.send","description":"Send an email right now to one or more people. Use when the user says 'email Alice the invoice', 'send a one-off message to bob@x.com', 'reach out to the support team', or 'fire off this update to <list>'. Don't use a template — just paste the body. Pass `body` as plain text or HTML and Plinth picks the right field; or pass `html` / `text` explicitly. `to` accepts a single address, an array, `{ email }`, an array of `{ email }`, or 'Name <email>' format. For saved templates and `templateKey`, use `emails.send_template` instead — this tool rejects unknown keys (e.g. `templateKey`) so mistakes fail fast. `from` defaults to the workspace's verified sender, and keeps a display name when given as 'Customer Care <support@acme.com>'. `cc` / `bcc` take the same address forms as `to`; `attachments` takes { filename, content (base64), contentType? }."},{"name":"emails.send_template","description":"Send an email using a saved template, filling in {{variables}}. Use when the user says 'send the welcome email to alice@x.com', 'fire the receipt template at this customer', or 'use my magic-link template for bob'. Pick the template by `templateKey` (e.g. 'welcome', 'receipt'). Pass any handlebars values in `variables`. `to` accepts one address, many, `{ email }`, or `[{ email }]`. For one-off HTML/text without a saved template, use `emails.send`. Unknown keys are rejected."},{"name":"emails.save_template","description":"Create or update a reusable email template. Use when the user says 'save this as the welcome email', 'make a template for password resets', or 'update the receipt template's wording'. Templates are workspace-wide — any app with the Manage templates capability can send them. Use {{handlebars}} for variables; Plinth auto-detects them if you don't pass `variables` explicitly. Re-saving an existing key OVERWRITES it (this is the edit path). For an idempotent 'create-only-if-missing' primitive that won't clobber a hand-tuned production template on retry, use `emails.ensure_template` instead."},{"name":"emails.ensure_template","description":"Idempotent fetch-or-create for an email template. The agent recovery primitive: when `emails.send_template` returns `TEMPLATE_NOT_FOUND`, follow the `details.recovery` hint and call this — it returns the existing template if one is already saved at `key`, otherwise installs a curated preset (`fromPreset`) or creates from your inline payload (`inline`). Never overwrites an existing template — pick `emails.save_template` if you want the upsert. Returns `{ template, created, source }` so an agent can log whether the call was a no-op (`source: \"existing\"`), a preset install (`\"preset\"`), or a fresh inline create (`\"inline\"`)."},{"name":"emails.list_presets","description":"Browse Plinth's curated, opinionated email-template presets (welcome, magic-link, OTP, click-to-verify, receipt, password reset, trial-expiring, …). Use to discover what `fromPreset` keys `emails.ensure_template` accepts, before deciding whether to install a preset or compose your own. Read-only and stateless — safe to call any time. Filter by `category` (\"auth\" | \"lifecycle\" | \"transactional\" | \"marketing\") to narrow the list."},{"name":"emails.compose_template","description":"Conversational email-template composer. Use when the user says 'draft a welcome email', 'help me write a receipt template', or any open-ended template ask. Pass the full conversation history each turn (user + assistant); Plinth either replies with `{ kind: 'ask', followups }` to gather more context or `{ kind: 'draft', draft }` once it has enough. The returned draft is NOT saved automatically — call `emails.save_template` with the draft fields to persist it. Uses the workspace's connected LLM provider (e.g. OpenAI in /connections/llm) when present; falls back to a deterministic heuristic and returns `heuristic: true` when no LLM is connected."},{"name":"emails.list_templates","description":"Show every email template saved in this workspace. Use when the user asks 'what templates do I have?', 'list my emails', or before picking one for emails.send_template. Newest first."},{"name":"emails.delete_template","description":"Delete a saved email template by key. Use when the user says 'remove the old welcome template' or 'delete the receipt one'. Idempotent for callers — sending after delete will fail with NotFoundError."},{"name":"emails.add_contact","description":"Add or update an email contact, optionally putting them on contact lists. Use when the user says 'add alice@x.com to my newsletter list', 'create a contact for bob with name Bob Smith', or 'put this person on the customers list'. Idempotent on email; calling again updates name/attributes/lists. Pass `lists` as a string ('newsletter') or array (['newsletter','customers'])."},{"name":"emails.list_contacts","description":"List contacts, optionally scoped to a single list. Use when the user asks 'who's on my newsletter list?', 'show all contacts', or before sending a one-off campaign. Pass `list` to filter ('list: \"newsletter\"')."},{"name":"emails.unsubscribe","description":"Suppress an email address so future sends skip them. Use when the user says 'unsubscribe alice@x.com', 'this address keeps bouncing', or after a webhook reports a complaint. Idempotent. Suppressions persist forever unless the user explicitly removes them via the dashboard."},{"name":"emails.create_newsletter","description":"Compose a newsletter as sections (intro + items + outro). Use when the user says 'draft this week's newsletter', 'create a digest about X', or 'put together our monthly update'. Sections render to a clean responsive HTML wrapper at send time — you don't write HTML yourself. Re-saving the same key overwrites it (drafting workflow)."},{"name":"emails.send_newsletter","description":"Broadcast a saved newsletter to every active contact on a list. Use when the user says 'send the may digest to my newsletter list', 'broadcast our launch update', or 'fan out the digest to subscribers'. Honors the suppression list automatically. Returns total / sent / failed / skipped counts."},{"name":"emails.list_newsletters","description":"Show every newsletter saved in this workspace. Use when the user asks 'what newsletters do I have?' or before picking one to send."},{"name":"emails.get_tag_stats","description":"Report how many emails each tag sent, and how they landed. Use when the user asks 'how many invoice emails went out this month', 'which campaign sent the most', 'break my sends down by tag', or 'how did the onboarding tag perform'. Scoped to the calling app by default — pass `appConnectionId: null` for the whole workspace. Returns per-tag totals plus delivered / opened / clicked / bounced / declined counts, and an `untagged` figure so you can see how much traffic carries no tag at all. A message with several tags counts under each one, so the columns add up to more than the message total. Tag your sends via the `tags` field on `emails.send` / `emails.send_template` for this to be useful."},{"name":"emails.get_message_status","description":"Fetch the live delivery + engagement status of one previously-sent email. Use when the user (or your own retry loop) asks 'did the welcome email to alice@x.com land?', 'has the receipt been opened?', or 'what's the state of message X?'. Pass the `messageId` returned by `emails.send` / `emails.send_template` (NOT the provider's own id). Plinth stitches three sources: the local message row, any webhook events recorded so far, and a live lookup against the configured provider (Brevo `/smtp/statistics/events`, Resend `GET /emails/:id`). Local data is returned synchronously; the provider lookup happens automatically when status is still 'sent' with no engagement and there's a providerMessageId. Set `refresh: true` to force a fresh provider call even when local data looks complete (counts toward the email vertical's budget). Returns `{ message, state, events, providerLookup }` — `state` is the highest-reached state across the merged sources (queued → sent → delivered → opened → clicked → bounced/complained/failed). `providerLookup.notFound` indicates the provider has no record (e.g. message id outside their retention window) — agents should not retry that lookup."},{"name":"emails.list_messages","description":"Paginated message log for the current workspace. Use when the user asks 'what did we send today?', 'show me everything that bounced this week', 'find sends to bob@y.com', or your agent needs to drain the recent log to reconcile state. Filters compose: any combination of `from` / `to` (ISO timestamps, half-open window), `status` (queued/sent/rejected/failed at submit time), `kind` (transactional/newsletter/template), `templateKey`, `newsletterKey`, `toEmail`, and `appConnectionId` (the AppConnection.id of the originating app — pass null to scope to dashboard sends). Returns `{ messages, nextCursor }`; thread `nextCursor` back into a follow-up call to drain pages. Each row carries the merged engagement state (deliveredAt / openedAt / clickedAt / openCount / clickCount) so you don't need a follow-up `get_message_status` per row. Per-message provider lookups are NOT performed by this tool — call `emails.get_message_status` on a specific row when you need fresh provider state."},{"name":"emails.get_webhook_endpoint","description":"Show the webhook URL Plinth listens on for a given email provider on a SPECIFIC app, plus whether the signing secret is wired and whether the operator has to paste anything into the provider's dashboard. Per-app credentials (0013): each app's Resend / Brevo subscription gets its own URL + secret, so two apps in the same workspace never share a webhook. Use when the user asks 'what's my Resend webhook URL?', 'is the Brevo webhook configured?', or before troubleshooting why deliverability events aren't flowing. `appId` defaults to the calling app when omitted."},{"name":"emails.ensure_webhook","description":"Register (or refresh) Plinth's webhook on the provider's side for ONE app. For Resend this is a real `POST /webhooks` API call that returns the Svix signing secret — Plinth seals it into the same credentials envelope as your API key. For Brevo (no public webhook-management API) this just generates a random shared token; the operator must paste it + the URL into Brevo's dashboard. Idempotent: if an endpoint already exists for this app, the existing registration is updated in place rather than duplicated. Per-app credentials (0013): app A and app B can each register a Resend webhook for their own subscriptions and they won't collide. Use when the user says 'set up Resend webhooks for my marketing app' or after a connect that didn't auto-register. Audited as `email.webhook.ensured`. `appId` defaults to the calling app when omitted."},{"name":"emails.rotate_webhook_secret","description":"Issue a fresh signing secret for the provider's webhook on ONE app (Resend rotates the Svix signing key in place; Brevo regenerates the shared token). Per-app credentials (0013): rotation only affects the named app's webhook — sibling apps' secrets are untouched. Use during routine secret rotation, after a suspected compromise, or when migrating between environments. Audited as `email.webhook.rotated`. NOTE: for Brevo, the rotated token must be pasted into the dashboard before it takes effect — until then, inbound webhooks will 401. `appId` defaults to the calling app when omitted."},{"name":"emails.list_verified_senders","description":"List the senders / domains the upstream provider considers verified for ONE app — the addresses the operator can actually send `from` without bouncing on a 'sender not authenticated' error. Per-app credentials (0013): the lookup is scoped to a single (appId, provider) pair so two apps' Brevo / Resend subscriptions never bleed verified-sender state across each other. Use BEFORE composing a send when you need to pick a `from` address you didn't already pin on the provider connection — much faster than asking the operator to retype, and avoids the deliverability quota burned by 'sender not verified' rejections. Also surfaced in the dashboard's 'fix sender' dialog so the operator can pick from a verified list with one click. Returns `{ providers: [{ provider, ok, supported, senders: [{ kind, email?, domain?, name?, verified, active, status, providerId }] }] }` — `kind: 'address'` is a specific email (Brevo's per-sender verification), `kind: 'domain'` covers any local-part of that domain (Resend / SES). When `provider` is omitted Plinth queries every email connection on the app and merges the results; pass `provider: 'brevo'` (or `'resend'`) to scope to one. Adapters that don't expose a verified-sender list (e.g. legacy SMTP) return `supported: false` so the caller can degrade gracefully. `appId` defaults to the calling app when omitted; required from dashboard contexts where the request isn't already bound to one."},{"name":"emails.list_recent_events","description":"Recent EmailEvent rows across the workspace (delivered / opened / clicked / bounced / complained / unsubscribed). Use to confirm webhooks are firing at all, to spot a deliverability cliff, or to debug why a specific message hasn't been marked delivered. Returns at most 500 rows, defaults to the last 24h. For per-message detail, call `emails.get_message_status`."},{"name":"emails.list_provider_safety","description":"List the deliverability safety profile for every email provider connected on ONE app. Returns one row per (app, provider) with the persisted policy, the per-provider Plinth preset (defaults + ceilings + which toggles are mandatory), and the well-meaning defaults so the caller can render a UI or compare to current overrides without a second round-trip. Use BEFORE composing a bulk send if the caller wants to know the per-domain cap or hard-bounce auto-pause threshold for the active provider; use after a deliverability decline to read what policy fired. `appId` defaults to the calling app when omitted; required from dashboard / cross-app contexts."},{"name":"emails.update_provider_safety","description":"Update the safety profile for ONE (app, provider). Plinth ENFORCES ceilings — values outside the per-provider ceiling are REJECTED with a structured `BAD_REQUEST` (details include `{ field, requested, maxAllowed, reason }`). Mandatory toggles (hard-bounce, complaint, per-domain cap) cannot be turned off; attempts return the same structured error. Audited as `email.safety_policy_updated`. Idempotent: calling twice with the same values is a no-op. Tip: call `emails.list_provider_safety` first to read the per-provider ceiling — picking values inside the envelope avoids the rejection round-trip."},{"name":"emails.list_deliverability_events","description":"Paginated read of deliverability decisions Plinth made: declines (`domain_daily_cap_exceeded`, `soft_bounce_cooldown`, `provider_backlog`, `suppression_list`, `provider_paused`), bounces classified by hard/soft, and provider auto-pauses. Use to drive a deliverability report (CSV / JSON), to debug why a specific recipient wasn't sent to, or to confirm the per-domain cap is actually firing on the live route. Filter combinations match the `/insights/email` Deliverability tab so the UI and the export return the same rows. Returns `{ events, nextCursor }`. Each event carries `{ at, kind, reason?, appConnectionId?, provider?, recipient?, recipientDomain?, retryAfter?, messageId? }` — `kind` is one of `decline | bounce | provider_paused`. Cross-link `messageId` to `emails.get_message_status` when you need the original send context."}]},{"id":"regulation","label":"Quotas & rate limits","tools":[{"name":"regulation.set_limit","description":"Create or update a token-bucket rate / quota rule. Use when the user says 'cap Brevo at 300 emails per day for this app', 'limit Resend to 5 sends/sec workspace-wide', 'soft-warn at 1000 emails/day on the marketing app'. Three-axis scope: omit `appId` to apply workspace-wide; omit `provider` to apply across all providers in the vertical; pass both for the tightest scope. `ratePerSecond` is tokens added per second (for daily quotas use `dailyQuota / 86400`); `burstCapacity` is the bucket cap. `hardStop: true` throws RATE_LIMITED with retry-after when the bucket is empty; `false` audits a warning but lets the send through (mirrors how budgets work). Idempotent on the (workspace, appId, provider, vertical, metric) tuple — re-calling with the same scope upserts the existing rule."},{"name":"regulation.list_limits","description":"List configured regulation rules. Use to answer 'what rate limits are in effect?', 'which apps have provider-specific caps?', 'show me everything that could throttle a Brevo send'. Optional filters narrow by app, provider, vertical, or metric. Returns each rule with its current bucket state (`tokensRemaining`, `lastRefillAt`) so the dashboard can render live capacity."},{"name":"regulation.delete_limit","description":"Remove a regulation rule by id. Use when the user says 'remove the daily cap on Brevo' or 'lift the rate limit on the marketing app'. Audited as `regulation.deleted` so the chain shows when a rule was lifted (compliance evidence)."},{"name":"regulation.preview","description":"Preview the *current* bucket state without consuming tokens. Use to answer 'how many sends do I have left right now?', 'will this 1000-recipient blast pass Brevo's daily quota?', or before showing the operator a 'fits' / 'will throttle' badge in a UI. Computes the refill curve from `lastRefillAt` to now and returns the would-be decision (`wouldAllow` + `retryAfterMs`) for the requested qty against every applicable rule."},{"name":"regulation.apply_presets","description":"Re-apply the curated regulation preset bundle for a provider. Used to refresh defaults after a Plinth release that bumped them, or to re-seed limits after they were deleted. Operator-edited rules (source='operator') are NOT touched unless `force: true`. Auto-fired on every `provider_connections.connect`, so manual calls are normally only needed when restoring defaults. For Resend, pass `resendAccountTier: 'paid'` when the API key is on a paid transactional plan so the free-tier daily send cap is not re-seeded."}]},{"id":"governance","label":"Audit, usage, cost","tools":[{"name":"governance.get_costs","description":"Read cost rollups for the workspace. Use when the user asks 'how much have I spent this month?', 'what's email costing me?', or 'show Brevo spend this week'. Filter by `provider` or `vertical` to narrow."},{"name":"governance.set_budget","description":"Create or update a budget that gates future provider calls. Use when the user says 'cap email spend at $50/mo', 'set a hard stop on Stripe writes', or 'budget the LLM tag'. `hardStop: true` aborts in-flight calls; default is soft (alert only)."},{"name":"governance.simulate_spend","description":"Estimate whether a planned set of changes would breach budgets. Use before bulk operations: 'will sending this newsletter to 50k people put me over budget?'."},{"name":"governance.export_audit_log","description":"Export the audit chain for an interval (CSV or JSON). Use when the user says 'pull last quarter's audit log' or 'I need our SOC2 evidence'. Result is verifiable client-side via AuditService.verifyChain."}]},{"id":"branding","label":"Brand & styling","tools":[{"name":"branding.get","description":"Read a brand profile (colours, fonts, logo URL, footer text, social links). Brand identity is per-app — every AppConnection owns its own brand row, so the marketing app and the transactional app in the same workspace can run completely different palettes / footers / logos. When `appId` is omitted the call defaults to the brand of the app whose API key authenticated the request (the natural shape for an agent: 'show me my brand'). Use when the user asks 'what does this app's brand look like?', 'what colour are the marketing emails?', or before composing any branded artifact. Returns sensible Plinth defaults for any field the app hasn't customised yet, so the response is always fully-populated."},{"name":"branding.update","description":"Update a brand profile. Brand is per-app: changes only affect the named AppConnection (or, when `appId` is omitted, the calling app — useful for an agent that wants to brand its own surface). All fields optional; pass only the ones you want to change (PATCH semantics). Use when the user says 'make this app's brand purple', 'use this logo URL for the marketing app', 'set the footer to my company address'. Hex colours like '#5B3DEC'. `socialLinks` is an array of {label,url}. After saving, every subsequent render under that app substitutes the new {{brand.*}} tokens at render-time — no need to re-edit each saved template / receipt / page. Other apps in the same workspace are unaffected."}]},{"id":"mcp","label":"App connections","tools":[{"name":"mcp.create_app_connection","description":"Mint a new API key bound to a specific app, environment, and allow-list of tools. Use when the user says 'create an app for our backend' or 'I need a key for staging'. The raw key is shown exactly once; store it immediately."}]},{"id":"other","label":"Other","tools":[{"name":"marketing.capture_signup","description":"Capture a product-updates / newsletter signup. Use when the user fills the marketing form. Normalizes the domain to eTLD+1 and flags free-mail addresses."},{"name":"marketing.get_stats","description":"Return public marketing stats (total signups + redacted recent list). Used by the public landing page; safe to call without a key."},{"name":"auth.create_user","description":"Create or upsert an end-user inside the workspace's connected auth provider. Use when the user says 'add user alice@x.com' or 'invite a teammate'. Idempotent on email — calling twice updates metadata, doesn't create duplicates."},{"name":"auth.configure_sso","description":"Wire up an OIDC or SAML SSO connection for the workspace. Use when the user says 'enable Google SSO' or 'connect Okta'. Pass `metadataUrl` if the IdP exposes one; otherwise the dashboard prompts for raw config."},{"name":"payments.create_plan","description":"Create a pricing plan with one or more tiers. Use when the user says 'add a $20/month Pro plan' or 'launch a yearly tier'. Amounts are in the smallest currency unit (cents)."},{"name":"payments.publish_pricing_page","description":"Publish a hosted pricing page tied to a plan. Use when the user says 'go live with our pricing' or 'show pricing on our site'."},{"name":"social.schedule_post","description":"Queue or send a multi-platform social post. Use when the user says 'tweet this', 'post to LinkedIn and X', or 'schedule this thread for tomorrow morning'. `variants` lets you tailor the copy per platform (X gets short, LinkedIn gets long-form)."},{"name":"content.create_blog_post","description":"Create or update a blog post (MDX). Use when the user says 'publish this post', 'draft a blog about X', or 'update the launch post'. Idempotent on `slug`."},{"name":"landing.create_page","description":"Create a landing page from a JSON section schema, with optional A/B variants. Use when the user says 'make a landing page for our launch' or 'set up a /trial page with two variants'."},{"name":"hosting.deploy","description":"Trigger a deployment from a Git ref or pre-built artifact. Use when the user says 'deploy main', 'ship to production', or 'roll out the staging build'. `production: true` is the live cutover; default is preview."},{"name":"hosting.attach_domain","description":"Attach a custom domain to a hosting project. Use when the user says 'point acme.com at this project'. Returns the DNS records the user needs to set on their registrar."}]}],"workspace":{"connectedProviders":0,"apps":0}}