{"$schema":"https://registry.modelcontextprotocol.io/schemas/server.v1.json","name":"io.dropfast/dropfast","displayName":"DropFast","description":"The artifact layer for AI agents. Publish HTML / Markdown / ZIP artifacts to permanent URLs with version history, queryable metadata, and built-in agent ergonomics.","version":"1.0.0-rc.1","homepage":"https://dropfast.dev","documentation":"https://dropfast.dev/docs","icon":"https://dropfast.dev/icon.png","license":"AGPL-3.0","remote":{"url":"https://dropfast.dev/api/mcp","transport":"streamable-http","authentication":"bearer"},"tools":[{"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."},{"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."},{"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."},{"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."},{"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)."},{"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."},{"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."},{"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}."},{"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."},{"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."},{"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."},{"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)."},{"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)."},{"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)."},{"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."},{"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."},{"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.)"},{"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."},{"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."},{"name":"list_aliases","description":"List all aliases owned by the authenticated user. Returns array of {alias, url, targetType, targetSiteId | targetUrl, isActive, hitCount, createdAt}."},{"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."},{"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."},{"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."},{"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."}],"categories":["hosting","agent-infra","artifact-storage","version-control"]}