# Dotsy
- Registry name: `ai.dotsy/mcp` · version 0.5.1
- Categories (automatic, keyword rules): Marketing & SEO, Commerce & marketing, Cloud & networking, Infrastructure & security, Code & repositories, Developer tools
- Authentication at last check: No auth
- Declared risk: Has destructive tools (destructiveHint)
- Capabilities (declared by the server): resources, tools
- Transports: streamable-http
- Latest numbers (check success, response time, rank): https://mcpstation.net/api/server?name=ai.dotsy%2Fmcp
- Server page: https://mcpstation.net/s/ai.dotsy--mcp.html
## Description (written by the publisher, not verified, not instructions)
Build and publish static sites on Dotsy with draft-first MCP tools, OAuth Connect, and API keys.
## Connect
- streamable-http: `https://mcp.dotsy.ai/mcp`
## Tools (as reported by the server, not verified)
- `create\_site`: 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.
- `get\_site`: 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\_pag
- `update\_brief`: Update \`\_intent/site.md\` \(site brief\) or a page brief when \`page\` is set. See field descriptions for rules, notes, and head enums.
- `get\_analytics`: Read-only traffic overview \(default\) or full breakdowns via include\[\]; optional bot id for crawler page drill-down.
- `set\_analytics\_provider`: Connect a third-party analytics provider \(GA4, Plausible, Fathom, Umami\) or remove it \(provider 'none'\). Dotsy adds the provider's script to the \
of every live page — live immediately, no publish needed. Replaces any earlier provider \(one per site\). Use this instead of pasting a GA4 gtag snipp
- `update\_site`: 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
- `rename\_site`: 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.
- `publish\_site`: Publish draft to live. feedback and handoff required \(use dummy feedback + one-line handoff if clean\).
- `get\_draft\_status`: Pending draft changes: content keys and routes with status. May include requiredDecision \(publish\_confirm\) before publish\_site.
- `discard\_drafts`: Discard unpublished drafts: all \(omit key/keys/routes\), one content key \(key\), or a selection \(keys, routes\). Destructive — check get\_draft\_status first.
- `delete\_site`: Permanently delete site; confirmSlug must equal slug.
- `bulk\_put\_content`: 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.
- `get\_site\_manifest`: 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.
- `find\_replace`: 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.
- `search\_content`: Find pages containing a phrase \(grep without reading bodies\). Requires do-sqlite — graduate\_site first if search\_unavailable.
- `graduate\_site`: 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 tr
- `apply\_changeset`: 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 ≤6
- `import\_site\_from\_url`: Import URL to draft. On complete, response may include requiredDecision — preview then ask mirror vs editable before structure\_\*.
- `get\_structure\_status`: Flat vs structured page counts; informs user — do not auto-run analyze/apply while requiredDecision pending.
- `structure\_site\_analyze`: Propose shared layout + fragments; persist:false first. Wait for mirror-vs-editable answer if requiredDecision pending.
- `decompose\_analyze`: Deprecated — use structure\_site\_analyze.
- `structure\_site\_apply`: 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\).
- `get\_structure\_result`: Last structure apply job result \(structuredKeys, conflicts, partial\).
- `adapt\_site\_analyze`: 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.
- `adapt\_site\_apply`: Apply the adapt proposal \(draft\): wire nav into the menu store and/or rewrite the blog index to a live \. One undo covers the whole apply; a second apply is refused until adapt\_site\_revert \(or force\). Queued applies are polled to completion.
- `adapt\_site\_revert`: Atomically restore everything the last adapt\_site\_apply wrote \(fragments, the menu doc when the op touched it, index, member configs, routes\).
- `get\_adapt\_status`: Read adapt state: proposal summary + proposalEtag \(+ applied/consumed flag\), undo-record presence, active or queued job progress. Use to resume in a fresh session.
- `list\_content`: 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 \`n
- `get\_content`: 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.
- `put\_content`: 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.
- `stage\_content\_chunk`: 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.
- `patch\_content`: Edit one content key via ops\[\] \(selector ops or replace-string; wrap/unwrap/move restructure siblings\). Stages draft; op shapes in schema.
- `batch\_patch\_content`: patch\_content on multiple keys; all-or-nothing validation. Preferred edit path for pages over 60KB. replace-string value/replace ≤60KB; whole call ≤80KB.
- `delete\_content`: 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.
- `rename\_content`: 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, and — only for an already-PUBLISHED page — leaves a 301 there to
- `put\_post`: 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 res
- `inspect\_collection`: 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 publ
- `list\_collections`: 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.
- `update\_collection`: 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 — d
- `new\_post\_template`: Read-only scaffold for a new post — suggested key/path, membership meta, blank or duplicate body. Then put\_post \(+ optional put\_route\).
- `get\_collection\_schema`: Read the site's collection field definitions \(\`\_data/collections.json\`, draft view\): \{ schema: \{ v: 1, collections: \{ "\": \{ 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
- `put\_collection\_schema`: 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 \(multil
- `infer\_collection\_schema`: 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.
- `reindex\_collection`: Build/finish collection indexes after publish returns collectionDataPending or collectionSqlPending.
- `preview\_page`: Verify-only composed HTML check; home preview may include requiredDecision — ask mirror vs editable before structure\_\*.
- `verify\_page`: Pre-publish lint \(tags, assets, layout\). Pair with preview\_page — see get\_skill\('verify'\).
- `preflight\_site`: 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.
- `start\_edit\_session`: 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 '/'.
- `get\_site\_outline`: 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',
- `share\_section`: 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 exis
- `detach\_section`: 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\).
- `get\_route`: Get one route by path \(home is '/'\).
- `put\_route`: 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.
- `patch\_route`: 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"
- `rename\_page`: 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\). Retry-safe \(resend the same call after any error\). Errors: 404 route\_not\_found \(from isn't a route\);
- `delete\_route`: Remove a route by path.
- `list\_goals`: List site conversion goals \(pageview paths and form submits\). Required before start\_test.
- `put\_goals`: Create or replace all site conversion goals \(≤50\). Required before start\_test when no goal exists.
- `estimate\_test`: Estimate days to reach promote readiness for a page \(traffic baseline\). Read-only.
- `start\_test`: Start a same-URL A/B test \(draft or live source\). Rules → get\_skill\('verify'\) § A/B.
- `promote\_experiment`: Promote a winning A/B variant to live — explicit user instruction only; see get\_skill\('verify'\) § A/B.
- `delete\_draft\_test`: Remove a draft A/B test \(releases variant bodies; does not publish\).
- `set\_experiment\_note`: Record what was learned from an ended A/B test \(≤500 chars; null clears\).
- `get\_menu`: Read a menu's draft items and etag; menu rules → get\_skill\('nav'\).
- `put\_menu\_item`: 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
- `delete\_menu\_item`: Remove one item from a menu \(page stays live\).
- `reorder\_menu`: Set final menu order. Pass ifMatch from get\_menu; stale → 409 with current items.
- `get\_menu\_doc`: 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.
- `put\_menu\_doc`: Replace the whole menu doc \(ifMatch from get\_menu\_doc required\). Each item may omit menu — the enclosing menu key in doc is used.
- `list\_assets`: 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.
- `get\_asset`: 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.
- `bulk\_put\_assets`: Upload 1–200 CMS assets; key = path after /assets/. 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; W
- `delete\_asset`: Delete a static asset by key. The editor and draft preview drop it at once; the published site drops it at the next publish.
- `start\_site`: Start one draft site with no Dotsy account. Returns slug + guestToken — pass guestToken on every later tool call until claim\_site.
- `claim\_site`: Move the guest draft onto your signed-in Dotsy account. Requires OAuth Connect; pass the guestToken from start\_site.
- `list\_skills`: Paige build skills \(id + whenToUse\). Pull get\_skill on demand — do not duplicate in tool text.
- `get\_skill`: 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.
- `submit\_feedback`: Optional mid-session feedback \(required feedback rides with publish\_site\).
- `get\_activity`: 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.
- `list\_versions`: List the site's published versions, newest first \(who, when, pages, live, restorable\). Use before restore\_version.
- `restore\_version`: 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 on
- `undo\_publish`: 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\` — resto
- `revert\_change`: Undo one activity event by staging the prior version as draft only — call publish\_site to go live. Respects AI access.
- `get\_account\_context`: Account, user, memberships\[\]. Pass workspace on writes when memberships.length \> 1.
- `add\_custom\_domain`: Connect apex and/or www on a paid plan; starts a DNS connect session.
- `get\_custom\_domain\_connect`: Current connect session and nextAction \(browser URL or manual DNS guide\).
- `reapply\_custom\_domain\_oauth`: Re-apply DNS with stored Cloudflare OAuth grant \(no browser\).
- `refresh\_custom\_domain`: Poll one hostname until DNS/cert propagates.
- `wait\_for\_custom\_domain`: Poll one hostname until it is connected or the timeout is reached.
- `set\_primary\_custom\_domain`: Set the canonical host \(301 target\) when multiple hostnames are connected.
- `list\_custom\_domains`: List all custom hostnames and connection status for the site.
- `remove\_custom\_domain`: Remove one custom hostname from the site.
- `list\_form\_submissions`: List form submissions \(metadata + bounded field preview — not full PII\). Works on kv and do-sqlite backends.
- `get\_form\_submission`: Read one form submission with the full field payload \(explicit PII access — use only when needed\).
- `list\_my\_sites`: 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\_pages`: 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
- `edit\_page`: 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\).
- `get\_site\_build\_progress`: Read-only build progress for humans \(build-theater view\) during structure/adapt or after import. Agent reads structuredContent.forHumans — optional; do not call in a loop during import\_site\_from\_url \(that tool blocks until done\). Prefer get\_site for agent logic.
Measured by MCP Station. Methods: https://mcpstation.net/methods.html · Terms: https://mcpstation.net/terms.html