Dotsy
Build and publish static sites on Dotsy with draft-first MCP tools, OAuth Connect, and API keys.
使えるツール
create_siteCreate 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_siteSite 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.update_briefUpdate `_intent/site.md` (site brief) or a page brief when `page` is set. See field descriptions for rules, notes, and head enums.get_planRead-only workspace plan and pooled usage; pass slug for this site's add-ons. Call before a large build.get_analyticsRead-only traffic overview (default) or full breakdowns via include[]; optional bot id for crawler page drill-down.set_analytics_providerConnect 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.get_custom_codeRead 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`.set_custom_codeSet 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.update_siteUpdate 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}); 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.rename_siteRename 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.publish_sitePublish draft to live. feedback and handoff required (use dummy feedback + one-line handoff if clean).get_draft_statusPending draft changes: content keys and routes with status. May include requiredDecision (publish_confirm) before publish_site.review_changesReview 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.discard_draftsDiscard 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.delete_sitePermanently delete site; confirmSlug must equal slug.bulk_put_contentStage 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_manifestContent 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_replaceCase-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_contentFind pages containing a phrase (grep without reading bodies). Requires do-sqlite — graduate_site first if search_unavailable.graduate_siteEnable 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.apply_changesetStage 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.import_site_from_urlImport URL to draft. On complete, response may include requiredDecision — preview then ask mirror vs editable before structure_*.get_structure_statusFlat vs structured page counts; informs user — do not auto-run analyze/apply while requiredDecision pending.structure_site_analyzePropose shared layout + fragments; persist:false first. Wait for mirror-vs-editable answer if requiredDecision pending.decompose_analyzeDeprecated — use structure_site_analyze.structure_site_applyRewrite 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_resultLast structure apply job result (structuredKeys, conflicts, partial).adapt_site_analyzePropose 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_applyApply 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.adapt_site_revertAtomically restore everything the last adapt_site_apply wrote (fragments, the menu doc when the op touched it, index, member configs, routes).get_adapt_statusRead adapt state: proposal summary + proposalEtag (+ applied/consumed flag), undo-record presence, active or queued job progress. Use to resume in a fresh session.list_contentList 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.get_contentRead 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_contentStage 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_chunkAppend 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_contentEdit one content key via ops[] (selector ops or replace-string; wrap/unwrap/move restructure siblings). Stages draft; op shapes in schema.batch_patch_contentpatch_content on multiple keys; all-or-nothing validation. Preferred edit path for pages over 60KB. replace-string value/replace ≤60KB; whole call ≤80KB.delete_contentDelete 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_contentRe-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.put_postStage 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).inspect_collectionCollection 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[]).list_collectionsCollections 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_collectionRename 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.new_post_templateRead-only scaffold for a new post — suggested key/path, membership meta, blank or duplicate body. Then put_post (+ optional put_route).get_collection_schemaRead 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.put_collection_schemaReplace 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.infer_collection_schemaSuggest 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_collectionBuild/finish collection indexes after publish returns collectionDataPending or collectionSqlPending.preview_pageVerify-only composed HTML check; home preview may include requiredDecision — ask mirror vs editable before structure_*.verify_pagePre-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').preflight_sitePlan 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_sessionOpen 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_outlineSite 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).share_sectionMake 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).detach_sectionGive 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_routeGet one route by path (home is '/').put_routeCreate/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_routePartial 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.rename_pageChange 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.delete_routeRemove a route by path.
Claude / Cursor への設定
{
"mcpServers": {
"remote-ai-dotsy-mcp": {
"type": "http",
"url": "https://mcp.dotsy.ai/mcp"
}
}
}出典
- 更新日:2026-10-29
この項目を含むまとめ
同じ分類の他の MCP
资料来自官方仓库与 MCP 接口探测,以官方最新文档为准;安装第三方 MCP server 前请自行评估安全风险。