# dotsy (remote · mcp.dotsy.ai)

Build and publish static sites on dotsy with draft-first MCP tools, OAuth Connect, and API keys.

- Trust score: 90/100 (high trust)
- Change this week: +3
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-11-05

## Components

- remote · `mcp.dotsy.ai`: 90/100 (this document), [markdown](https://verifymcp.io/servers/ai-dotsy-mcp/mcp.md), [page](https://verifymcp.io/servers/ai-dotsy-mcp/mcp)

## Channel facts

- Endpoint: `https://mcp.dotsy.ai/mcp`
- Transports: `streamable-http`
- Auth: `required`
- Version: `0.8.8`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically against the live server, 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-11-05.

- **Endpoint Security**: 89/100
  - The endpoint's TLS certificate is valid, in date, and uses a strong key.
  - Authorisation is enforced on tool calls, advertised via RFC 9728 protected-resource metadata. Discovery is public, which costs nothing: no tool can be invoked without a token.
  - HTTPS is enforced; there's no plaintext access path.
  - HSTS check failed: the Strict-Transport-Security header is absent.
  - DNSSEC check failed: this domain isn't protected by DNSSEC.
  - The authorisation server offers only Dynamic Client Registration (RFC 7591), which MCP 2026-07-28 deprecated in favour of Client ID Metadata Documents.
- **Transport & Reachability**: 100/100
  - Verified streamable-http transport via a live MCP handshake.
- **Schema Quality & AI Usability**: 76/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (good).
  - Context-footprint check failed: tool/resource definitions use about 18437 tokens (~147/item across 125 items; 117 tools + 8 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 94/100
  - Stability check failed: schema churn in the 29 days we've observed: 1 tool removals, 3 breaking changes, 0 auth/transport breaks, 29 additions.
- **Tool Coverage**: 92/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 76% of tool parameters carry a description.
- **Tool Safety**: 100/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - All 10 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation.
  - An AI judge read all 119 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.
  - Supports UI / widget rendering.

## Install

### How do I install the dotsy MCP server?

dotsy is a hosted endpoint at https://mcp.dotsy.ai/mcp, so there is nothing to install locally. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add --transport http ai-dotsy-mcp 'https://mcp.dotsy.ai/mcp'
```

### Cursor

```json
{
  "mcpServers": {
    "ai-dotsy-mcp": {
      "url": "https://mcp.dotsy.ai/mcp"
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "ai-dotsy-mcp": {
      "type": "http",
      "url": "https://mcp.dotsy.ai/mcp"
    }
  }
}
```

### Codex

```toml
[mcp_servers.ai-dotsy-mcp]
url = "https://mcp.dotsy.ai/mcp"
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "ai-dotsy-mcp": {
      "type": "remote",
      "url": "https://mcp.dotsy.ai/mcp",
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add ai-dotsy-mcp --url 'https://mcp.dotsy.ai/mcp' --transport streamable-http
```

### Hermes

```yaml
mcp_servers:
  ai-dotsy-mcp:
    url: "https://mcp.dotsy.ai/mcp"
```

### Netclaw

```json
{
  "McpServers": {
    "ai-dotsy-mcp": {
      "Transport": "http",
      "Url": "https://mcp.dotsy.ai/mcp"
    }
  }
}
```

### Vellum

```bash
assistant mcp add ai-dotsy-mcp -t streamable-http -u 'https://mcp.dotsy.ai/mcp'
```

### Other

```json
{
  "mcpServers": {
    "ai-dotsy-mcp": {
      "type": "http",
      "url": "https://mcp.dotsy.ai/mcp"
    }
  }
}
```

The mcpServers block is a cross-client convention. Remote transports vary, so check your client's docs.

## 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-11-05 (score 90, +1)

- [security] Resource “get_site_build_progress” now points somewhere else: ui://views/apps-sdk/build-theater.html?v=0b9659cd → ui://views/ext-apps/build-theater.html?v=0b9659cd
- [security] Resource “edit_page” now points somewhere else: ui://views/apps-sdk/page-canvas.html?v=88974c83 → ui://views/ext-apps/page-canvas.html?v=88974c83
- [security] Resource “get_site_build_progress” now points somewhere else: ui://views/apps-sdk/build-theater.html?v=34c6d210 → ui://views/ext-apps/build-theater.html?v=0b9659cd
- [security] Resource “get_site_build_progress” now points somewhere else: ui://views/ext-apps/build-theater.html?v=34c6d210 → ui://views/ext-apps/build-theater.html?v=0b9659cd
- [security] Resource “list_my_sites” now points somewhere else: ui://views/apps-sdk/site-picker.html?v=20fa5fa4 → ui://views/ext-apps/site-picker.html?v=20fa5fa4
- [security] Resource “list_pages” now points somewhere else: ui://views/apps-sdk/page-picker.html?v=3fce7297 → ui://views/ext-apps/page-picker.html?v=3fce7297
- [security] Tool “update_site” rewrote its description, which is the text the model reads
- [functional] Server version: 0.8.6 → 0.8.8
- [functional] New tool “form_recipients”
- [functional] New tool “install_product_kit”

### 2026-11-04 (score 89, 0)

- [security] Resource “edit_page” now points somewhere else: ui://views/apps-sdk/page-canvas.html?v=88974c83 → ui://views/ext-apps/page-canvas.html?v=88974c83
- [security] Resource “get_site_build_progress” now points somewhere else: ui://views/apps-sdk/build-theater.html?v=34c6d210 → ui://views/ext-apps/build-theater.html?v=34c6d210
- [security] Resource “list_my_sites” now points somewhere else: ui://views/apps-sdk/site-picker.html?v=20fa5fa4 → ui://views/ext-apps/site-picker.html?v=20fa5fa4
- [security] Resource “list_pages” now points somewhere else: ui://views/apps-sdk/page-picker.html?v=3fce7297 → ui://views/ext-apps/page-picker.html?v=3fce7297

### 2026-11-03 (score 89, +1)

- [security] Resource “edit_page” now points somewhere else: ui://views/apps-sdk/page-canvas.html?v=bfd4da19 → ui://views/ext-apps/page-canvas.html?v=88974c83
- [security] Resource “edit_page” now points somewhere else: ui://views/ext-apps/page-canvas.html?v=bfd4da19 → ui://views/ext-apps/page-canvas.html?v=88974c83
- [security] Resource “get_site_build_progress” now points somewhere else: ui://views/apps-sdk/build-theater.html?v=317f2c43 → ui://views/ext-apps/build-theater.html?v=34c6d210
- [security] Resource “get_site_build_progress” now points somewhere else: ui://views/ext-apps/build-theater.html?v=317f2c43 → ui://views/ext-apps/build-theater.html?v=34c6d210
- [security] Resource “list_my_sites” now points somewhere else: ui://views/apps-sdk/site-picker.html?v=f9802318 → ui://views/ext-apps/site-picker.html?v=20fa5fa4
- [security] Resource “list_my_sites” now points somewhere else: ui://views/ext-apps/site-picker.html?v=f9802318 → ui://views/ext-apps/site-picker.html?v=20fa5fa4
- [security] Resource “list_pages” now points somewhere else: ui://views/apps-sdk/page-picker.html?v=f80b1d11 → ui://views/ext-apps/page-picker.html?v=3fce7297
- [security] Resource “list_pages” now points somewhere else: ui://views/ext-apps/page-picker.html?v=f80b1d11 → ui://views/ext-apps/page-picker.html?v=3fce7297
- [security] Resource “edit_page” now points somewhere else: ui://views/apps-sdk/page-canvas.html?v=bfd4da19 → ui://views/ext-apps/page-canvas.html?v=bfd4da19
- [security] Resource “get_site_build_progress” now points somewhere else: ui://views/apps-sdk/build-theater.html?v=317f2c43 → ui://views/ext-apps/build-theater.html?v=317f2c43
- [security] Resource “list_my_sites” now points somewhere else: ui://views/apps-sdk/site-picker.html?v=f9802318 → ui://views/ext-apps/site-picker.html?v=f9802318
- [security] Resource “list_pages” now points somewhere else: ui://views/apps-sdk/page-picker.html?v=f80b1d11 → ui://views/ext-apps/page-picker.html?v=f80b1d11
- [security] Tool “report_bug” rewrote its description, which is the text the model reads
- [functional] Server version: 0.8.5 → 0.8.6
- [functional] Server version: 0.8.4 → 0.8.5
- [cosmetic] “get_analytics” reworded the description of “include”
- [cosmetic] “report_bug” reworded the description of “kind”

### 2026-11-02 (score 88, 0)

We did not load change detail this far back for this component, so this day may have recorded more than is shown.

- [security regression] Stability: 0.83 → fail
- [security regression] Tool “approve_suggestion” was removed
- [security regression] Tool “decline_suggestion” was removed
- [security regression] Tool “merge_suggestion” was removed
- [security regression] Tool “support_inbox” was removed
- [security regression] Tool “decompose_analyze” was removed
- [security] The server rewrote its instructions, which are the text every model session reads
- [security] The server changed its declared name: paige → dotsy
- [security] Tool “claim_site” rewrote its description, which is the text the model reads
- [security] Tool “get_report” rewrote its description, which is the text the model reads
- [security] Tool “list_my_sites” rewrote its description, which is the text the model reads
- [security] Tool “reply_to_report” rewrote its description, which is the text the model reads
- [security] Tool “report_bug” rewrote its description, which is the text the model reads
- [security] Tool “set_analytics_provider” rewrote its description, which is the text the model reads
- [security] Tool “start_site” rewrote its description, which is the text the model reads
- [security] Tool “update_site” rewrote its description, which is the text the model reads
- [security] Tool “publish_site” rewrote its description, which is the text the model reads
- [security] Tool “get_site_build_progress” rewrote its description, which is the text the model reads
- [security] Tool “support_inbox” is now declared destructive
- [security] Tool “restore_version” is now declared destructive
- [security] Tool “graduate_site” is now declared destructive
- [functional regression] “get_skill” dropped the required parameter “context”
- [functional regression] “start_site” dropped the required parameter “context”
- [functional regression] “list_skills” dropped the required parameter “context”
- [functional improvement] Schema quality: 208 → 147
- [functional improvement] Schema quality: 208 → 161
- [functional] Too many changes on this day to record them all, so 157 were dropped
- [functional] Server version: 0.8.1 → 0.8.4
- [functional] Server version: 0.3.0 → 0.8.1
- [functional] New tool “moderate_suggestion”
- [cosmetic] “add_custom_domain” reworded the description of “context”
- [cosmetic] “batch_patch_content” reworded the description of “context”
- [cosmetic] “bulk_put_assets” reworded the description of “context”
- [cosmetic] “create_site” reworded the description of “context”
- [cosmetic] “delete_content” reworded the description of “context”
- [cosmetic] “delete_draft_test” reworded the description of “context”
- [cosmetic] “delete_route” reworded the description of “context”
- [cosmetic] “delete_site” reworded the description of “context”
- [cosmetic] “discard_drafts” reworded the description of “context”
- [cosmetic] “estimate_test” reworded the description of “context”
- [cosmetic] “find_replace” reworded the description of “context”
- [cosmetic] “get_analytics” reworded the description of “context”
- [cosmetic] “get_asset” reworded the description of “context”
- [cosmetic] “get_board” reworded the description of “context”
- [cosmetic] “get_collection_schema” reworded the description of “context”
- [cosmetic] “get_custom_domain_connect” reworded the description of “context”
- [cosmetic] “get_menu_doc” reworded the description of “context”
- [cosmetic] “get_site” reworded the description of “context”
- [cosmetic] “get_site_manifest” reworded the description of “context”
- [cosmetic] “get_structure_status” reworded the description of “context”
- [cosmetic] “graduate_site” reworded the description of “context”
- [cosmetic] “list_assets” reworded the description of “context”
- [cosmetic] “list_collections” reworded the description of “context”
- [cosmetic] “list_content” reworded the description of “context”
- [cosmetic] “list_ideas” reworded the description of “context”
- [cosmetic] “list_notifications” reworded the description of “context”
- [cosmetic] “patch_route” reworded the description of “context”
- [cosmetic] “preflight_site” reworded the description of “context”
- [cosmetic] “promote_experiment” reworded the description of “context”
- [cosmetic] “publish_site” reworded the description of “context”
- [cosmetic] “put_collection_schema” reworded the description of “context”
- [cosmetic] “put_goals” reworded the description of “context”
- [cosmetic] “put_menu_item” reworded the description of “context”
- [cosmetic] “put_post” reworded the description of “context”
- [cosmetic] “refresh_custom_domain” reworded the description of “context”
- [cosmetic] “remove_votes” reworded the description of “context”
- [cosmetic] “rename_content” reworded the description of “context”
- [cosmetic] “rename_site” reworded the description of “context”
- [cosmetic] “reorder_menu” reworded the description of “context”
- [cosmetic] “report_bug” reworded the description of “context”
- [cosmetic] “report_bug” reworded the description of “site”
- [cosmetic] “review_changes” reworded the description of “context”
- [cosmetic] “search_content” reworded the description of “context”
- [cosmetic] “set_analytics_provider” reworded the description of “context”
- [cosmetic] “set_custom_code” reworded the description of “context”
- [cosmetic] “set_experiment_note” reworded the description of “context”
- [cosmetic] “set_item_status” reworded the description of “context”
- [cosmetic] “set_primary_custom_domain” reworded the description of “context”
- [cosmetic] “share_section” reworded the description of “context”
- [cosmetic] “start_edit_session” reworded the description of “context”
- [cosmetic] “start_test” reworded the description of “handsOff”
- [cosmetic] “structure_site_apply” reworded the description of “context”
- [cosmetic] “update_site” reworded the description of “context”
- [cosmetic] “update_site” reworded the description of “signature”
- [cosmetic] “submit_feedback” reworded the description of “context”
- [cosmetic] “decline_suggestion” reworded the description of “context”
- [cosmetic] “get_report” reworded the description of “context”
- [cosmetic] “claim_site” reworded the description of “guestToken”
- [cosmetic] “list_form_submissions” reworded the description of “context”
- [cosmetic] “revert_change” reworded the description of “context”
- [cosmetic] “import_site_from_url” reworded the description of “context”
- [cosmetic] “list_votes” reworded the description of “context”
- [cosmetic] “approve_suggestion” reworded the description of “context”
- [cosmetic] “get_account_context” reworded the description of “workspace”
- [cosmetic] “apply_changeset” reworded the description of “guestToken”
- [cosmetic] “support_inbox” reworded the description of “context”
- [cosmetic] “preview_page” reworded the description of “context”
- [cosmetic] “bulk_put_content” reworded the description of “context”
- [cosmetic] “reply_to_report” reworded the description of “context”
- [cosmetic] “adapt_site_apply” reworded the description of “context”
- [cosmetic] “list_versions” reworded the description of “context”
- [cosmetic] “publish_site” reworded the description of “feedback”
- [cosmetic] “list_goals” reworded the description of “context”
- [cosmetic] “get_adapt_status” reworded the description of “context”
- [cosmetic] “get_site_outline” reworded the description of “context”
- [cosmetic] “get_content” reworded the description of “context”
- [cosmetic] “reapply_custom_domain_oauth” reworded the description of “context”
- [cosmetic] “get_form_submission” reworded the description of “context”
- [cosmetic] “get_site” reworded the description of “guestToken”
- [cosmetic] “list_content” reworded the description of “guestToken”
- [cosmetic] “update_collection” reworded the description of “context”
- [cosmetic] “structure_site_analyze” reworded the description of “context”
- [cosmetic] “get_account_context” reworded the description of “context”
- [cosmetic] “inspect_collection” reworded the description of “context”
- [cosmetic] “get_menu” reworded the description of “context”
- [cosmetic] “list_suggestions” reworded the description of “context”
- [cosmetic] “wait_for_custom_domain” reworded the description of “context”
- [cosmetic] “patch_route” dropped the optional parameter “nav”
- [cosmetic] “patch_route” dropped the optional parameter “navLabel”
- [cosmetic] “put_route” dropped the optional parameter “nav”
- [cosmetic] “put_route” dropped the optional parameter “navLabel”
- [cosmetic] “remove_votes” dropped the optional parameter “guestToken”
- [cosmetic] “get_structure_status” dropped the optional parameter “guestToken”
- [cosmetic] “graduate_site” dropped the optional parameter “guestToken”
- [cosmetic] “get_collection_schema” dropped the optional parameter “guestToken”
- [cosmetic] “get_adapt_status” dropped the optional parameter “guestToken”
- [cosmetic] “put_collection_schema” dropped the optional parameter “guestToken”
- [cosmetic] “delete_content” dropped the optional parameter “guestToken”
- [cosmetic] “optimize_assets” dropped the optional parameter “guestToken”
- [cosmetic] “structure_site_analyze” dropped the optional parameter “guestToken”
- [cosmetic] “inspect_collection” dropped the optional parameter “guestToken”
- [cosmetic] “set_custom_code” dropped the optional parameter “guestToken”
- [cosmetic] “start_test” dropped the optional parameter “guestToken”
- [cosmetic] “review_changes” dropped the optional parameter “guestToken”
- [cosmetic] “list_ideas” dropped the optional parameter “guestToken”
- [cosmetic] “list_assets” dropped the optional parameter “guestToken”
- [cosmetic] “delete_asset” dropped the optional parameter “guestToken”
- [cosmetic] “reindex_collection” made “context” optional
- [cosmetic] “patch_route” made “context” optional
- [cosmetic] “add_custom_domain” made “context” optional
- [cosmetic] “list_ideas” made “context” optional
- [cosmetic] “get_site_manifest” made “context” optional
- [cosmetic] “inspect_collection” made “context” optional
- [cosmetic] “rename_content” made “context” optional
- [cosmetic] “get_site_outline” made “context” optional
- [cosmetic] “promote_experiment” made “context” optional
- [cosmetic] “update_collection” made “context” optional
- [cosmetic] “refresh_custom_domain” made “context” optional
- [cosmetic] “put_menu_doc” made “context” optional
- [cosmetic] “verify_page” made “context” optional
- [cosmetic] “preflight_site” made “context” optional
- [cosmetic] “get_adapt_status” made “context” optional
- [cosmetic] “start_test” made “context” optional
- [cosmetic] “adapt_site_apply” made “context” optional
- [cosmetic] “merge_suggestion” made “context” optional
- [cosmetic] “add_idea” made “context” optional
- [cosmetic] “put_menu_item” made “context” optional
- [cosmetic] “put_post” made “context” optional
- [cosmetic] “get_activity” made “context” optional
- [cosmetic] “publish_site” made “context” optional
- [cosmetic] “list_goals” made “context” optional
- [cosmetic] “patch_content” made “context” optional
- [cosmetic] “get_board” made “context” optional
- [cosmetic] “search_content” made “context” optional
- [cosmetic] “put_route” made “context” optional
- [cosmetic] “decline_suggestion” made “context” optional

### 2026-10-31 (score 88, +1)

We did not load change detail this far back for this component, so this day may have recorded more than is shown.

### 2026-10-29 (score 87, +1)

We did not load change detail this far back for this component, so this day may have recorded more than is shown.

### 2026-10-27 (score 86, +1)

We did not load change detail this far back for this component, so this day may have recorded more than is shown.

### 2026-10-26 (score 85, +5)

We did not load change detail this far back for this component, so this day may have recorded more than is shown.

## MCP tools (117)

### `create_site` (~211 tokens)

Create empty site; seeds brief. Globally taken slugs auto-suffix to `{slug}-{adjective}-{noun}` (default); pass onSlugTaken:"fail" to get a 409 instead of a renamed site.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `editingMode` (string): How the editor's Save behaves. 'live' (default) publishes each save immediately; 'staged' keeps saves as drafts until publish_site. Prefer 'staged' when an agent will hand the editor to a person (see…
- `keyConvention` (string)
- `onSlugTaken` (string)
- `purpose` (string)
- `slug` (string, required)
- `title` (string)
- `workspace` (string|null): Workspace org id or slug (multi-workspace only; omit if one workspace).

### `get_site` (~200 tokens)

Site detail, stats, publicUrl, brief head (default-on), and readiness flags. pages[] is a ~25-entry head by default (stats.pages has the true count) — pass pages: true for the full array (capped at 1000), or enumerate everything with list_content/get_site_manifest. Large sites (200+ pages): list_pages is paginated — use cursor or get_site_manifest for the full route set.

Input parameters:

- `brief` (boolean): Include parsed brief head (default true).
- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `pages` (boolean): Return the full pages[] array instead of the default ~25-entry head (server-capped at 1000 either way). Default false.
- `site` (string, required): Site slug, e.g. acme-studio

### `update_brief` (~546 tokens)

Update `_intent/site.md` (site brief) or a page brief when `page` is set. See field descriptions for rules, notes, and head enums.

Input parameters:

- `appendHandoff` (string): Append a hand-off log line — site brief only; ignored when page is set.
- `body` (string): Replace narrative body (site ≤50k chars; page ≤10k).
- `context` (string): Why you're calling this (activity log).
- `goal` (string): Current goal (site ≤4k chars; page ≤2k).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `head`: Head fields to merge. Site: purpose, audience, kind (exploration|production), priorities ({seo,analytics,design,mobile}: must|nice|skip), editingModel (ai|simple-editor|creator), sections, design (st…
- `notes`: Site: free-form ## Notes string (≤4k) OR per-section notes object for Design system / Content & CMS / SEO (null clears one). Page brief: string only (≤2k).
- `page` (string): Content key — targets that page's brief (`_intent/pages/<key>.md`). Omit for the site brief.
- `removeRules` (array): Ids of rules or guidelines to remove (site brief only).
- `rules` (array): Rules & guidelines lines to add or change (upsert by id — send only fields you change; omitted fields keep their stored value; check: null turns a Rule into a Guideline). A supported check (title-max…
- `site` (string, required): Site slug, e.g. acme-studio

### `get_plan` (~65 tokens)

Read-only workspace plan and pooled usage; pass slug for this site's add-ons. Call before a large build.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `slug` (string): Optional site slug — adds a per-site add-ons segment to the summary.

### `get_analytics` (~180 tokens)

Read-only traffic overview (default) or full breakdowns via include[]; optional bot id for crawler page drill-down.

Input parameters:

- `bot` (string): Crawler bot id (e.g. googlebot, gptbot) → that bot's top crawled pages under `crawlerBotPages`.
- `context` (string): Why you're calling this (activity log).
- `include` (array): Full breakdown sections to add under `detail`: pages, sources, countries, devices, browsers, os, utm, goals, series, crawlers, experiments, events, helpful (all-time "Was this helpful?" yes/no per pa…
- `range` (string): Time window; default 30d.
- `site` (string, required): Site slug, e.g. acme-studio

### `set_analytics_provider` (~265 tokens)

Connect a third-party analytics provider (GA4, Plausible, Fathom, Umami) or remove it (provider 'none'). dotsy adds the provider's script to the <head> of every live page — live immediately, no publish needed. Replaces any earlier provider (one per site). Use this instead of pasting a GA4 gtag snippet into a layout. Other tags (Google Tag Manager, ads pixels, Clarity, search-console verification <meta>) go in set_custom_code.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `domain` (string): Plausible only: the site domain registered in Plausible, e.g. example.com.
- `host` (string): Umami only: https origin of the Umami instance, e.g. https://umami.example.com.
- `measurementId` (string): GA4 only: measurement ID, e.g. G-ABC123XYZ.
- `provider` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio
- `siteId` (string): Fathom only: site ID (uppercase letters/digits).
- `websiteId` (string): Umami only: website ID (UUID).

### `get_custom_code` (~99 tokens)

Read the site-wide custom code: `draft` (what the next publish puts live) and `published` (what visitors get now) for both boxes — head (before </head>) and body (before </body>) — plus `pending`, `runOn` and `maxChars`.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `set_custom_code` (~314 tokens)

Set site-wide custom code: `head` goes before </head> and `body` before </body> on every page, on every layout. It is saved as a draft and goes live with the next publish_site (on a live-mode site the dashboard publishes only the code). It never runs in the editor. Up to 20,000 characters per box, stored as sent; `warnings` flags likely mistakes (unclosed <script>, <html>/<head>/<body> tags, a canonical or robots tag, GA4 already connected) without rejecting. Send only the boxes you change; "" clears a box. `runOn` ('live' = live site only, default; 'live-and-preview' = also in the editor's preview) applies at once — agents limited to drafts can't set it. Use for tag managers (GTM), Clarity, ads pixels, chat widgets and verification tags. For plain GA4, Plausible, Fathom or Umami prefer set_analytics_provider.

Input parameters:

- `body` (string): Code for before </body> on every page. "" clears it.
- `context` (string): Why you're calling this (activity log).
- `head` (string): Code for before </head> on every page. "" clears it.
- `runOn` (string): Where it runs: 'live' (live site only) or 'live-and-preview'. Applies at once.
- `site` (string, required): Site slug, e.g. acme-studio

### `update_site` (~593 tokens)

Update site settings: title, keyConvention, or editingMode ('live' = each editor save publishes immediately; 'staged' = saves stay drafts until publish_site). Slug is the hostname — change it with rename_site, not this tool. Set 'staged' before handing the editor to a person via start_edit_session's draftEditorUrl. signature toggles the "Made with dotsy" badge ({enabled, mark: logo|dot, reveal: pill|seal, side: right|left}); it is live immediately. trailingSlash (ignore|never|always) sets the page URL form: the other form 301s to it and canonicals/sitemap use it; live immediately. ownSeoFiles: true lets the next publish use the site's own robots.txt that blocks the whole site (Disallow: /) and its own static sitemap.xml (replaces the generated one, collection pages included); a robots.txt without Disallow: / is used anyway. aiTrain (true or false) says whether AI training crawlers may use the live site; false asks them not to, true allows them (the default). Live immediately.

Input parameters:

- `aiTrain` (boolean): Whether AI training crawlers may use the live site. true (the default) allows them. false asks them not to. Live immediately, no publish needed. Same callers who can set trailingSlash can set this.
- `context` (string): Why you're calling this (activity log).
- `editingMode` (string): How the editor's Save behaves. 'live' (default) publishes each save immediately; 'staged' keeps saves as drafts until publish_site. Prefer 'staged' when an agent will hand the editor to a person (see…
- `keyConvention` (string)
- `ownSeoFiles` (boolean): Use the site's own robots.txt and sitemap.xml even when they can hurt search: a robots.txt that blocks the whole site (Disallow: /), and a static sitemap.xml, which replaces the generated one (collec…
- `signature` (object): "Made with dotsy" signature badge. Takes effect on the live site immediately, no publish needed.
- `site` (string, required): Site slug, e.g. acme-studio
- `title` (string)
- `trailingSlash` (string): Page URL form. 'ignore' (default): /blog and /blog/ both serve the page. 'never': /blog/ 301s to /blog. 'always': /blog 301s to /blog/. The canonical tag and sitemap use the same form. Live immediate…

### `rename_site` (~182 tokens)

Rename the site slug (public hostname {slug}.dotsy.site). Owner-only. After success: use the returned slug in every later tool. Published site: the old slug redirects on the API (301 for reads, 308 for writes) while it stays reserved; the old host 301s and stays reserved until delete or rename-back. Never published: the old slug 404s on the API and the old hostname is released (no 301; create_site can reuse it). Analytics history stays on the old slug. HTML absolute URLs are not rewritten — publish_site refreshes sitemap; find_replace if stored HTML must change.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `newSlug` (string, required): New site slug (lowercase hostname stem)
- `site` (string, required): Site slug, e.g. acme-studio

### `publish_site` (~235 tokens)

Publish draft to live. handoff required; feedback optional.

Input parameters:

- `acknowledgeSeoWarnings` (boolean): Required when brief.priorities.seo is must and verify_page reported SEO errors (not warnings). User must have explicitly chosen to publish despite errors.
- `confirmedDraftFingerprint` (string): Required when get_draft_status returned requiredDecision (publish_confirm). Must match the current draft fingerprint from get_draft_status.
- `context` (string): Why you're calling this (activity log).
- `feedback` (object): Optional session feedback; tool intents are attached automatically.
- `handoff` (string, required): One sentence: decisions, preferences, or open threads for the next session (appended to the brief).
- `onlyReady` (boolean): Publish only changes marked ready (server hashes). Incompatible with pages. Skips publish_confirm fingerprint when set.
- `pages` (array): Publish only these pages — use when a publish returns rules_not_fully_checked; publish the rest in further batches (≤50 route paths, e.g. /pricing).
- `site` (string, required): Site slug, e.g. acme-studio

### `get_draft_status` (~64 tokens)

Pending draft changes: content keys and routes with status. May include requiredDecision (publish_confirm) before publish_site.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `review_changes` (~257 tokens)

Review queue: what changed and isn't live yet. list (counts first) → get one item (hidden changes, optional diff, bodyChanged: false means only title/meta/route fields changed — review hidden, not the visible page) → mark_ready / unmark (≤50 ids). Then publish_site onlyReady:true. Drafts-only sites: a person marks.

Input parameters:

- `action` (string): list (default) · get one item · mark_ready · unmark
- `all` (boolean): unmark: clear every ready mark
- `context` (string): Why you're calling this (activity log).
- `cursor` (string): list pagination (full only)
- `detail` (string): list detail (default summary)
- `diff` (boolean): get: include first diff rows
- `dryRun` (boolean): mark_ready / unmark: preview only
- `id` (string): get: item id from list (p:/path, s:key, m:menus)
- `ids` (array): mark_ready / unmark: item ids from list (≤50)
- `site` (string, required): Site slug, e.g. acme-studio
- `state` (string): list filter (default all)

### `discard_drafts` (~114 tokens)

Discard unpublished drafts: all (omit key/keys/routes/menus), one content key (key), menu draft only (menus:true), or a selection (keys, routes). Destructive — check get_draft_status first.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `key` (string)
- `keys` (array)
- `menus` (boolean)
- `routes` (array)
- `site` (string, required): Site slug, e.g. acme-studio

### `delete_site` (~58 tokens)

Permanently delete site; confirmSlug must equal slug.

Input parameters:

- `confirmSlug` (string, required)
- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `bulk_put_content` (~110 tokens)

Stage many content keys (pages, layouts, fragments). Content only — not routes or assets. Each body ≤60KB and the whole call ≤80KB; larger pages use stage_content_chunk, dotsy push, or PUT content-raw. May queue above the inline threshold.

Input parameters:

- `content` (array, required)
- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio
- `title` (string)

### `get_site_manifest` (~104 tokens)

Content keys + hashes only (optional routes=true) — static files live in list_assets, not manifest. Diff keys+hashes before apply_changeset; stage only changed bodies.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `cursor` (string)
- `limit` (integer)
- `mode` (string)
- `routes` (boolean)
- `site` (string, required): Site slug, e.g. acme-studio

### `find_replace` (~115 tokens)

Case-insensitive find→replace across pages in one staged commit. Batch all replacements into ONE call — never parallelize find_replace on the same key (concurrent edits surface as per-key conflict). find/replace ≤60KB.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `expectPerKey` (integer)
- `find` (string, required)
- `keys` (array)
- `replace` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `search_content` (~78 tokens)

Find pages containing a phrase (grep without reading bodies). Requires do-sqlite — graduate_site first if search_unavailable.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `limit` (integer)
- `q` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `graduate_site` (~349 tokens)

Enable site search & collections (kv → do-sqlite), one-way. Unlocks search_content, collection indexes, visitor /__paige/search, and moves any R2 form-submission ledger into SiteDb. 409 blocked covers three unrelated gates: an unsupported source backend (no override, ever); strictDrafts — default true, this client sends true unless you pass strictDrafts:false — blocking on unpublished drafts or unrecovered KV content rows; or a KV snapshot that looks incomplete (e.g. a page deleted in the last couple of minutes). force:true overrides ONLY that last, incomplete-snapshot gate — never the backend or strictDrafts gates. The blocked response's `reason` names which gate fired, and `audit` (+ `audit.warnings`) is aggregate COUNTS only (published/route/pending-draft counts) — never row keys — so judge the size of the gap, not a row list, before forcing. The flip is one-way: anything outside the frozen snapshot is unreachable once it lands.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `force` (boolean): Proceed past the incomplete-KV-snapshot 409 only — not the unsupported-backend or strictDrafts blocks, which force does nothing for. Read the blocked response's `reason` + `audit` first; the flip is…
- `site` (string, required): Site slug, e.g. acme-studio
- `strictDrafts` (boolean): Block on unpublished drafts or unrecovered KV content rows (default true — publish/discard first, or pass false to proceed anyway).

### `apply_changeset` (~286 tokens)

Stage writes[] + deletes[] in one call; attach routes via writes[].route. writes[].ifMatch (hash from get_site_manifest; "" = must not exist) guards each key (a delete takes { key, ifMatch } too); atomic:true makes one stale key write nothing (409 changeset_conflict). At most 10 writes; each body ≤60KB and the call ≤80KB. Larger new pages: stage_content_chunk then writes[].bodyStaging, or dotsy push / PUT content-raw.

Input parameters:

- `atomic` (boolean): All or nothing: every writes[].ifMatch and deletes[].ifMatch is checked before anything is written. One stale key writes nothing and the call fails with 409 changeset_conflict listing `conflicts`. De…
- `context` (string): Why you're calling this (activity log).
- `deletes` (array): Keys to delete: a key, or { key, ifMatch } (the hash you read) so a page changed since is not deleted (listed in conflicts).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `message` (string)
- `site` (string, required): Site slug, e.g. acme-studio
- `writes` (array)

### `import_site_from_url` (~210 tokens)

Import URL to draft. On complete, response may include requiredDecision — preview then ask mirror vs editable before structure_*.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `keyConvention` (string): Content key shape: directory = Astro dist (about/index.html); default flat
- `mode` (string): Default auto — per-page mirror with silent render escalation
- `onSlugTaken` (string)
- `pages` (array): Explicit page list; omit to import sourceUrl as index.html
- `segments` (array): Split-origin segments — each expands into pages by fetching its own `sitemap`. A segment without `sitemap` imports only its pathPrefix hub page (with a warning), not the whole prefix.
- `site` (string, required): Site slug, e.g. acme-studio
- `sourceUrl` (string, required)
- `workspace` (string|null): Workspace org id or slug (multi-workspace only; omit if one workspace).

### `get_structure_status` (~61 tokens)

Flat vs structured page counts; informs user — do not auto-run analyze/apply while requiredDecision pending.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `structure_site_analyze` (~172 tokens)

Propose shared layout + fragments; persist:false first. Wait for mirror-vs-editable answer if requiredDecision pending.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `full` (boolean): Return the full manifest incl. per-page HTML samples (default: compacted — etag/action/counts kept, samples truncated — to fit the token budget).
- `persist` (boolean): First call: false (preview only). true only after user confirms editable/CMS mode.
- `profileId` (string)
- `refreshManifest` (boolean): Replace persisted manifest when true.
- `sampleLimit` (integer)
- `scope` (string): Default flat-only; analyze clusters flat keys only.
- `site` (string, required): Site slug, e.g. acme-studio

### `structure_site_apply` (~212 tokens)

Rewrite flat pages to layout + fragments (draft). Do not call while requiredDecision pending — user must pick editable first. It extracts header/footer only: afterwards share_section with no arguments lists the other repeated blocks (banner, CTA).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `ifMatch` (string, required): Manifest etag from structure_site_analyze or get_structure_status.
- `keys` (array): Required when scope is keys.
- `mode` (string): Default apply. Neither value is a dry run — review-only persists the same content/layout write as apply. Only pass review-only after a review_required error (low analyze confidence); it labels the re…
- `overrides` (object)
- `restructure` (boolean): Required true when scope all and site has structured pages.
- `scope` (string)
- `site` (string, required): Site slug, e.g. acme-studio

### `get_structure_result` (~74 tokens)

Last structure apply job result (structuredKeys, conflicts, partial).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `full` (boolean): Return the full manifest/detail (default: compacted for the token budget).
- `site` (string, required): Site slug, e.g. acme-studio

### `adapt_site_analyze` (~174 tokens)

Propose nav wiring + blog collectionization for an imported mirror (draft, opt-in). Read-only; returns per-adapter reports + proposalEtag. Nav decline may include nextStep (e.g. mega-menu → edit fragments/header.html). Wait for mirror-vs-editable answer if requiredDecision pending.

Input parameters:

- `adapters` (array): Which adapters to analyze (default: both).
- `context` (string): Why you're calling this (activity log).
- `full` (boolean): Return the full per-member harvest report (default: compacted for the token budget).
- `options` (object)
- `refreshProposal` (boolean): Replace an existing UNCONSUMED proposal (otherwise re-analyze 409s proposal_exists).
- `site` (string, required): Site slug, e.g. acme-studio

### `adapt_site_apply` (~209 tokens)

Apply the adapt proposal (draft): wire nav into the menu store and/or rewrite the blog index to a live <dotsy-collection>. One undo covers the whole apply; a second apply is refused until adapt_site_revert (or force). Queued applies are polled to completion.

Input parameters:

- `adapters` (array, required): Which analyzed adapters to apply.
- `context` (string): Why you're calling this (activity log).
- `dryRun` (boolean): Full computation, would-write set + previews, ZERO writes, no undo.
- `force` (string): An undo record from a prior apply exists (409 adapt_undo_exists): defer-discard it — the prior record is dropped only when this apply SUCCEEDS; a partial-failure rollback reinstates it. Prefer adapt…
- `ifMatch` (string, required): The proposal etag from analyze (or GET …/adapt/proposal / status).
- `site` (string, required): Site slug, e.g. acme-studio

### `adapt_site_revert` (~71 tokens)

Atomically restore everything the last adapt_site_apply wrote (fragments, the menu doc when the op touched it, index, member configs, routes).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `get_adapt_status` (~76 tokens)

Read adapt state: proposal summary + proposalEtag (+ applied/consumed flag), undo-record presence, active or queued job progress. Use to resume in a fresh session.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `list_content` (~262 tokens)

List content keys (HTML/md); not assets — use list_assets. Defaults to 50 keys per page — raise limit (max 1000) or pass cursor (from nextCursor) to page through the rest. Per-row `size`/`missing` are measured automatically only on pages of ≤100 rows; past that they're ABSENT (not measured, never `null`) unless you pass withSize:true, which forces measurement at a cost of one storage read per returned row. Only `size: null` + `missing: true` means the page's stored bytes are gone.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `cursor` (string): Resume from a prior response's nextCursor.
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `limit` (integer): Keys per page (default 50, max 1000).
- `site` (string, required): Site slug, e.g. acme-studio
- `withSize` (boolean): Force size/missing measurement on a page bigger than 100 rows (costs one storage read per returned row). Default false — rows in a ≤100-row page are measured either way.

### `get_content` (~127 tokens)

Read one content key; optional selector (region) or published=true (live body). Bodies over 40KB without a selector return a truncated summary — use selector, batch_patch_content, or dotsy pull.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `key` (string, required)
- `published` (boolean)
- `selector` (string)
- `site` (string, required): Site slug, e.g. acme-studio

### `put_content` (~187 tokens)

Stage one content key. Body ≤60KB. Larger pages: stage_content_chunk + apply_changeset bodyStaging, dotsy push, or PUT content-raw. Edits to an existing large page: batch_patch_content.

Input parameters:

- `body` (string, required)
- `contentType` (string)
- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `key` (string, required): Content key: a page (about.html, blog/post.html), a layout (layouts/<name>.html) or a fragment (fragments/<name>.html). A <dotsy-fragment name> tag and a route's layout use the bare name, not the key.
- `site` (string, required): Site slug, e.g. acme-studio

### `stage_content_chunk` (~126 tokens)

Append one ≤60KB chunk of a large new page. Omit stagingId on seq 0; pass the returned stagingId on later seq. Set finalize:true on the last chunk, then apply_changeset with writes[].bodyStaging.

Input parameters:

- `body` (string, required)
- `contentType` (string)
- `context` (string): Why you're calling this (activity log).
- `finalize` (boolean)
- `seq` (integer, required)
- `site` (string, required): Site slug, e.g. acme-studio
- `stagingId` (string)

### `patch_content` (~109 tokens)

Edit one content key via ops[] (selector ops or replace-string; wrap/unwrap/move restructure siblings). Stages draft; op shapes in schema.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `key` (string, required)
- `ops` (array, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `batch_patch_content` (~109 tokens)

patch_content on multiple keys; all-or-nothing validation. Preferred edit path for pages over 60KB. replace-string value/replace ≤60KB; whole call ≤80KB.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `items` (array, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `delete_content` (~83 tokens)

Delete content key (tombstone if published). A fragments/* section that pages still place is refused with section_in_use (lists the pages) — detach it or remove it first.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `key` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `rename_content` (~372 tokens)

Re-key a page by slug (draft or published static). Server-side hash copy — no body round-trip. Default deletes static routes (obscure-URL); movePath:true repoints the route, rewrites menu refs and other redirects aimed at the old path, rewrites root-relative and https://{slug}.dotsy.site/ hrefs in draft HTML (always linksRewritten; not custom-domain absolutes or script/code samples; linksRewriteTruncated when scan caps at 100 HTML keys (500 on sites with full content search); publish to ship), and — only for an already-PUBLISHED page — leaves a 301 there too (leaveRedirect, default true; no effect on a never-published page, or when movePath is false). Retry-safe: a part-way failure (409 rename_failed) rolls everything back, so resend the same call. On fragments/<name>.html it also rewrites every <dotsy-fragment name> reference and hide marker in the same all-or-nothing commit. Then publish_site.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `key` (string, required): Current content key, e.g. dashboard.html
- `leaveRedirect` (boolean): With movePath:true on an already-published page, leave a 301 at the old path (default true). false removes the old path outright — old links break. No effect when movePath is false, or on a page that…
- `movePath` (boolean): false (default): delete routes bound to old key. true: move static route path to match slug.
- `newSlug` (string, required): New filename stem; directory prefix preserved (blog/foo → blog/bar)
- `site` (string, required): Site slug, e.g. acme-studio

### `put_post` (~148 tokens)

Stage a blog post via structured meta+body (?format=post). Key e.g. blog/my-post.html. Declared membership: meta.collections; schedule: meta.visibleAt; SEO title override: meta.seoTitle (card always shows meta.title); custom fields: meta.custom (typed per collection by get_collection_schema; the response's fieldWarnings flag values that don't fit — soft, never blocking).

Input parameters:

- `body` (string, required)
- `context` (string): Why you're calling this (activity log).
- `head` (array)
- `key` (string, required)
- `meta` (object, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `inspect_collection` (~318 tokens)

Collection state — count, server.mode, sample entries, tag (membershipKind, match=) when discovered; plus name, noun, URL pattern, layout, where it's listed, and post fields used. Pass status/q/cursor/limit (any one, even empty) for a full items[] page instead: status draft = never published; a published post with unpublished edits is status published with hasChanges:true (never draft) and shows its staged title/date/tags/author; scheduled = future visibleAt. Never-published drafts come from staged content (no extra reads, no publish needed to see them); published/scheduled come from the index. limit defaults to 50, clamped 1-200. Omit all four for just the base inspect (no items[]).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `cursor` (string): Resume from a prior response's nextCursor.
- `limit` (integer): Items per page (default 50, max 200).
- `prefix` (string, required): Collection id or prefix, e.g. "posts" or "blog/" — use the key exactly as list_collections returned it.
- `q` (string): Case-insensitive title search within items[].
- `site` (string, required): Site slug, e.g. acme-studio
- `status` (string): Filter items[] by status (see the tool description for what each means). Providing this (or q/cursor/limit) is what makes the response include items[].

### `list_collections` (~102 tokens)

Collections on the site — name, noun, item count (or 'not published yet'), URL pattern, and whether any member is staged. Includes a collection only a draft page lists. Returns a bare array (not wrapped in an object) — pass each row's key verbatim to inspect_collection/update_collection.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `update_collection` (~205 tokens)

Rename a collection's display name and/or noun (e.g. the generic 'posts' key shown as 'Recipes' / 'recipe') — cosmetic only, stored separately from the tag that defines membership; doesn't need publish. Provide at least one of name/noun. null (or "") resets that field back to its derived default — different from omitting it, which leaves the current value alone.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `key` (string, required): Collection key exactly as list_collections returned it, e.g. "posts" or "blog/" (a hand-typed key may 404 or get silently normalized).
- `name`: Display name (≤80 chars). null resets to the derived name.
- `noun`: Singular noun for one item, e.g. "post" (≤40 chars). null resets to the derived noun.
- `site` (string, required): Site slug, e.g. acme-studio

### `new_post_template` (~122 tokens)

Read-only scaffold for a new post — suggested key/path, membership meta, blank or duplicate body. Then put_post (+ optional put_route).

Input parameters:

- `collection` (string, required): Collection id or prefix, e.g. "posts" or "blog/"
- `context` (string): Why you're calling this (activity log).
- `mode` (string): blank (default) or duplicate full body
- `site` (string, required): Site slug, e.g. acme-studio
- `templateKey` (string): Exemplar content key; default = most recent member

### `get_collection_schema` (~127 tokens)

Read the site's collection field definitions (`_data/collections.json`, draft view): { schema: { v: 1, collections: { "<key>": { fields[], settings? } } }, hash }. hash is null when no schema exists yet — pass it as put_collection_schema ifMatch. 409 collection_schema_invalid { hash, issues } means the stored file is broken: fix it with put_collection_schema using that hash.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `put_collection_schema` (~351 tokens)

Replace the whole field schema (draft; publish makes it live). Read it with get_collection_schema first and send it back edited. Collection keys exactly as list_collections returns them ("blog/" or an id). Field: { key, type, label?, help?, required?, unique?, inCard?, default? } — type text (multiline, minLength, maxLength) | number (min, max, integer) | boolean | option (options[] required) | multiOption (options[] required, max) | link | email | phone | color. Keys: letter then letters/digits/_ (≤40), not a built-in post field (title, date, excerpt, tags, image, url, slug, …; author only as text), not secret-looking. ≤60 fields per collection. settings.defaultSort: date:desc|date:asc|title:asc|slug:asc. Removing a collection's entry drops its definitions (values stay on the posts). Values live in put_post meta.custom; writes then return fieldWarnings (soft, never blocking). 400 invalid_collection_schema lists issues; 409 conflict = stale ifMatch.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `ifMatch` (string|null): hash from get_collection_schema (stale → 409 conflict). null = the schema must not exist yet. Omit to write unconditionally.
- `schema` (object, required): Whole schema file: { "v": 1, "collections": { "<key>": { "fields": [...], "settings"?: {...} } } }.
- `site` (string, required): Site slug, e.g. acme-studio

### `set_item_status` (~188 tokens)

Change a roadmap/board item status (stamps dates; on release drafts the changelog post and links both ways). Never hand-edit *At, daysToRelease, or votes.

Input parameters:

- `at` (string): ISO date for the status stamp (backfill).
- `collection` (string): Board collection (default "roadmap").
- `context` (string): Why you're calling this (activity log).
- `draftChangelog` (boolean): Default true when the board defines a changelog collection.
- `key` (string, required): Content key, e.g. roadmap/my-feature.html
- `publish` (boolean): Publish the touched keys after staging.
- `reply` (string): Decline reply when moving to an archived state.
- `site` (string, required): Site slug, e.g. acme-studio
- `status` (string, required): One of the board states from get_collection_schema.

### `get_board` (~85 tokens)

List board items with status, stamp fields, and baked vote counts (draft overlay included). Live vote totals: site host GET /__dotsy/votes.

Input parameters:

- `collection` (string): Board collection (default "roadmap").
- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `list_votes` (~83 tokens)

List voters on one board item (voter id + source only — no email).

Input parameters:

- `collection` (string, required): Board collection id.
- `context` (string): Why you're calling this (activity log).
- `item` (string, required): Content key, e.g. roadmap/my-feature.html
- `site` (string, required): Site slug, e.g. acme-studio

### `remove_votes` (~75 tokens)

Remove one voter's vote and drop their release subscription for that item.

Input parameters:

- `collection` (string, required)
- `context` (string): Why you're calling this (activity log).
- `item` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio
- `voterId` (string, required)

### `list_suggestions` (~66 tokens)

List form suggestions awaiting moderation (duplicate hints against the board).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio
- `status` (string): Filter by status (default "new").

### `moderate_suggestion` (~176 tokens)

Decide one form suggestion from list_suggestions: approve (new draft board item; stages content, does not publish), merge (into an existing board item, subscribes the voter/email) or decline (no board item).

Input parameters:

- `body` (string): approve only.
- `context` (string): Why you're calling this (activity log).
- `decision` (string, required)
- `id` (string, required)
- `itemKey` (string): merge: the existing board item's key (required).
- `reason` (string): decline: note kept with the suggestion.
- `site` (string, required): Site slug, e.g. acme-studio
- `slug` (string): approve only.
- `status` (string): approve: initial board status.
- `title` (string): approve: item title (default: the suggestion's).

### `add_idea` (~127 tokens)

Add an idea from outside the site (draft board item or inbox suggestion). Idempotent on externalRef.

Input parameters:

- `area` (string)
- `asSuggestion` (boolean)
- `board` (string)
- `context` (string): Why you're calling this (activity log).
- `description` (string, required)
- `externalRef` (object)
- `requester` (object)
- `site` (string, required): Site slug, e.g. acme-studio
- `source` (string, required)
- `status` (string)
- `title` (string, required)

### `install_product_kit` (~75 tokens)

Add a roadmap, changelog and/or help centre to a site as drafts (never overwrites; rerun finishes a partial install).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `parts` (array, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `list_ideas` (~68 tokens)

Search draft/published board items and open suggestions by title token overlap.

Input parameters:

- `board` (string)
- `context` (string): Why you're calling this (activity log).
- `q` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `infer_collection_schema` (~131 tokens)

Suggest fields for one collection from a sample of its items' meta.custom values (published data, else drafts) — writes nothing. Returns { sampled, suggestions: [{ field: { key, type, options? }, count, samples, defined }], skipped }. Review, then add the ones you want via put_collection_schema.

Input parameters:

- `collection` (string, required): Collection key exactly as list_collections returned it, e.g. "blog/" or "posts".
- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `reindex_collection` (~56 tokens)

Build/finish collection indexes after publish returns collectionDataPending or collectionSqlPending.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `preview_page` (~178 tokens)

Verify-only composed HTML check; home preview may include requiredDecision — ask mirror vs editable before structure_*.

Input parameters:

- `contains` (array): Assert substrings present in composed HTML.
- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `notContains` (array): Assert absent — e.g. `<dotsy-` for unresolved template tags.
- `path` (string)
- `selector` (string): Return matched elements only (tag/attrs/text).
- `site` (string, required): Site slug, e.g. acme-studio
- `variant` (string): A/B variant arm to compose (e.g. "b") — same as ?dotsy-variant= on the public URL.

### `verify_page` (~210 tokens)

Pre-publish lint (tags, assets, layout, SEO, speed and accessibility). Summary first: health issues grouped by code, each with fix:{tool,args,fill?} — supply the fill args, then call. health.summary counts are whole-site (groups/codes filter only issues and assets). Pair with preview_page — see get_skill('verify').

Input parameters:

- `codes` (array): Only these health codes, e.g. perf_image_heavy.
- `context` (string): Why you're calling this (activity log).
- `detail` (string): summary (default): grouped by code, clean pages counted. full: per-page lists.
- `groups` (array): Only these groups: speed = perf_*, a11y = a11y_*, links = link_* + content_*. Omit for all.
- `paths` (array): Routes to check; omit to verify every route on the site.
- `site` (string, required): Site slug, e.g. acme-studio

### `preflight_site` (~102 tokens)

Plan check before publish or large build (counts, route soft limit, seo suggestions). When ok is false, blocking lists the failed suggestion codes. Read-only.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `requiredAssets` (integer): How many assets you plan to upload
- `requiredRoutes` (integer): How many routes you plan to add
- `site` (string, required): Site slug, e.g. acme-studio

### `start_edit_session` (~83 tokens)

Open a draft session. Returns preview/edit URLs for the person (see linkGuidance). draftEditorUrl only when the site gives AI agents Full access. path defaults to '/'.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `path` (string)
- `site` (string, required): Site slug, e.g. acme-studio

### `get_site_outline` (~141 tokens)

Site shape: every layout and shared section (`sections`: fragments/* by name, with page count, whether the file exists and staged-change flag), whether a layout is missing, redirects count, menus grouped by where they show, and collections with their page placements. Use for 'is my layout missing', 'which shared sections exist and where', 'where does this menu show' — without listing every page. Read-only; indexing mirrors list_pages (a freshly imported/graduated site may still be indexing).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `share_section` (~656 tokens)

Make a block that repeats on several pages one shared section, or change where one shows. No arguments: lists blocks repeated on 3+ pages. sourceKey + selector (or selectors: a sibling run, one wrapper): a dry run finds every page with it; dryRun:false shares it. name + scope/hide/show: move an existing section to/from every page, or hide/show it per page. Over the per-call cap (too_many_pages, nothing written): share with keys, then again under the same name from a page that still has a copy; for every page then move with name + scope every_page + keys (max-1 pages a call; put all hide on the first) until all are moved. Always a draft (publish_site to go live; undo: revert_change).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `dryRun` (boolean): Default true: report what would change and write nothing. false: stage the change as a draft (publish_site makes it live).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `hide` (array): every_page: pages that should not show it (they keep their own copy). Send them with the first move, never later.
- `ifMatch` (string): The page's content hash (analyze's sourceEtag). If the page changed since, nothing is written.
- `keepLayout` (boolean): With selectors: wrapper gets style="display:contents" so a flex/grid parent lays the run out as before. Default true.
- `keys` (array): Pages with an exact copy to share it on (analyze's matches.keys). Omit for every exact copy. The source page is always included.
- `name` (string): With sourceKey + selector: the new section's name (default: proposed). Alone: the existing section to re-place.
- `scope` (string): where_found: each page that has it gets a reference. every_page: one slot in the layout, at the top or bottom of the page content; pages listed in `hide` don't show it.
- `selector` (string): CSS selector of the element in the page's stored HTML (the editor's selector, or one checked with get_content?selector=). It must match exactly one element.
- `selectors` (array): Instead of selector: a run of elements next to each other in one parent, in page order (each matches one element). Shared as one section inside a wrapper, on the pages with the same run.
- `show` (array): Pages that should show it again (removes their marker).
- `similar` (array): Pages with a similar copy (analyze's similar.keys) that take the shared version, losing their own wording. Default none.
- `site` (string, required): Site slug, e.g. acme-studio
- `sourceKey` (string): Content key of the page the element is on, e.g. 'about.html'.
- `wrapTag` (string): With selectors: the wrapper. Default footer when the run ends the page content, else section.

### `detach_section` (~222 tokens)

Give one page its own copy of a shared section; every other page keeps the shared one. A draft — publish_site to go live. A section the layout places is hidden on that page instead (share_section name + hide).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `dryRun` (boolean): Default true: report what would change and write nothing. false: stage the change as a draft (publish_site makes it live).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `ifMatch` (string): The page's content hash. If the page changed since, nothing is written.
- `name` (string, required): Section name. Its file is fragments/<name>.html, placed with <dotsy-fragment name="<name>">.
- `pageKey` (string, required): Content key of the page that gets its own copy, e.g. 'pricing.html'.
- `site` (string, required): Site slug, e.g. acme-studio

### `get_route` (~81 tokens)

Get one route by path (home is '/').

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `path` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `put_route` (~359 tokens)

Create/replace route; contentKey derived from path if omitted. On a field that accepts null (contentKey, redirectTo, redirectStatus, preserveQuery), sending null clears it back to its default.

Input parameters:

- `contentKey`: Explicit content key (defaults to a path-derived key when omitted). null clears an explicit key back to that path default — same as omitting it, but usable on a PATCH to undo a prior set.
- `context` (string): Why you're calling this (activity log).
- `experiment`
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `layout` (string|null): Layout NAME, bare: "default" for layouts/default.html — not the file key. It must already exist as layouts/<name>.html (400 layout_not_found otherwise). null removes the layout.
- `noindex` (boolean|null): Exclude from sitemap/RSS; inject noindex meta (0.69).
- `path` (string, required)
- `preserveQuery` (boolean|null): Append the visitor's original query string to the redirect target (default true on a redirect). null clears an explicit override.
- `purpose` (string|null)
- `redirectStatus`: 301 permanent / 302 or 308 temporary (default 301 on a redirect). null clears an explicit override.
- `redirectTo`: Redirect target, a root-relative path (/…). Required when purpose is "redirect". null clears it — only valid once purpose stops being "redirect" (a redirect route always needs a target).
- `site` (string, required): Site slug, e.g. acme-studio
- `title` (string|null)
- `vars`

### `patch_route` (~434 tokens)

Partial route update (only sent fields change; an OMITTED field is left alone). On a field that accepts null (contentKey, redirectTo, redirectStatus, preserveQuery), sending null CLEARS it back to its default — different from omitting it. To set redirectTo/redirectStatus, include purpose: "redirect" in the SAME call — the check looks only at what's sent, not the route's stored purpose, so a PATCH that sets redirectTo without repeating purpose: "redirect" (even on a route that's already a redirect) 400s invalid_body.

Input parameters:

- `contentKey`: Explicit content key (defaults to a path-derived key when omitted). null clears an explicit key back to that path default — same as omitting it, but usable on a PATCH to undo a prior set.
- `context` (string): Why you're calling this (activity log).
- `experiment`
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `layout` (string|null): Layout NAME, bare: "default" for layouts/default.html — not the file key. It must already exist as layouts/<name>.html (400 layout_not_found otherwise). null removes the layout.
- `noindex` (boolean|null): Exclude from sitemap/RSS; inject noindex meta (0.69).
- `path` (string, required)
- `preserveQuery` (boolean|null): Append the visitor's original query string to the redirect target (default true on a redirect). null clears an explicit override.
- `purpose` (string|null)
- `redirectStatus`: 301 permanent / 302 or 308 temporary (default 301 on a redirect). null clears an explicit override.
- `redirectTo`: Redirect target, a root-relative path (/…). Required when purpose is "redirect". null clears it — only valid once purpose stops being "redirect" (a redirect route always needs a target).
- `site` (string, required): Site slug, e.g. acme-studio
- `title` (string|null)
- `vars`

### `rename_page` (~374 tokens)

Change a page's URL: moves its route from `from` to `to`. Menu links and other redirects that pointed at the old path retarget automatically; a 301 is left at the old path by default (leaveRedirect). Root-relative and https://{slug}.dotsy.site/ hrefs in draft page, layout and fragment HTML are rewritten (always linksRewritten; not custom-domain absolutes or script/code samples; linksRewriteTruncated when scan caps at 100 HTML keys (500 on sites with full content search); publish to ship). Retry-safe (resend the same call after any error). Errors: 404 route_not_found (from isn't a route); 400 reserved_route (to is under /__dotsy/) or invalid_body (missing/malformed from or to); 422 rename_same_path (from equals to already), rename_pattern_route (from or to contains a :param segment — pattern routes can't be renamed this way), rename_redirect_route (from is itself a different redirect — edit its redirectTo instead); 409 rename_experiment_running (an A/B test is running on from — end it first) or rename_target_exists (to is already a different route — unless to already serves the exact content from would, which is treated as already done and returns 200). Then publish_site.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `from` (string, required): Current route path, e.g. /old-page
- `leaveRedirect` (boolean): Leave a 301 from the old path to the new one (default true). false removes the old path outright — old links break.
- `site` (string, required): Site slug, e.g. acme-studio
- `to` (string, required): New route path, e.g. /new-page

### `delete_route` (~52 tokens)

Remove a route by path.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `path` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `list_goals` (~57 tokens)

List site conversion goals (pageview paths and form submits). Required before start_test.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `put_goals` (~67 tokens)

Create or replace all site conversion goals (≤50). Required before start_test when no goal exists.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `goals` (array, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `estimate_test` (~70 tokens)

Estimate days to reach promote readiness for a page (traffic baseline). Read-only.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `path` (string, required): Route path, e.g. /pricing
- `site` (string, required): Site slug, e.g. acme-studio

### `start_test` (~220 tokens)

Start a same-URL A/B test (draft or live source). Rules → get_skill('verify') § A/B.

Input parameters:

- `changeNote` (string): One line on what the variant changes, shown on the test card (≤120 chars).
- `context` (string): Why you're calling this (activity log).
- `goalId` (string, required)
- `handsOff` (boolean): Hands-off mode (0.56e M4): dotsy finishes the test WITHOUT further user instruction — the LIVE SITE may change later on its own (a ≥95% winner is auto-promoted, old page kept as a draft; or after 30…
- `hypothesis` (string)
- `name` (string)
- `path` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio
- `source` (string, required)
- `weights` (array)

### `promote_experiment` (~96 tokens)

Promote a winning A/B variant to live — explicit user instruction only; see get_skill('verify') § A/B.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `experimentId` (string, required)
- `loserDisposition` (string)
- `site` (string, required): Site slug, e.g. acme-studio
- `variantId` (string, required): Winning variant arm id (usually "b").

### `delete_draft_test` (~65 tokens)

Remove a draft A/B test (releases variant bodies; does not publish).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `experimentId` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `set_experiment_note` (~90 tokens)

Record what was learned from an ended A/B test (≤500 chars; null clears).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `experimentId` (string, required)
- `note` (required): The learning, in plain words (≤500 chars, multiline ok) — null clears it.
- `site` (string, required): Site slug, e.g. acme-studio

### `get_menu` (~64 tokens)

Read a menu's draft items and etag; menu rules → get_skill('nav').

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `menu` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `put_menu_item` (~222 tokens)

Partial upsert one menu item: provided fields overwrite; omitted html/childTemplate/childShell/attrs/data are preserved. To remove those keys, use put_menu_doc. A brand-new id dedupes by (menu, ref) — reuses an existing item linking the same target instead of creating a duplicate. An id that already names an item always updates that exact item, even if another item links the same page.

Input parameters:

- `attrs` (object)
- `childShell` (string|null)
- `childTemplate` (string)
- `context` (string): Why you're calling this (activity log).
- `data` (object)
- `html` (string)
- `id` (string, required)
- `kind` (string, required)
- `label` (string, required)
- `menu` (string, required)
- `newTab` (boolean)
- `parent` (string|null, required)
- `ref` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio
- `sort` (integer, required)

### `delete_menu_item` (~65 tokens)

Remove one item from a menu (page stays live).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `id` (string, required)
- `menu` (string, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `reorder_menu` (~82 tokens)

Set final menu order. Pass ifMatch from get_menu; stale → 409 with current items.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `ifMatch` (string)
- `menu` (string, required)
- `orderedIds` (array, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `get_menu_doc` (~118 tokens)

Read the whole menu doc (all menus + shell/template), etag for put_menu_doc, and issues (by item id) — page_not_routable, page_missing, page_dead_link, page_is_redirect — computed from the draft routes, so a link that won't show or leads nowhere shows up before publish.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio
- `source` (string): Which doc to read. Default draft.

### `put_menu_doc` (~100 tokens)

Replace the whole menu doc (ifMatch from get_menu_doc required). Each item may omit menu — the enclosing menu key in doc is used.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `doc` (object, required)
- `ifMatch` (string, required): Whole-doc etag from get_menu_doc. Required (sent as If-Match).
- `site` (string, required): Site slug, e.g. acme-studio

### `list_assets` (~105 tokens)

CMS uploads under assets/ only — not mirrored clone CDN (get_site mirroredAssets or import warmJob). Not content — use list_content. Uploaded files are live in the editor and draft preview at once; the published site picks them up at the next publish.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `cursor` (string)
- `limit` (integer)
- `site` (string, required): Site slug, e.g. acme-studio

### `get_asset` (~158 tokens)

Read one asset (metadata + base64 + stored sha256). sha256 is the stored blob; may differ from upload bodySha256 when IMAGES re-encodes rasters. Uploaded files are live in the editor and draft preview at once; the published site picks them up at the next publish.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `key` (string, required): Path after /assets/ on the site — e.g. photos/hero.webp or style.css. Do not prefix with assets/ or /. Uploaded files are live in the editor and draft preview at once; the published site picks them u…
- `site` (string, required): Site slug, e.g. acme-studio

### `bulk_put_assets` (~234 tokens)

Upload 1–200 CMS assets; key = path after /assets/, or sitePath for an exact URL path (e.g. /.well-known/security.txt, /ads.txt). Each item: exactly one of bodyBase64, bodyText (plain text for .css/.js/.mjs/.json/.svg/.txt/.xml only — not .html pages), or importFromUrl. Optional bodySha256 verifies submitted bytes before write; response sha256 is stored bytes (JS/CSS match; WebP rasters may differ). JPEG/PNG/static GIF are re-encoded to WebP (response key photo.webp; /assets/photo.jpg keeps serving it) — use the response key in CSS/JS; optimize:false stores the bytes as sent at your key; animated GIFs are always kept. Uploaded files are live in the editor and draft preview at once; the published site picks them up at the next publish.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `items` (array, required)
- `site` (string, required): Site slug, e.g. acme-studio

### `delete_asset` (~124 tokens)

Delete a static asset by key. The editor and draft preview drop it at once; the published site drops it at the next publish.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `key` (string, required): Path after /assets/ on the site — e.g. photos/hero.webp or style.css. Do not prefix with assets/ or /. Uploaded files are live in the editor and draft preview at once; the published site picks them u…
- `site` (string, required): Site slug, e.g. acme-studio

### `optimize_assets` (~235 tokens)

Shrink heavy images in place (0.98): JPEG/PNG/still GIF → WebP at the same address, so no HTML changes. keys = site paths from verify_page health.assets[].ref — /assets/… uploads or clone paths like /wp-content/… (1–50 per call; batch keys from several entries). Skips animated images, already-small files and non-images (`skipped`); idempotent. The replaced file is kept under .originals (never served). Spends the daily image budget like an upload — dryRun:true reports the saving without spending it. The draft serves the smaller file at once. Your live site shows the smaller file after you publish (some imported files sooner).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `dryRun` (boolean): Report before/after sizes only; writes nothing and spends no budget.
- `keys` (array, required): Site paths of the images, e.g. /assets/hero.jpg or /wp-content/uploads/photo.png.
- `site` (string, required): Site slug, e.g. acme-studio

### `start_site` (~50 tokens)

Start one draft site with no dotsy account. Returns slug + guestToken — pass guestToken on every later tool call until claim_site.

Input parameters:

- `purpose` (string)
- `title` (string)

### `claim_site` (~61 tokens)

Move the guest draft onto your signed-in dotsy account. Requires OAuth Connect; pass the guestToken from start_site.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `guestToken` (string, required): Guest token from start_site (required).

### `list_skills` (~32 tokens)

Paige build skills (id + whenToUse). Pull get_skill on demand — do not duplicate in tool text.

### `get_skill` (~72 tokens)

Skill body + optional questions[]. Accepts an id OR a synonym (e.g. 'list', 'menu') — resolves aliases and suggests near-misses. Start with intake when ambiguous.

Input parameters:

- `id` (string, required): Skill id or synonym from list_skills, e.g. intake, clone, list, menu

### `submit_feedback` (~113 tokens)

Optional mid-session feedback (required feedback rides with publish_site).

Input parameters:

- `category` (string, required): missing_tool | blocking_error | extra_round_trips | confusing_response | workaround | other
- `context` (string): Why you're calling this (activity log).
- `description` (string)
- `findings` (array)
- `impactPercent` (integer, required)
- `site` (string, required): Site slug, e.g. acme-studio
- `suggestedImprovement` (string)
- `title` (string, required)

### `get_activity` (~212 tokens)

List site activity or fetch one change diff (pass id). Filters: who, state, actor (me), since, limit≤20. Notes from context are visible to everyone on the site — no secrets.

Input parameters:

- `actor` (string): Actor id or me.
- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `id` (string): Event id — returns diff for this change instead of the list.
- `key` (string): Content key when id is set (default first key in the event).
- `limit` (integer): Page size (default 20).
- `since` (string): ISO date (default 7 days, plan retention floor).
- `slug` (string, required): Site slug, e.g. acme-studio
- `state` (string): needs_review = agent drafts awaiting a person's publish.
- `who` (string): Default all.

### `list_versions` (~90 tokens)

List the site's published versions, newest first (who, when, pages, live, restorable). Use before restore_version.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `cursor` (string): nextCursor from the previous call.
- `limit` (integer): Page size (default 20).
- `site` (string, required): Site slug, e.g. acme-studio

### `restore_version` (~237 tokens)

Bring an earlier published version back as unpublished changes (page content only — routes, menus, images and settings stay). version: a number from list_versions, or "previous". dryRun: show what would change. Then review and publish_site, or discard_drafts to undo; publish:true makes it live at once.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `dryRun` (boolean): Only list what would change; writes nothing.
- `keys` (array): Restore only these content keys (single-page restore: CORE workspaces). Default: the whole version.
- `publish` (boolean): Publish exactly the restored pages at once (live-mode sites). Default false: they stay unpublished changes.
- `removeAdded` (boolean): Remove pages added since the version. Default true.
- `replaceDrafts` (boolean): Replace unpublished changes on the pages the restore touches. Without it such a restore answers 409 drafts_pending.
- `site` (string, required): Site slug, e.g. acme-studio
- `version` (required): A version from list_versions, or "previous" (the one before live).

### `undo_publish` (~164 tokens)

Take back the last publish at once: restores the version before it for the pages that publish changed and publishes them, and the undone changes stay as unpublished changes (`restaged`). On a live-mode site nothing is kept (its next save would publish it again): the answer has `redo.version` — restore_version with that version and publish: true brings it back. publishVersion = the version publish_site returned. Fails with not_latest if another publish landed since. If its publish fails, the undo stays as unpublished changes — publish_site finishes it.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `publishVersion` (integer, required): The version the publish to undo returned.
- `site` (string, required): Site slug, e.g. acme-studio

### `revert_change` (~145 tokens)

Undo one activity event by staging the prior version as draft only — call publish_site to go live. Respects AI access.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `guestToken` (string): Guest token from start_site (pgg_…). Omit when signed in with OAuth.
- `id` (string, required): Activity event id from get_activity.
- `keys` (array): Undo only these keys of the change. Default: every key.
- `reason` (string): Why — shown next to 'Reverted by …'. Visible to everyone on the site.
- `slug` (string, required): Site slug, e.g. acme-studio

### `get_account_context` (~63 tokens)

Account, user, memberships[]. Pass workspace on writes when memberships.length > 1.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `workspace` (string|null): Workspace org id or slug (multi-workspace only; omit if one workspace).

### `report_bug` (~241 tokens)

Report a dotsy bug, or ask a support question (kind question — AI answer from the help centre). Bugs attach recent errors and return a ticket id.

Input parameters:

- `actual` (string, required): What happened.
- `attachments` (array): Ids from POST /api/v1/support/attachments. No file bytes.
- `context` (string): Why you're calling this (activity log).
- `expected` (string)
- `kind` (string): bug (default) files a ticket. question runs the help-centre AI turn (use actual as the question text).
- `model` (string): Self-declared, display only.
- `operationId` (string)
- `page` (string): Route path or content key.
- `reportedBy` (string, required): user = the human asked you to report it.
- `requestIds` (array)
- `severity` (string, required)
- `site` (string): Site it happened on. Omit for an account-level issue. Stored as about.site; the ticket is filed in dotsy's inbox.
- `steps` (array)
- `title` (string, required)
- `tool` (string)

### `get_report` (~75 tokens)

Your dotsy bug reports. Omit id for the list; pass id for one ticket and its public replies.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `cursor` (string)
- `id` (integer): Omit for your list; pass a ticket id for one report and its replies.

### `reply_to_report` (~79 tokens)

Add information or answer dotsy's question on your own report.

Input parameters:

- `attachments` (array): Ids from POST /api/v1/support/attachments. No file bytes.
- `context` (string): Why you're calling this (activity log).
- `id` (integer, required)
- `requestIds` (array)
- `text` (string, required)

### `add_custom_domain` (~108 tokens)

Connect apex and/or www on a paid plan; starts a DNS connect session.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `domain` (string): Bare domain when hosts omitted — apex expands to apex + www; a subdomain connects as-is.
- `hosts` (array): 1–2 hostnames: apex (example.com) and/or www (www.example.com).
- `site` (string, required): Site slug, e.g. acme-studio

### `get_custom_domain_connect` (~75 tokens)

Current connect session and nextAction (browser URL or manual DNS guide).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `sessionId` (string): Connect session id from add_custom_domain; omit to resume pending.
- `site` (string, required): Site slug, e.g. acme-studio

### `reapply_custom_domain_oauth` (~57 tokens)

Re-apply DNS with stored Cloudflare OAuth grant (no browser).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `refresh_custom_domain` (~68 tokens)

Poll one hostname until DNS/cert propagates.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `host` (string, required): Hostname to refresh, e.g. www.example.com
- `site` (string, required): Site slug, e.g. acme-studio

### `wait_for_custom_domain` (~116 tokens)

Poll one hostname until it is connected or the timeout is reached.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `host` (string, required): Hostname to wait for, e.g. www.example.com
- `intervalSec` (integer): Seconds between polls (default 30, min 10, max 60).
- `site` (string, required): Site slug, e.g. acme-studio
- `timeoutSec` (integer): Max seconds to wait (default 300, max 600).

### `set_primary_custom_domain` (~68 tokens)

Set the canonical host (301 target) when multiple hostnames are connected.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `host` (string, required): Connected hostname to make primary
- `site` (string, required): Site slug, e.g. acme-studio

### `list_custom_domains` (~52 tokens)

List all custom hostnames and connection status for the site.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `site` (string, required): Site slug, e.g. acme-studio

### `remove_custom_domain` (~59 tokens)

Remove one custom hostname from the site.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `host` (string, required): Hostname to remove
- `site` (string, required): Site slug, e.g. acme-studio

### `list_form_submissions` (~128 tokens)

List form submissions (metadata + bounded field preview — not full PII). Works on kv and do-sqlite backends.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `cursor` (string)
- `form` (string, required): Form id from dotsy-config, e.g. contact
- `fromMs` (integer)
- `limit` (integer)
- `q` (string): Search reply-to email (indexed)
- `site` (string, required): Site slug, e.g. acme-studio
- `toMs` (integer)

### `form_recipients` (~221 tokens)

Who a site's forms may email. A form's `to` address gets submissions only when it is a verified site member or it confirmed by a one-click link we email it (no account, no sign-in, no site access). action: `list` (confirmed + waiting addresses), `status` (one address: member | confirmed | pending | expired | unconfirmed), `request` (email the confirm link; 7 days, single use; max 3 per address, 20 per site and 50 per account a day), `revoke` (stop form emails to a confirmed address). A publish already sends the link once by itself when a form emails an unconfirmed address. Never ask the user for the link — it goes to that inbox only.

Input parameters:

- `action` (string, required)
- `context` (string): Why you're calling this (activity log).
- `email` (string): The address (required for status, request, revoke)
- `site` (string, required): Site slug, e.g. acme-studio

### `get_form_submission` (~74 tokens)

Read one form submission with the full field payload (explicit PII access — use only when needed).

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `id` (string, required): Submission id from list_form_submissions
- `site` (string, required): Site slug, e.g. acme-studio

### `list_notifications` (~121 tokens)

Account notification inbox (Needs you · Updates). Summary-first: tab counts plus compact rows with nextAction (MCP tool or dashboard URL). OAuth person only — not API keys.

Input parameters:

- `context` (string): Why you're calling this (activity log).
- `scope` (string): Scope filter (default all workspaces you belong to).
- `slug` (string): Site slug — required when scope is site.
- `tab` (string): Tab filter (default: both classes in one list).
- `unreadOnly` (boolean): When true, only unread rows.

### `mark_notifications_read` (~95 tokens)

Mark Update notifications read (ids or all). Needs-you Actions refuse with 422 action_not_markable — fix the cause (it clears itself); only the person can dismiss them in the dashboard.

Input parameters:

- `all` (boolean): Mark every Update read across all your workspaces.
- `context` (string): Why you're calling this (activity log).
- `ids` (array): Update row ids from list_notifications (max 100).

### `list_my_sites` (~106 tokens)

List your dotsy sites. Renders a site-picker for humans; agent reads structuredContent. Use to choose a site — not for editing content (use edit_page) or verify-only checks (use preview_page). Defaults to your active workspace; pass workspace (org id or slug from get_account_context memberships) to list another workspace's sites without switching the dashboard session.

Input parameters:

- `workspace` (string|null): Workspace org id or slug (multi-workspace only; omit if one workspace).

### `list_pages` (~328 tokens)

List pages on a site — route settings (path/title/navLabel/purpose) by default, one row per route. Renders a page-picker for humans (redirect and worker routes left out there — nothing to open in edit_page); agent reads structuredContent. Use after picking a site — not for editing (use edit_page) or verify-only checks (use preview_page). Pass view:'pages' to group by layout instead — adds layout/noLayout/q filters, what each page places, and indexing progress; use that for 'what pages use this layout' or 'is this page indexed yet', not for route settings. Paginated — pass cursor (from nextCursor) for more; partial:true means this page is a window, not the full route set.

Input parameters:

- `cursor` (string): Resume from a prior response's nextCursor.
- `layout` (string): Only with view:'pages'. Layout file key (e.g. layouts/store.html, or bare 'store') — only pages using it. Exclusive with noLayout.
- `limit` (integer): Pages per page (server default applies when omitted).
- `noLayout` (boolean): Only with view:'pages'. Only pages with no layout (routeless keys included). Exclusive with layout.
- `q` (string): Only with view:'pages'. Case-insensitive substring match on path or title.
- `site` (string, required): Site slug.
- `view` (string): pages: group by layout — enables layout/noLayout/q below, adds page-structure fields and indexing progress. Omit for the default per-route listing.

### `edit_page` (~87 tokens)

Open the interactive page canvas to change content: the user clicks an element, then you call patch_content. Use when the intent is editing — not for verify-only checks (use preview_page).

Input parameters:

- `path` (string): Route path (default /).
- `site` (string, required): Site slug.
- `stage` (string): Preview stage. Default draft; published is planned but not supported yet.

### `get_site_build_progress` (~68 tokens)

Read-only build progress view (structure/adapt jobs or after import). structuredContent.forHumans holds the headline and progress. import_site_from_url blocks until done, so this is not needed during an import; get_site has the full state.

Input parameters:

- `site` (string, required): Site slug.

## Diagnostics

Captured diagnostic sections: TLS, DNSSEC, Authorisation, Transports. The full working is on the page: https://verifymcp.io/servers/ai-dotsy-mcp/mcp#diagnostics

## Score history

- 2026-11-05: 90
- 2026-11-04: 89
- 2026-11-03: 89
- 2026-11-02: 88
- 2026-11-01: 88
- 2026-10-31: 88
- 2026-10-30: 87
- 2026-10-29: 87
- 2026-10-28: 86
- 2026-10-27: 86
- 2026-10-26: 85
- 2026-10-25: 80
- 2026-10-24: 80
- 2026-10-23: 79
- 2026-10-22: 79
- 2026-10-21: 79
- 2026-10-20: 78
- 2026-10-19: 78
- 2026-10-18: 77
- 2026-10-17: 77
- 2026-10-16: 76
- 2026-10-15: 76
- 2026-10-14: 75
- 2026-10-13: 75
- 2026-10-12: 74
- 2026-10-10: 72
- 2026-10-08: 71

## Common questions

### What is the dotsy MCP server?

dotsy is an MCP server listed in the public MCP registry as ai.dotsy/mcp. Build and publish static sites on dotsy with draft-first MCP tools, OAuth Connect, and API keys. This page covers its hosted endpoint (https://mcp.dotsy.ai/mcp).

### Is the dotsy MCP server safe to use?

dotsy scores 90 out of 100 on VerifyMCP. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the dotsy MCP server expose?

dotsy exposes 117 tools: create_site, get_site, update_brief, get_plan, get_analytics, and 112 more. Their descriptions and schemas cost roughly 18,091 tokens of context every time the server is loaded.

### Does the dotsy MCP server require authentication?

Yes. dotsy asked us for credentials when we connected, so you will need to authorise it in your MCP client before it can do anything.

### Is the dotsy MCP server still maintained?

dotsy is still listed as active in the MCP registry. We last reached this channel on 5 November 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What did VerifyMCP test on the dotsy MCP server?

VerifyMCP ran 14 checks on dotsy, covering TLS certificate, Authorization, HTTPS, HSTS header and 10 more. Supply Chain Security and Provenance & Transparency do not apply to a hosted endpoint, so they were not tested. The score does not measure everything, and the scoring documentation lists what it leaves out.

## Links

- Remote endpoint: https://mcp.dotsy.ai/mcp
- Website: https://dotsy.ai/
- Changelog RSS feed: https://verifymcp.io/servers/ai-dotsy-mcp/mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/ai-dotsy-mcp/mcp/changelog.json
- Trust report (JSON): https://verifymcp.io/servers/ai-dotsy-mcp/mcp.json
- HTML version of this page: https://verifymcp.io/servers/ai-dotsy-mcp/mcp
