{"version":"1.0.0","mcp_server":"https://dropfast.dev/api/mcp","auth":{"type":"bearer","header":"Authorization","scheme":"Bearer","prefix":"df_sk_","legacy_header":"x-api-key","signup":"https://dropfast.dev/dashboard/api-keys"},"anthropic":[{"name":"publish_html","description":"Publish a single HTML page as a static site with a permanent URL. Use for single-file output. For multi-file sites with assets, use publish_zip. Returns {slug, url, version, versionUrl, name, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is the integer version number just created (1 for a new site); versionUrl pins that exact version (…/s/<slug>/?v=<version>). ogImageUrl is the stored OG-card setting (null = auto card); ogImage is the effective absolute card URL injected into the shared page for social previews. The URL is permanent and immediately live. TIP: set metadata.df.project and metadata.df.type at publish so query_sites can recall this site later — untagged sites pile up as unsearchable slugs.","input_schema":{"type":"object","properties":{"html":{"type":"string","description":"The HTML document to publish. Treated as a complete index.html."},"name":{"type":"string","description":"Optional display name shown in the dashboard. Defaults to derived from <title>."},"accessMode":{"type":"string","enum":["public","private","password"],"description":"Visibility. Default: public."},"password":{"type":"string","description":"Required when accessMode is \"password\"."},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site so humans and other agents can leave structured feedback (later resolved via resolve_comment). Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path (e.g. \"/og_image.png\"). Omit for the auto-generated branded card. No base64 upload in v1 — bundle a root og_image.png in a publish_zip archive to host a custom image."},"metadata":{"type":"object","description":"Optional metadata written at publish (same closed-world shape as set_metadata). Set df.project (collection) + df.type (tag) so this site is recallable via query_sites and the dashboard organizer.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["html"],"additionalProperties":false}},{"name":"publish_zip","description":"Publish a ZIP archive as a multi-file static site (HTML + CSS + JS + images). ZIP must contain an index.html at the root. Use for sites with assets; use publish_html for single-page output. Returns {slug, url, version, versionUrl, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is the integer version just created (1 for a new site); versionUrl pins it (…/s/<slug>/?v=<version>). ogImage is the effective social-card URL. OG-CARD CONVENTION: a root-level og_image.png (or .jpg/.jpeg/.webp/.gif) in the archive is auto-used as the site's social card unless you pass ogImageUrl explicitly. Payload limit: 3 MB base64-encoded (~2.25 MB raw) due to platform body limits — use REST + S3 presigned URLs for larger sites. TIP: set metadata.df.project and metadata.df.type at publish so query_sites can recall this site later.","input_schema":{"type":"object","properties":{"zip_base64":{"type":"string","description":"Base64-encoded ZIP archive."},"name":{"type":"string","description":"Optional display name."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string"},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site. Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Overrides the zip-root og_image.* convention when set. Omit to auto-detect a bundled og_image.png or fall back to the auto card."},"metadata":{"type":"object","description":"Optional metadata written at publish (same closed-world shape as set_metadata). Set df.project (collection) + df.type (tag) so this site is recallable via query_sites and the dashboard organizer.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["zip_base64"],"additionalProperties":false}},{"name":"update_site","description":"Replace the files of an existing site by slug. Same URL, new content. Accepts either raw HTML (single file) or a base64-encoded ZIP. Returns {slug, url, version, versionUrl, updatedAt}. version is the new integer version number; versionUrl pins it (…/s/<slug>/?v=<version>). Re-uploading byte-identical content is a no-op that returns the current (unchanged) version. To change name/accessMode/password without replacing files, use update_site_settings. To tag a version with metadata (df.pr, df.project, etc.), use set_metadata; df.* keys do NOT belong in update_site_settings.","input_schema":{"type":"object","properties":{"slug":{"type":"string","description":"8-char slug from a prior publish."},"html":{"type":"string","description":"Single-page replacement HTML. Provide either html or zip_base64."},"zip_base64":{"type":"string","description":"Multi-file ZIP replacement."},"message":{"type":"string","description":"Optional commit-message-style note attached to this version (D2). Surfaces in list_versions and the version-history dashboard."}},"required":["slug"],"additionalProperties":false}},{"name":"update_site_settings","description":"Change a site's site-behavior settings (display name, access mode, password, commentsEnabled, ogImageUrl). Returns {slug, name, accessMode, commentsEnabled, ogImageUrl, ogImage, updatedAt}. Pass null to clear an optional field (ogImageUrl:null restores the auto social card). NOTE (TD-05): this tool owns site-behavior knobs only. Use set_metadata for df.* metadata (df.pr, df.project, df.session, ...); the namespaces are intentionally split so closed-world validation can run on metadata without coupling to site behavior.","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string","description":"Display name. Pass null to clear."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string","description":"Required when accessMode is \"password\"."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Pass null to clear back to the auto-generated card; omit to leave unchanged."},"commentsEnabled":{"type":"boolean","description":"Toggle the Phase 4 comments overlay/API on this site. true enables, false disables. Omit to leave unchanged."}},"required":["slug"],"additionalProperties":false}},{"name":"get_site","description":"Get metadata for one site by slug: URL, name, access mode, file count, size, created/updated timestamps, and OG social-card setting (ogImageUrl + effective ogImage). Use to verify a publish succeeded (URL is strongly consistent after publish).","input_schema":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"list_sites","description":"List all sites owned by the authenticated user. Returns array of {slug, url, name, title, excerpt, accessMode, commentsEnabled, ogImageUrl, ogImage, createdAt, updatedAt}. Use before update_site if you do not have the slug. For more than a handful of sites, prefer query_sites — it filters by metadata (df.project/df.type), searches free text, sorts, and paginates instead of returning everything.","input_schema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"query_sites","description":"Search and filter your sites — the narrowing counterpart to list_sites (which returns everything). Accepts structured metadata filters (dotted keys, AND semantics), a free-text search term q (fuzzy match over name, df.*/user metadata values, and extracted page content), sort (created | updated | name | relevance; relevance requires q and returns up to 100 ranked results with no cursor), limit (1-100, default 20), and cursor (pass the previous response's nextCursor; a cursor only replays under the sort it was minted for). Returns {sites: [{slug, url, name, title, excerpt, accessMode, metadata, fileCount, fileSizeBytes, commentsEnabled, ogImageUrl, ogImage, createdAt, updatedAt}], nextCursor}. Use to recall something you published earlier (\"the auth-redesign review from last week\"): filter metadata {\"df.project\": \"auth-redesign\"} and/or q=\"review\". Results are always scoped to the authenticated user.","input_schema":{"type":"object","properties":{"metadata":{"type":"object","description":"Structured filters as dotted keys, e.g. {\"df.project\": \"auth-redesign\", \"df.type\": \"review\"}. Multiple keys AND together. An array value (user keys and df.type) requires ALL listed values to be present (jsonb containment). Same closed-world df.* rules as the REST ?metadata.* query.","properties":{},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"q":{"type":"string","maxLength":200,"description":"Free-text search term. Fuzzy, case-insensitive, matches partial words. Empty/omitted = filter-only."},"sort":{"type":"string","enum":["created","updated","name","relevance"],"description":"Result order. Default: created (newest first). relevance requires q."},"limit":{"type":"integer","description":"Page size 1-100; default 20."},"cursor":{"type":"string","description":"Opaque cursor from the previous response's nextCursor."}},"additionalProperties":false}},{"name":"delete_site","description":"Permanently delete a site by slug. The URL stops resolving. Any aliases pointing at this site become inactive (so the owner can repoint them). This is irreversible. Returns {slug, deleted: true}.","input_schema":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"publish_markdown","description":"Publish a Markdown (.md) artifact. GFM-flavored. Top-level YAML frontmatter populates site metadata via the same closed-world schema set_metadata uses (df.pr, df.project, etc. land as if the agent had called set_metadata). Body metadata REPLACES frontmatter metadata if both are passed (locked decision #13). Returns {slug, url, version, versionUrl, name, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is 1 for a new site; versionUrl pins it (…/s/<slug>/?v=<version>); ogImage is the effective social-card URL. Use publish_html for plain HTML, publish_zip for multi-file archives. NOTE: the `name` arg sets the site's display name; it does NOT auto-fall-back to the frontmatter title -- pass it explicitly if you want it.","input_schema":{"type":"object","properties":{"markdown":{"type":"string","description":"The .md document including any YAML frontmatter block."},"name":{"type":"string","description":"Display name. Not derived from frontmatter -- pass explicitly."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string"},"message":{"type":"string","description":"Optional commit-message-style note attached to v1."},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site. Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Omit for the auto-generated card."},"metadata":{"type":"object","description":"Optional explicit metadata. Mirrors set_metadata shape exactly. Body REPLACES frontmatter on conflict.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["markdown"],"additionalProperties":false}},{"name":"list_versions","description":"List version history for a site, newest first. Returns {versions: [{version, createdAt, author, message, byteSize, fileCount, manifestHash}], nextCursor}. Cursor pagination keyed on version number. Owner-only. Use before get_version when you do not know which version number to fetch.","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"cursor":{"type":"string","description":"Pass the previous response's nextCursor to fetch older versions."},"limit":{"type":"integer","description":"Page size 1-100; default 20."}},"required":["slug"],"additionalProperties":false}},{"name":"get_version","description":"Fetch one historical version of a site by version number. Returns {version, createdAt, author, message, manifestHash, fileCount, byteSize, metadataSnapshot, files: [{path, blobHash, byteSize}]}. metadataSnapshot is the metadata as it stood when this version was published (useful for diffing across versions). To browse the rendered content of a historical version, visit /s/<slug>/?v=<v> in a browser.","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"version":{"type":"integer","description":"Version number (1-indexed). Get the list via list_versions."}},"required":["slug","version"],"additionalProperties":false}},{"name":"get_diff","description":"Diff two versions of a site by version number (Phase 4). Returns {slug, from, to, files: [{path, status:\"added\"|\"removed\"|\"modified\"|\"unchanged\", hunks}], metadata, cached}. hunks is a line-op array ({op:\"=\"|\"+\"|\"-\", text}) for modified text files (null for binary/unchanged/large). metadata is a jsondiffpatch delta of the df.* snapshot, or null. Read-only, owner-only. Use after update_site to reason about what changed since a comment was left (compare the comment's versionAtCreate to the current version).","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"from":{"type":"integer","description":"Base version number (1-indexed)."},"to":{"type":"integer","description":"Target version number (1-indexed). Get numbers via list_versions."}},"required":["slug","from","to"],"additionalProperties":false}},{"name":"get_metadata","description":"Read the metadata object on a site (df.* reserved keys + user keys). Returns {slug, metadata}. Use to discover the existing tagging before set_metadata (which uses REPLACE semantics).","input_schema":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"set_metadata","description":"Set the metadata object on a site. REPLACE semantics (locked decision #13): the entire metadata column is overwritten with the argument. To clear, pass {}. Closed-world rules apply: seven reserved df.* keys (df.pr, df.repo, df.session, df.agent, df.project, df.type, df.parent) are accepted; unknown df.* keys reject with METADATA_UNKNOWN_RESERVED_KEY. User keys (no df. prefix) accept string | string[]. Total JSON-stringify size capped at 8 KB. Use update_site_settings for site behavior (name/accessMode/password) -- these are separate (TD-05).","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"metadata":{"type":"object","description":"The new metadata object. Nested under df.* for reserved keys; free-form at the top level for user keys.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string"},"repo":{"type":"string"},"session":{"type":"string"},"agent":{"type":"string"},"project":{"type":"string"},"type":{"type":"string"},"parent":{"type":"string"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["slug","metadata"],"additionalProperties":false}},{"name":"get_comments","description":"Read comments for a site from the Phase 4 API. Comments are VERSION-PINNED: by default this returns only the comments left on the site's current version. Pass `version` (an integer from list_versions) to read a specific version's comments instead — e.g. to address feedback left on an earlier version before it was re-published. Returns {format:\"dropfast-comment-spike-v1\", slug, viewingVersion, currentVersion, comments:[{id,status,bodyText,target,author,versionAtCreate,createdAt,links}], versionCounts:[{version,count}]}. Each comment carries links.resolve/links.reopen with the exact request to change its status (over REST: PATCH or PUT /api/v1/comments/{id}); over MCP use resolve_comment. versionCounts lists open-comment totals per version so you can see where unresolved feedback lives. Use before update_site when a human asks an agent to address review feedback.","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"status":{"type":"string","enum":["open","resolved","all"],"description":"Default: open."},"version":{"type":"integer","description":"Which version's comments to read (>= 1). Default: the current version."}},"required":["slug"],"additionalProperties":false}},{"name":"add_comment","description":"Create a plain-text comment on a comments-enabled site using the Phase 4 spike JSON target shape. Useful for agent-to-agent review. Body is plain text only (no HTML) and target should include path plus cssSelector and/or textQuote.","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"bodyText":{"type":"string","minLength":1,"maxLength":2000,"description":"Plain text, 1-2000 chars. HTML tags reject."},"target":{"type":"object","description":"Requires at least one real anchor: cssSelector, textQuote, or both x and y.","properties":{"path":{"type":"string","minLength":1,"maxLength":512,"description":"Path inside the hosted site, e.g. / or /docs/."},"cssSelector":{"type":"string","minLength":1,"maxLength":1000},"textQuote":{"type":"object","properties":{"prefix":{"type":"string","maxLength":64},"exact":{"type":"string","minLength":1,"maxLength":512},"suffix":{"type":"string","maxLength":64}},"required":["exact"],"additionalProperties":false},"x":{"type":"number","minimum":0,"maximum":1,"description":"Viewport-relative x coordinate, 0-1. Must be paired with y when used as the only anchor."},"y":{"type":"number","minimum":0,"maximum":1,"description":"Viewport-relative y coordinate, 0-1. Must be paired with x when used as the only anchor."}},"required":["path"],"additionalProperties":false}},"required":["slug","bodyText","target"],"additionalProperties":false}},{"name":"resolve_comment","description":"Mark one Phase 4 spike comment open or resolved. Use after update_site when the agent has addressed a human comment. Returns the updated comment. The caller must be the site owner or the original comment author. (REST equivalent: PATCH or PUT /api/v1/comments/{id} with {\"status\":\"resolved\"} — see each comment’s links.resolve.)","input_schema":{"type":"object","properties":{"commentId":{"type":"string"},"status":{"type":"string","enum":["open","resolved"],"description":"Default: resolved."}},"required":["commentId"],"additionalProperties":false}},{"name":"create_alias","description":"Create a short-link alias. Resolves at /_/<alias> with a 302 redirect to either an owned site (targetType=\"site\" + targetSiteId) or any external URL (targetType=\"external\" + targetUrl). Aliases are globally unique across all users.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"Lowercase letters, digits, hyphens. 2–48 chars. Reserved names rejected."},"targetType":{"type":"string","enum":["site","external"]},"targetSiteId":{"type":"string","description":"Required when targetType is \"site\"."},"targetUrl":{"type":"string","description":"Required when targetType is \"external\"."}},"required":["alias","targetType"],"additionalProperties":false}},{"name":"repoint_alias","description":"Change where an existing alias points without changing its name. Can repoint between site and external. Pass isActive=false to pause without deleting (resolver returns 404 while paused). Returns the updated alias row.","input_schema":{"type":"object","properties":{"alias":{"type":"string","description":"Current alias name."},"targetType":{"type":"string","enum":["site","external"]},"targetSiteId":{"type":"string"},"targetUrl":{"type":"string"},"isActive":{"type":"boolean"}},"required":["alias"],"additionalProperties":false}},{"name":"list_aliases","description":"List all aliases owned by the authenticated user. Returns array of {alias, url, targetType, targetSiteId | targetUrl, isActive, hitCount, createdAt}.","input_schema":{"type":"object","properties":{},"additionalProperties":false}},{"name":"delete_alias","description":"Permanently delete an alias. The /_/<alias> URL stops resolving. Returns {alias, deleted: true}. To pause without deleting, use repoint_alias with isActive=false instead.","input_schema":{"type":"object","properties":{"alias":{"type":"string"}},"required":["alias"],"additionalProperties":false}},{"name":"create_share_link","description":"Mint a temporary, revocable share link that grants READ access to one of your sites via a bearer token in the URL (…/s/<slug>/?share=<token>). Use this when a human wants to give someone a private preview WITHOUT publishing the site — the link is the right tool for \"let me share this for review.\" IMPORTANT: this does NOT change the site's accessMode; the site stays private/password-protected and only holders of this exact link can view it. Do NOT reach for update_site_settings (accessMode: \"public\") when a preview link is what's wanted — that exposes the site to everyone. The token is a bearer secret shown ONCE, only inside the returned url. Returns {url, id, expiresAt, oneTimeUse, createdAt}. Every link expires (default 24h, 30-day ceiling); pass oneTimeUse:true for a link that self-consumes on first view. Revoke early with revoke_share_link.","input_schema":{"type":"object","properties":{"slug":{"type":"string","description":"8-char slug of a site you own."},"expiresIn":{"type":"string","description":"How long the link stays valid. Shorthand (\"1h\", \"24h\", \"7d\") or an ISO-8601 duration (\"PT1H\", \"P7D\"). Default 24h; 30-day hard ceiling."},"oneTimeUse":{"type":"boolean","description":"When true, the link self-consumes on first successful view. Default false (reusable until it expires or is revoked)."}},"required":["slug"],"additionalProperties":false}},{"name":"list_share_links","description":"List the temporary share links for a site you own — metadata only, NEVER the token (a bearer secret shown once at mint by create_share_link). Returns {links: [{id, oneTimeUse, expiresAt, usedAt, revokedAt, createdAt, status}]}, newest first; status is active | used | expired | revoked. This is a read-only audit view of who could preview the site; it does NOT reveal or change the site's accessMode (share links leave the site private and grant temporary bearer access alongside it). Use the id here with revoke_share_link to kill a link.","input_schema":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"revoke_share_link","description":"Instantly revoke one temporary share link by id (get ids from list_share_links). The link stops working on the next redeem. Returns {id, revoked: true}. This only kills the temporary bearer link — it does NOT change the site's accessMode; the site keeps whatever visibility it already had (private/password stay private/password). To make a site broadly public instead, that is a deliberate update_site_settings change, not a revoke.","input_schema":{"type":"object","properties":{"slug":{"type":"string"},"id":{"type":"string","description":"The share-link id from list_share_links."}},"required":["slug","id"],"additionalProperties":false}}],"openai":[{"type":"function","function":{"name":"publish_html","description":"Publish a single HTML page as a static site with a permanent URL. Use for single-file output. For multi-file sites with assets, use publish_zip. Returns {slug, url, version, versionUrl, name, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is the integer version number just created (1 for a new site); versionUrl pins that exact version (…/s/<slug>/?v=<version>). ogImageUrl is the stored OG-card setting (null = auto card); ogImage is the effective absolute card URL injected into the shared page for social previews. The URL is permanent and immediately live. TIP: set metadata.df.project and metadata.df.type at publish so query_sites can recall this site later — untagged sites pile up as unsearchable slugs.","parameters":{"type":"object","properties":{"html":{"type":"string","description":"The HTML document to publish. Treated as a complete index.html."},"name":{"type":"string","description":"Optional display name shown in the dashboard. Defaults to derived from <title>."},"accessMode":{"type":"string","enum":["public","private","password"],"description":"Visibility. Default: public."},"password":{"type":"string","description":"Required when accessMode is \"password\"."},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site so humans and other agents can leave structured feedback (later resolved via resolve_comment). Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path (e.g. \"/og_image.png\"). Omit for the auto-generated branded card. No base64 upload in v1 — bundle a root og_image.png in a publish_zip archive to host a custom image."},"metadata":{"type":"object","description":"Optional metadata written at publish (same closed-world shape as set_metadata). Set df.project (collection) + df.type (tag) so this site is recallable via query_sites and the dashboard organizer.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["html"],"additionalProperties":false}}},{"type":"function","function":{"name":"publish_zip","description":"Publish a ZIP archive as a multi-file static site (HTML + CSS + JS + images). ZIP must contain an index.html at the root. Use for sites with assets; use publish_html for single-page output. Returns {slug, url, version, versionUrl, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is the integer version just created (1 for a new site); versionUrl pins it (…/s/<slug>/?v=<version>). ogImage is the effective social-card URL. OG-CARD CONVENTION: a root-level og_image.png (or .jpg/.jpeg/.webp/.gif) in the archive is auto-used as the site's social card unless you pass ogImageUrl explicitly. Payload limit: 3 MB base64-encoded (~2.25 MB raw) due to platform body limits — use REST + S3 presigned URLs for larger sites. TIP: set metadata.df.project and metadata.df.type at publish so query_sites can recall this site later.","parameters":{"type":"object","properties":{"zip_base64":{"type":"string","description":"Base64-encoded ZIP archive."},"name":{"type":"string","description":"Optional display name."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string"},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site. Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Overrides the zip-root og_image.* convention when set. Omit to auto-detect a bundled og_image.png or fall back to the auto card."},"metadata":{"type":"object","description":"Optional metadata written at publish (same closed-world shape as set_metadata). Set df.project (collection) + df.type (tag) so this site is recallable via query_sites and the dashboard organizer.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["zip_base64"],"additionalProperties":false}}},{"type":"function","function":{"name":"update_site","description":"Replace the files of an existing site by slug. Same URL, new content. Accepts either raw HTML (single file) or a base64-encoded ZIP. Returns {slug, url, version, versionUrl, updatedAt}. version is the new integer version number; versionUrl pins it (…/s/<slug>/?v=<version>). Re-uploading byte-identical content is a no-op that returns the current (unchanged) version. To change name/accessMode/password without replacing files, use update_site_settings. To tag a version with metadata (df.pr, df.project, etc.), use set_metadata; df.* keys do NOT belong in update_site_settings.","parameters":{"type":"object","properties":{"slug":{"type":"string","description":"8-char slug from a prior publish."},"html":{"type":"string","description":"Single-page replacement HTML. Provide either html or zip_base64."},"zip_base64":{"type":"string","description":"Multi-file ZIP replacement."},"message":{"type":"string","description":"Optional commit-message-style note attached to this version (D2). Surfaces in list_versions and the version-history dashboard."}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"update_site_settings","description":"Change a site's site-behavior settings (display name, access mode, password, commentsEnabled, ogImageUrl). Returns {slug, name, accessMode, commentsEnabled, ogImageUrl, ogImage, updatedAt}. Pass null to clear an optional field (ogImageUrl:null restores the auto social card). NOTE (TD-05): this tool owns site-behavior knobs only. Use set_metadata for df.* metadata (df.pr, df.project, df.session, ...); the namespaces are intentionally split so closed-world validation can run on metadata without coupling to site behavior.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string","description":"Display name. Pass null to clear."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string","description":"Required when accessMode is \"password\"."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Pass null to clear back to the auto-generated card; omit to leave unchanged."},"commentsEnabled":{"type":"boolean","description":"Toggle the Phase 4 comments overlay/API on this site. true enables, false disables. Omit to leave unchanged."}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"get_site","description":"Get metadata for one site by slug: URL, name, access mode, file count, size, created/updated timestamps, and OG social-card setting (ogImageUrl + effective ogImage). Use to verify a publish succeeded (URL is strongly consistent after publish).","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"list_sites","description":"List all sites owned by the authenticated user. Returns array of {slug, url, name, title, excerpt, accessMode, commentsEnabled, ogImageUrl, ogImage, createdAt, updatedAt}. Use before update_site if you do not have the slug. For more than a handful of sites, prefer query_sites — it filters by metadata (df.project/df.type), searches free text, sorts, and paginates instead of returning everything.","parameters":{"type":"object","properties":{},"additionalProperties":false}}},{"type":"function","function":{"name":"query_sites","description":"Search and filter your sites — the narrowing counterpart to list_sites (which returns everything). Accepts structured metadata filters (dotted keys, AND semantics), a free-text search term q (fuzzy match over name, df.*/user metadata values, and extracted page content), sort (created | updated | name | relevance; relevance requires q and returns up to 100 ranked results with no cursor), limit (1-100, default 20), and cursor (pass the previous response's nextCursor; a cursor only replays under the sort it was minted for). Returns {sites: [{slug, url, name, title, excerpt, accessMode, metadata, fileCount, fileSizeBytes, commentsEnabled, ogImageUrl, ogImage, createdAt, updatedAt}], nextCursor}. Use to recall something you published earlier (\"the auth-redesign review from last week\"): filter metadata {\"df.project\": \"auth-redesign\"} and/or q=\"review\". Results are always scoped to the authenticated user.","parameters":{"type":"object","properties":{"metadata":{"type":"object","description":"Structured filters as dotted keys, e.g. {\"df.project\": \"auth-redesign\", \"df.type\": \"review\"}. Multiple keys AND together. An array value (user keys and df.type) requires ALL listed values to be present (jsonb containment). Same closed-world df.* rules as the REST ?metadata.* query.","properties":{},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"q":{"type":"string","maxLength":200,"description":"Free-text search term. Fuzzy, case-insensitive, matches partial words. Empty/omitted = filter-only."},"sort":{"type":"string","enum":["created","updated","name","relevance"],"description":"Result order. Default: created (newest first). relevance requires q."},"limit":{"type":"integer","description":"Page size 1-100; default 20."},"cursor":{"type":"string","description":"Opaque cursor from the previous response's nextCursor."}},"additionalProperties":false}}},{"type":"function","function":{"name":"delete_site","description":"Permanently delete a site by slug. The URL stops resolving. Any aliases pointing at this site become inactive (so the owner can repoint them). This is irreversible. Returns {slug, deleted: true}.","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"publish_markdown","description":"Publish a Markdown (.md) artifact. GFM-flavored. Top-level YAML frontmatter populates site metadata via the same closed-world schema set_metadata uses (df.pr, df.project, etc. land as if the agent had called set_metadata). Body metadata REPLACES frontmatter metadata if both are passed (locked decision #13). Returns {slug, url, version, versionUrl, name, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is 1 for a new site; versionUrl pins it (…/s/<slug>/?v=<version>); ogImage is the effective social-card URL. Use publish_html for plain HTML, publish_zip for multi-file archives. NOTE: the `name` arg sets the site's display name; it does NOT auto-fall-back to the frontmatter title -- pass it explicitly if you want it.","parameters":{"type":"object","properties":{"markdown":{"type":"string","description":"The .md document including any YAML frontmatter block."},"name":{"type":"string","description":"Display name. Not derived from frontmatter -- pass explicitly."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string"},"message":{"type":"string","description":"Optional commit-message-style note attached to v1."},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site. Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Omit for the auto-generated card."},"metadata":{"type":"object","description":"Optional explicit metadata. Mirrors set_metadata shape exactly. Body REPLACES frontmatter on conflict.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["markdown"],"additionalProperties":false}}},{"type":"function","function":{"name":"list_versions","description":"List version history for a site, newest first. Returns {versions: [{version, createdAt, author, message, byteSize, fileCount, manifestHash}], nextCursor}. Cursor pagination keyed on version number. Owner-only. Use before get_version when you do not know which version number to fetch.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"cursor":{"type":"string","description":"Pass the previous response's nextCursor to fetch older versions."},"limit":{"type":"integer","description":"Page size 1-100; default 20."}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"get_version","description":"Fetch one historical version of a site by version number. Returns {version, createdAt, author, message, manifestHash, fileCount, byteSize, metadataSnapshot, files: [{path, blobHash, byteSize}]}. metadataSnapshot is the metadata as it stood when this version was published (useful for diffing across versions). To browse the rendered content of a historical version, visit /s/<slug>/?v=<v> in a browser.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"version":{"type":"integer","description":"Version number (1-indexed). Get the list via list_versions."}},"required":["slug","version"],"additionalProperties":false}}},{"type":"function","function":{"name":"get_diff","description":"Diff two versions of a site by version number (Phase 4). Returns {slug, from, to, files: [{path, status:\"added\"|\"removed\"|\"modified\"|\"unchanged\", hunks}], metadata, cached}. hunks is a line-op array ({op:\"=\"|\"+\"|\"-\", text}) for modified text files (null for binary/unchanged/large). metadata is a jsondiffpatch delta of the df.* snapshot, or null. Read-only, owner-only. Use after update_site to reason about what changed since a comment was left (compare the comment's versionAtCreate to the current version).","parameters":{"type":"object","properties":{"slug":{"type":"string"},"from":{"type":"integer","description":"Base version number (1-indexed)."},"to":{"type":"integer","description":"Target version number (1-indexed). Get numbers via list_versions."}},"required":["slug","from","to"],"additionalProperties":false}}},{"type":"function","function":{"name":"get_metadata","description":"Read the metadata object on a site (df.* reserved keys + user keys). Returns {slug, metadata}. Use to discover the existing tagging before set_metadata (which uses REPLACE semantics).","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"set_metadata","description":"Set the metadata object on a site. REPLACE semantics (locked decision #13): the entire metadata column is overwritten with the argument. To clear, pass {}. Closed-world rules apply: seven reserved df.* keys (df.pr, df.repo, df.session, df.agent, df.project, df.type, df.parent) are accepted; unknown df.* keys reject with METADATA_UNKNOWN_RESERVED_KEY. User keys (no df. prefix) accept string | string[]. Total JSON-stringify size capped at 8 KB. Use update_site_settings for site behavior (name/accessMode/password) -- these are separate (TD-05).","parameters":{"type":"object","properties":{"slug":{"type":"string"},"metadata":{"type":"object","description":"The new metadata object. Nested under df.* for reserved keys; free-form at the top level for user keys.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string"},"repo":{"type":"string"},"session":{"type":"string"},"agent":{"type":"string"},"project":{"type":"string"},"type":{"type":"string"},"parent":{"type":"string"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["slug","metadata"],"additionalProperties":false}}},{"type":"function","function":{"name":"get_comments","description":"Read comments for a site from the Phase 4 API. Comments are VERSION-PINNED: by default this returns only the comments left on the site's current version. Pass `version` (an integer from list_versions) to read a specific version's comments instead — e.g. to address feedback left on an earlier version before it was re-published. Returns {format:\"dropfast-comment-spike-v1\", slug, viewingVersion, currentVersion, comments:[{id,status,bodyText,target,author,versionAtCreate,createdAt,links}], versionCounts:[{version,count}]}. Each comment carries links.resolve/links.reopen with the exact request to change its status (over REST: PATCH or PUT /api/v1/comments/{id}); over MCP use resolve_comment. versionCounts lists open-comment totals per version so you can see where unresolved feedback lives. Use before update_site when a human asks an agent to address review feedback.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"status":{"type":"string","enum":["open","resolved","all"],"description":"Default: open."},"version":{"type":"integer","description":"Which version's comments to read (>= 1). Default: the current version."}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"add_comment","description":"Create a plain-text comment on a comments-enabled site using the Phase 4 spike JSON target shape. Useful for agent-to-agent review. Body is plain text only (no HTML) and target should include path plus cssSelector and/or textQuote.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"bodyText":{"type":"string","minLength":1,"maxLength":2000,"description":"Plain text, 1-2000 chars. HTML tags reject."},"target":{"type":"object","description":"Requires at least one real anchor: cssSelector, textQuote, or both x and y.","properties":{"path":{"type":"string","minLength":1,"maxLength":512,"description":"Path inside the hosted site, e.g. / or /docs/."},"cssSelector":{"type":"string","minLength":1,"maxLength":1000},"textQuote":{"type":"object","properties":{"prefix":{"type":"string","maxLength":64},"exact":{"type":"string","minLength":1,"maxLength":512},"suffix":{"type":"string","maxLength":64}},"required":["exact"],"additionalProperties":false},"x":{"type":"number","minimum":0,"maximum":1,"description":"Viewport-relative x coordinate, 0-1. Must be paired with y when used as the only anchor."},"y":{"type":"number","minimum":0,"maximum":1,"description":"Viewport-relative y coordinate, 0-1. Must be paired with x when used as the only anchor."}},"required":["path"],"additionalProperties":false}},"required":["slug","bodyText","target"],"additionalProperties":false}}},{"type":"function","function":{"name":"resolve_comment","description":"Mark one Phase 4 spike comment open or resolved. Use after update_site when the agent has addressed a human comment. Returns the updated comment. The caller must be the site owner or the original comment author. (REST equivalent: PATCH or PUT /api/v1/comments/{id} with {\"status\":\"resolved\"} — see each comment’s links.resolve.)","parameters":{"type":"object","properties":{"commentId":{"type":"string"},"status":{"type":"string","enum":["open","resolved"],"description":"Default: resolved."}},"required":["commentId"],"additionalProperties":false}}},{"type":"function","function":{"name":"create_alias","description":"Create a short-link alias. Resolves at /_/<alias> with a 302 redirect to either an owned site (targetType=\"site\" + targetSiteId) or any external URL (targetType=\"external\" + targetUrl). Aliases are globally unique across all users.","parameters":{"type":"object","properties":{"alias":{"type":"string","description":"Lowercase letters, digits, hyphens. 2–48 chars. Reserved names rejected."},"targetType":{"type":"string","enum":["site","external"]},"targetSiteId":{"type":"string","description":"Required when targetType is \"site\"."},"targetUrl":{"type":"string","description":"Required when targetType is \"external\"."}},"required":["alias","targetType"],"additionalProperties":false}}},{"type":"function","function":{"name":"repoint_alias","description":"Change where an existing alias points without changing its name. Can repoint between site and external. Pass isActive=false to pause without deleting (resolver returns 404 while paused). Returns the updated alias row.","parameters":{"type":"object","properties":{"alias":{"type":"string","description":"Current alias name."},"targetType":{"type":"string","enum":["site","external"]},"targetSiteId":{"type":"string"},"targetUrl":{"type":"string"},"isActive":{"type":"boolean"}},"required":["alias"],"additionalProperties":false}}},{"type":"function","function":{"name":"list_aliases","description":"List all aliases owned by the authenticated user. Returns array of {alias, url, targetType, targetSiteId | targetUrl, isActive, hitCount, createdAt}.","parameters":{"type":"object","properties":{},"additionalProperties":false}}},{"type":"function","function":{"name":"delete_alias","description":"Permanently delete an alias. The /_/<alias> URL stops resolving. Returns {alias, deleted: true}. To pause without deleting, use repoint_alias with isActive=false instead.","parameters":{"type":"object","properties":{"alias":{"type":"string"}},"required":["alias"],"additionalProperties":false}}},{"type":"function","function":{"name":"create_share_link","description":"Mint a temporary, revocable share link that grants READ access to one of your sites via a bearer token in the URL (…/s/<slug>/?share=<token>). Use this when a human wants to give someone a private preview WITHOUT publishing the site — the link is the right tool for \"let me share this for review.\" IMPORTANT: this does NOT change the site's accessMode; the site stays private/password-protected and only holders of this exact link can view it. Do NOT reach for update_site_settings (accessMode: \"public\") when a preview link is what's wanted — that exposes the site to everyone. The token is a bearer secret shown ONCE, only inside the returned url. Returns {url, id, expiresAt, oneTimeUse, createdAt}. Every link expires (default 24h, 30-day ceiling); pass oneTimeUse:true for a link that self-consumes on first view. Revoke early with revoke_share_link.","parameters":{"type":"object","properties":{"slug":{"type":"string","description":"8-char slug of a site you own."},"expiresIn":{"type":"string","description":"How long the link stays valid. Shorthand (\"1h\", \"24h\", \"7d\") or an ISO-8601 duration (\"PT1H\", \"P7D\"). Default 24h; 30-day hard ceiling."},"oneTimeUse":{"type":"boolean","description":"When true, the link self-consumes on first successful view. Default false (reusable until it expires or is revoked)."}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"list_share_links","description":"List the temporary share links for a site you own — metadata only, NEVER the token (a bearer secret shown once at mint by create_share_link). Returns {links: [{id, oneTimeUse, expiresAt, usedAt, revokedAt, createdAt, status}]}, newest first; status is active | used | expired | revoked. This is a read-only audit view of who could preview the site; it does NOT reveal or change the site's accessMode (share links leave the site private and grant temporary bearer access alongside it). Use the id here with revoke_share_link to kill a link.","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}}},{"type":"function","function":{"name":"revoke_share_link","description":"Instantly revoke one temporary share link by id (get ids from list_share_links). The link stops working on the next redeem. Returns {id, revoked: true}. This only kills the temporary bearer link — it does NOT change the site's accessMode; the site keeps whatever visibility it already had (private/password stay private/password). To make a site broadly public instead, that is a deliberate update_site_settings change, not a revoke.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"id":{"type":"string","description":"The share-link id from list_share_links."}},"required":["slug","id"],"additionalProperties":false}}}],"gemini":[{"name":"publish_html","description":"Publish a single HTML page as a static site with a permanent URL. Use for single-file output. For multi-file sites with assets, use publish_zip. Returns {slug, url, version, versionUrl, name, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is the integer version number just created (1 for a new site); versionUrl pins that exact version (…/s/<slug>/?v=<version>). ogImageUrl is the stored OG-card setting (null = auto card); ogImage is the effective absolute card URL injected into the shared page for social previews. The URL is permanent and immediately live. TIP: set metadata.df.project and metadata.df.type at publish so query_sites can recall this site later — untagged sites pile up as unsearchable slugs.","parameters":{"type":"object","properties":{"html":{"type":"string","description":"The HTML document to publish. Treated as a complete index.html."},"name":{"type":"string","description":"Optional display name shown in the dashboard. Defaults to derived from <title>."},"accessMode":{"type":"string","enum":["public","private","password"],"description":"Visibility. Default: public."},"password":{"type":"string","description":"Required when accessMode is \"password\"."},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site so humans and other agents can leave structured feedback (later resolved via resolve_comment). Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path (e.g. \"/og_image.png\"). Omit for the auto-generated branded card. No base64 upload in v1 — bundle a root og_image.png in a publish_zip archive to host a custom image."},"metadata":{"type":"object","description":"Optional metadata written at publish (same closed-world shape as set_metadata). Set df.project (collection) + df.type (tag) so this site is recallable via query_sites and the dashboard organizer.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["html"],"additionalProperties":false}},{"name":"publish_zip","description":"Publish a ZIP archive as a multi-file static site (HTML + CSS + JS + images). ZIP must contain an index.html at the root. Use for sites with assets; use publish_html for single-page output. Returns {slug, url, version, versionUrl, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is the integer version just created (1 for a new site); versionUrl pins it (…/s/<slug>/?v=<version>). ogImage is the effective social-card URL. OG-CARD CONVENTION: a root-level og_image.png (or .jpg/.jpeg/.webp/.gif) in the archive is auto-used as the site's social card unless you pass ogImageUrl explicitly. Payload limit: 3 MB base64-encoded (~2.25 MB raw) due to platform body limits — use REST + S3 presigned URLs for larger sites. TIP: set metadata.df.project and metadata.df.type at publish so query_sites can recall this site later.","parameters":{"type":"object","properties":{"zip_base64":{"type":"string","description":"Base64-encoded ZIP archive."},"name":{"type":"string","description":"Optional display name."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string"},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site. Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Overrides the zip-root og_image.* convention when set. Omit to auto-detect a bundled og_image.png or fall back to the auto card."},"metadata":{"type":"object","description":"Optional metadata written at publish (same closed-world shape as set_metadata). Set df.project (collection) + df.type (tag) so this site is recallable via query_sites and the dashboard organizer.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["zip_base64"],"additionalProperties":false}},{"name":"update_site","description":"Replace the files of an existing site by slug. Same URL, new content. Accepts either raw HTML (single file) or a base64-encoded ZIP. Returns {slug, url, version, versionUrl, updatedAt}. version is the new integer version number; versionUrl pins it (…/s/<slug>/?v=<version>). Re-uploading byte-identical content is a no-op that returns the current (unchanged) version. To change name/accessMode/password without replacing files, use update_site_settings. To tag a version with metadata (df.pr, df.project, etc.), use set_metadata; df.* keys do NOT belong in update_site_settings.","parameters":{"type":"object","properties":{"slug":{"type":"string","description":"8-char slug from a prior publish."},"html":{"type":"string","description":"Single-page replacement HTML. Provide either html or zip_base64."},"zip_base64":{"type":"string","description":"Multi-file ZIP replacement."},"message":{"type":"string","description":"Optional commit-message-style note attached to this version (D2). Surfaces in list_versions and the version-history dashboard."}},"required":["slug"],"additionalProperties":false}},{"name":"update_site_settings","description":"Change a site's site-behavior settings (display name, access mode, password, commentsEnabled, ogImageUrl). Returns {slug, name, accessMode, commentsEnabled, ogImageUrl, ogImage, updatedAt}. Pass null to clear an optional field (ogImageUrl:null restores the auto social card). NOTE (TD-05): this tool owns site-behavior knobs only. Use set_metadata for df.* metadata (df.pr, df.project, df.session, ...); the namespaces are intentionally split so closed-world validation can run on metadata without coupling to site behavior.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string","description":"Display name. Pass null to clear."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string","description":"Required when accessMode is \"password\"."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Pass null to clear back to the auto-generated card; omit to leave unchanged."},"commentsEnabled":{"type":"boolean","description":"Toggle the Phase 4 comments overlay/API on this site. true enables, false disables. Omit to leave unchanged."}},"required":["slug"],"additionalProperties":false}},{"name":"get_site","description":"Get metadata for one site by slug: URL, name, access mode, file count, size, created/updated timestamps, and OG social-card setting (ogImageUrl + effective ogImage). Use to verify a publish succeeded (URL is strongly consistent after publish).","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"list_sites","description":"List all sites owned by the authenticated user. Returns array of {slug, url, name, title, excerpt, accessMode, commentsEnabled, ogImageUrl, ogImage, createdAt, updatedAt}. Use before update_site if you do not have the slug. For more than a handful of sites, prefer query_sites — it filters by metadata (df.project/df.type), searches free text, sorts, and paginates instead of returning everything.","parameters":{"type":"object","properties":{},"additionalProperties":false}},{"name":"query_sites","description":"Search and filter your sites — the narrowing counterpart to list_sites (which returns everything). Accepts structured metadata filters (dotted keys, AND semantics), a free-text search term q (fuzzy match over name, df.*/user metadata values, and extracted page content), sort (created | updated | name | relevance; relevance requires q and returns up to 100 ranked results with no cursor), limit (1-100, default 20), and cursor (pass the previous response's nextCursor; a cursor only replays under the sort it was minted for). Returns {sites: [{slug, url, name, title, excerpt, accessMode, metadata, fileCount, fileSizeBytes, commentsEnabled, ogImageUrl, ogImage, createdAt, updatedAt}], nextCursor}. Use to recall something you published earlier (\"the auth-redesign review from last week\"): filter metadata {\"df.project\": \"auth-redesign\"} and/or q=\"review\". Results are always scoped to the authenticated user.","parameters":{"type":"object","properties":{"metadata":{"type":"object","description":"Structured filters as dotted keys, e.g. {\"df.project\": \"auth-redesign\", \"df.type\": \"review\"}. Multiple keys AND together. An array value (user keys and df.type) requires ALL listed values to be present (jsonb containment). Same closed-world df.* rules as the REST ?metadata.* query.","properties":{},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"q":{"type":"string","maxLength":200,"description":"Free-text search term. Fuzzy, case-insensitive, matches partial words. Empty/omitted = filter-only."},"sort":{"type":"string","enum":["created","updated","name","relevance"],"description":"Result order. Default: created (newest first). relevance requires q."},"limit":{"type":"integer","description":"Page size 1-100; default 20."},"cursor":{"type":"string","description":"Opaque cursor from the previous response's nextCursor."}},"additionalProperties":false}},{"name":"delete_site","description":"Permanently delete a site by slug. The URL stops resolving. Any aliases pointing at this site become inactive (so the owner can repoint them). This is irreversible. Returns {slug, deleted: true}.","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"publish_markdown","description":"Publish a Markdown (.md) artifact. GFM-flavored. Top-level YAML frontmatter populates site metadata via the same closed-world schema set_metadata uses (df.pr, df.project, etc. land as if the agent had called set_metadata). Body metadata REPLACES frontmatter metadata if both are passed (locked decision #13). Returns {slug, url, version, versionUrl, name, accessMode, commentsEnabled, ogImageUrl, ogImage}. version is 1 for a new site; versionUrl pins it (…/s/<slug>/?v=<version>); ogImage is the effective social-card URL. Use publish_html for plain HTML, publish_zip for multi-file archives. NOTE: the `name` arg sets the site's display name; it does NOT auto-fall-back to the frontmatter title -- pass it explicitly if you want it.","parameters":{"type":"object","properties":{"markdown":{"type":"string","description":"The .md document including any YAML frontmatter block."},"name":{"type":"string","description":"Display name. Not derived from frontmatter -- pass explicitly."},"accessMode":{"type":"string","enum":["public","private","password"]},"password":{"type":"string"},"message":{"type":"string","description":"Optional commit-message-style note attached to v1."},"commentsEnabled":{"type":"boolean","description":"Enables the Phase 4 comments overlay/API on the new site. Defaults to true (comments on by default for new sites); pass false to opt out."},"ogImageUrl":{"type":"string","description":"OG social-card image: an https:// URL or a single-leading-slash site path. Omit for the auto-generated card."},"metadata":{"type":"object","description":"Optional explicit metadata. Mirrors set_metadata shape exactly. Body REPLACES frontmatter on conflict.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string","description":"e.g. \"github:org/repo#247\""},"repo":{"type":"string","description":"e.g. \"github:org/repo\""},"session":{"type":"string","description":"Session id"},"agent":{"type":"string","description":"Agent identifier"},"project":{"type":"string","description":"Project label"},"type":{"type":"string","description":"Site type tag (or use the array form via repeated frontmatter)"},"parent":{"type":"string","description":"Parent site slug"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["markdown"],"additionalProperties":false}},{"name":"list_versions","description":"List version history for a site, newest first. Returns {versions: [{version, createdAt, author, message, byteSize, fileCount, manifestHash}], nextCursor}. Cursor pagination keyed on version number. Owner-only. Use before get_version when you do not know which version number to fetch.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"cursor":{"type":"string","description":"Pass the previous response's nextCursor to fetch older versions."},"limit":{"type":"integer","description":"Page size 1-100; default 20."}},"required":["slug"],"additionalProperties":false}},{"name":"get_version","description":"Fetch one historical version of a site by version number. Returns {version, createdAt, author, message, manifestHash, fileCount, byteSize, metadataSnapshot, files: [{path, blobHash, byteSize}]}. metadataSnapshot is the metadata as it stood when this version was published (useful for diffing across versions). To browse the rendered content of a historical version, visit /s/<slug>/?v=<v> in a browser.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"version":{"type":"integer","description":"Version number (1-indexed). Get the list via list_versions."}},"required":["slug","version"],"additionalProperties":false}},{"name":"get_diff","description":"Diff two versions of a site by version number (Phase 4). Returns {slug, from, to, files: [{path, status:\"added\"|\"removed\"|\"modified\"|\"unchanged\", hunks}], metadata, cached}. hunks is a line-op array ({op:\"=\"|\"+\"|\"-\", text}) for modified text files (null for binary/unchanged/large). metadata is a jsondiffpatch delta of the df.* snapshot, or null. Read-only, owner-only. Use after update_site to reason about what changed since a comment was left (compare the comment's versionAtCreate to the current version).","parameters":{"type":"object","properties":{"slug":{"type":"string"},"from":{"type":"integer","description":"Base version number (1-indexed)."},"to":{"type":"integer","description":"Target version number (1-indexed). Get numbers via list_versions."}},"required":["slug","from","to"],"additionalProperties":false}},{"name":"get_metadata","description":"Read the metadata object on a site (df.* reserved keys + user keys). Returns {slug, metadata}. Use to discover the existing tagging before set_metadata (which uses REPLACE semantics).","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"set_metadata","description":"Set the metadata object on a site. REPLACE semantics (locked decision #13): the entire metadata column is overwritten with the argument. To clear, pass {}. Closed-world rules apply: seven reserved df.* keys (df.pr, df.repo, df.session, df.agent, df.project, df.type, df.parent) are accepted; unknown df.* keys reject with METADATA_UNKNOWN_RESERVED_KEY. User keys (no df. prefix) accept string | string[]. Total JSON-stringify size capped at 8 KB. Use update_site_settings for site behavior (name/accessMode/password) -- these are separate (TD-05).","parameters":{"type":"object","properties":{"slug":{"type":"string"},"metadata":{"type":"object","description":"The new metadata object. Nested under df.* for reserved keys; free-form at the top level for user keys.","properties":{"df":{"type":"object","properties":{"pr":{"type":"string"},"repo":{"type":"string"},"session":{"type":"string"},"agent":{"type":"string"},"project":{"type":"string"},"type":{"type":"string"},"parent":{"type":"string"}},"additionalProperties":false}},"additionalProperties":{"type":"string"}}},"required":["slug","metadata"],"additionalProperties":false}},{"name":"get_comments","description":"Read comments for a site from the Phase 4 API. Comments are VERSION-PINNED: by default this returns only the comments left on the site's current version. Pass `version` (an integer from list_versions) to read a specific version's comments instead — e.g. to address feedback left on an earlier version before it was re-published. Returns {format:\"dropfast-comment-spike-v1\", slug, viewingVersion, currentVersion, comments:[{id,status,bodyText,target,author,versionAtCreate,createdAt,links}], versionCounts:[{version,count}]}. Each comment carries links.resolve/links.reopen with the exact request to change its status (over REST: PATCH or PUT /api/v1/comments/{id}); over MCP use resolve_comment. versionCounts lists open-comment totals per version so you can see where unresolved feedback lives. Use before update_site when a human asks an agent to address review feedback.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"status":{"type":"string","enum":["open","resolved","all"],"description":"Default: open."},"version":{"type":"integer","description":"Which version's comments to read (>= 1). Default: the current version."}},"required":["slug"],"additionalProperties":false}},{"name":"add_comment","description":"Create a plain-text comment on a comments-enabled site using the Phase 4 spike JSON target shape. Useful for agent-to-agent review. Body is plain text only (no HTML) and target should include path plus cssSelector and/or textQuote.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"bodyText":{"type":"string","minLength":1,"maxLength":2000,"description":"Plain text, 1-2000 chars. HTML tags reject."},"target":{"type":"object","description":"Requires at least one real anchor: cssSelector, textQuote, or both x and y.","properties":{"path":{"type":"string","minLength":1,"maxLength":512,"description":"Path inside the hosted site, e.g. / or /docs/."},"cssSelector":{"type":"string","minLength":1,"maxLength":1000},"textQuote":{"type":"object","properties":{"prefix":{"type":"string","maxLength":64},"exact":{"type":"string","minLength":1,"maxLength":512},"suffix":{"type":"string","maxLength":64}},"required":["exact"],"additionalProperties":false},"x":{"type":"number","minimum":0,"maximum":1,"description":"Viewport-relative x coordinate, 0-1. Must be paired with y when used as the only anchor."},"y":{"type":"number","minimum":0,"maximum":1,"description":"Viewport-relative y coordinate, 0-1. Must be paired with x when used as the only anchor."}},"required":["path"],"additionalProperties":false}},"required":["slug","bodyText","target"],"additionalProperties":false}},{"name":"resolve_comment","description":"Mark one Phase 4 spike comment open or resolved. Use after update_site when the agent has addressed a human comment. Returns the updated comment. The caller must be the site owner or the original comment author. (REST equivalent: PATCH or PUT /api/v1/comments/{id} with {\"status\":\"resolved\"} — see each comment’s links.resolve.)","parameters":{"type":"object","properties":{"commentId":{"type":"string"},"status":{"type":"string","enum":["open","resolved"],"description":"Default: resolved."}},"required":["commentId"],"additionalProperties":false}},{"name":"create_alias","description":"Create a short-link alias. Resolves at /_/<alias> with a 302 redirect to either an owned site (targetType=\"site\" + targetSiteId) or any external URL (targetType=\"external\" + targetUrl). Aliases are globally unique across all users.","parameters":{"type":"object","properties":{"alias":{"type":"string","description":"Lowercase letters, digits, hyphens. 2–48 chars. Reserved names rejected."},"targetType":{"type":"string","enum":["site","external"]},"targetSiteId":{"type":"string","description":"Required when targetType is \"site\"."},"targetUrl":{"type":"string","description":"Required when targetType is \"external\"."}},"required":["alias","targetType"],"additionalProperties":false}},{"name":"repoint_alias","description":"Change where an existing alias points without changing its name. Can repoint between site and external. Pass isActive=false to pause without deleting (resolver returns 404 while paused). Returns the updated alias row.","parameters":{"type":"object","properties":{"alias":{"type":"string","description":"Current alias name."},"targetType":{"type":"string","enum":["site","external"]},"targetSiteId":{"type":"string"},"targetUrl":{"type":"string"},"isActive":{"type":"boolean"}},"required":["alias"],"additionalProperties":false}},{"name":"list_aliases","description":"List all aliases owned by the authenticated user. Returns array of {alias, url, targetType, targetSiteId | targetUrl, isActive, hitCount, createdAt}.","parameters":{"type":"object","properties":{},"additionalProperties":false}},{"name":"delete_alias","description":"Permanently delete an alias. The /_/<alias> URL stops resolving. Returns {alias, deleted: true}. To pause without deleting, use repoint_alias with isActive=false instead.","parameters":{"type":"object","properties":{"alias":{"type":"string"}},"required":["alias"],"additionalProperties":false}},{"name":"create_share_link","description":"Mint a temporary, revocable share link that grants READ access to one of your sites via a bearer token in the URL (…/s/<slug>/?share=<token>). Use this when a human wants to give someone a private preview WITHOUT publishing the site — the link is the right tool for \"let me share this for review.\" IMPORTANT: this does NOT change the site's accessMode; the site stays private/password-protected and only holders of this exact link can view it. Do NOT reach for update_site_settings (accessMode: \"public\") when a preview link is what's wanted — that exposes the site to everyone. The token is a bearer secret shown ONCE, only inside the returned url. Returns {url, id, expiresAt, oneTimeUse, createdAt}. Every link expires (default 24h, 30-day ceiling); pass oneTimeUse:true for a link that self-consumes on first view. Revoke early with revoke_share_link.","parameters":{"type":"object","properties":{"slug":{"type":"string","description":"8-char slug of a site you own."},"expiresIn":{"type":"string","description":"How long the link stays valid. Shorthand (\"1h\", \"24h\", \"7d\") or an ISO-8601 duration (\"PT1H\", \"P7D\"). Default 24h; 30-day hard ceiling."},"oneTimeUse":{"type":"boolean","description":"When true, the link self-consumes on first successful view. Default false (reusable until it expires or is revoked)."}},"required":["slug"],"additionalProperties":false}},{"name":"list_share_links","description":"List the temporary share links for a site you own — metadata only, NEVER the token (a bearer secret shown once at mint by create_share_link). Returns {links: [{id, oneTimeUse, expiresAt, usedAt, revokedAt, createdAt, status}]}, newest first; status is active | used | expired | revoked. This is a read-only audit view of who could preview the site; it does NOT reveal or change the site's accessMode (share links leave the site private and grant temporary bearer access alongside it). Use the id here with revoke_share_link to kill a link.","parameters":{"type":"object","properties":{"slug":{"type":"string"}},"required":["slug"],"additionalProperties":false}},{"name":"revoke_share_link","description":"Instantly revoke one temporary share link by id (get ids from list_share_links). The link stops working on the next redeem. Returns {id, revoked: true}. This only kills the temporary bearer link — it does NOT change the site's accessMode; the site keeps whatever visibility it already had (private/password stay private/password). To make a site broadly public instead, that is a deliberate update_site_settings change, not a revoke.","parameters":{"type":"object","properties":{"slug":{"type":"string"},"id":{"type":"string","description":"The share-link id from list_share_links."}},"required":["slug","id"],"additionalProperties":false}}],"restFallback":{"openapi":"https://dropfast.dev/api/openapi.json","docs":"https://dropfast.dev/docs/openapi","base_url":"https://dropfast.dev/api/v1"}}