Artifact Publisher

Agents publish self-contained HTML to a stable review link, with page-level feedback and revoke.

Hosted MCP Server

npx add-mcp 'https://app.artifactpublisher.com/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

Connect

Endpointhttps://app.artifactpublisher.com/mcp (HTTP POST)
AuthAuthorization: Bearer <publisher key>
KeyMinted once at Your workspace; only a hash is stored
Legacyhttps://app.currico.ch/mcp still works for existing agents; use the new endpoint for new setups

Claude Code:

claude mcp add --transport http artifact-publisher https://app.artifactpublisher.com/mcp \
  --header 'Authorization: Bearer <your-key>'

Other clients use the same endpoint and header. Never put the key in a prompt, repo, or artifact; rotate it if exposed.

Discovery without a key

Directories and agents can read what the server offers before they have a key. A POST to the endpoint with no Authorization header at all answers initialize, tools/list and ping (and accepts the notifications/initialized notification). The tool list is the one a key sees. It has no per-user data.

Keyless limits:

  • The request body may be at most 16 KiB (16,384 bytes). A malformed, non-JSON or larger body gets 401, like any other request without a key.
  • 60 calls per fixed one-minute window per client (IPv6 clients are counted by their /64 prefix). Past that the answer is 429 with Retry-After. The window is fixed, not sliding, so calls just before and after a boundary can add up to 120 within one minute. All IPv6 clients in one /48 network also share a ceiling of 240 calls per minute, so changing addresses inside that network does not give you a fresh budget.
  • If the rate limiter is unavailable, keyless calls get 503 with Retry-After: 30 (they are never served unchecked). Calls with a key do not use this limiter.

ping returns {} with or without a key (with a key it used to be an unknown method).

Everything else, including every tools/call, answers 401 with WWW-Authenticate: Bearer realm="Artifact Publisher MCP". A request that sends the header in any form is checked, so a wrong, empty or blank Authorization gets 401 even for tools/list. Requests that carry an Origin header must come from an Artifact Publisher site or app origin, otherwise 403; agents and other non-browser clients send none. GET /mcp answers 405 with Allow: POST.

A server card at /mcp/server-card (also at /.well-known/mcp/server-card.json, same body) follows the MCP Server Card format (SEP-2127). It names the endpoint, the streamable-http transport and the required Authorization header, and links to this page. It allows any origin to read it (including a browser preflight, which gets 204) and a successful answer is cached for an hour. Like every response on the app host, it is marked noindex.

Older installs that named the server artifact-relay keep working: the name is only a local label in your client, and the endpoint and key are unchanged.

Tools

The server’s tool discovery is authoritative for input schemas. To keep tools/list small (about 10.5 KB, under 16,000 bytes), it omits outputSchema, which is optional in MCP. Every result still has structuredContent and the same JSON as text, and the server checks each result against its contract before sending it.

ToolScopeDoes
publish_artifactartifacts:writePublish html (+ optional title, summary, expires_at, comments_enabled, visibility, allowed_emails) as version 1; returns the link
update_artifactartifacts:writeAdd a new immutable version with html; expected_version must equal the current one. Same link. Optional title renames the artifact (same rules as publish); give only title to rename without a new version. A rename also changes the title on the viewer pages of older pinned versions, and a title-only call counts against the publish rate limit
set_artifact_accessartifacts:writeChange visibility / allowed_emails without a new version
revoke_artifactartifacts:writeStop serving the artifact
list_artifactsartifacts:readList workspace artifacts, newest first, with unresolved comment counts
get_feedbackfeedback:readVersion-pinned comments with anchor_status for optional version (defaults to current)
reply_to_feedbackfeedback:writeReply in a comment thread. A comment takes one reply; a second one gets a conflict error and the first reply stays
configure_workspaceworkspace:configureRead/change comments_enabled, default_expiry_seconds, default_artifact_title
get_artifact_stateartifacts:readRead the shared JSON state { revision, values, resetAt }
set_artifact_stateartifacts:writeSet top-level state keys; others survive
reset_artifact_stateartifacts:writeClear the whole state document
get_artifact_board, reset_artifact_boardread / writeDeprecated and no longer listed. Calls by name still work; use the state tools
get_authoring_guideartifacts:readRead-only reference: theme catalogue (newest ref and every pinnable versions entry per theme), ap-* classes, example, snippet, size and sandbox rules. Optional theme (firn, firn@1) validates one theme. Still works while writes are paused for maintenance

workspace:configure is separate so a publish-only key cannot change workspace defaults.

Tools that take artifact_id accept the artifact id, its slug or its viewer URL. Only artifacts in the key’s workspace are found; anything else returns not_found.

Themes in tool results

A page opts into a built-in theme with <meta name="ap-theme" content="firn"> in <head>.

  • publish_artifact and update_artifact return theme_ref: the snapshot pinned on the new version (for example firn@1), or null when the HTML has no marker. The same value is in artifact.theme_ref.
  • list_artifacts and set_artifact_access return artifact.theme_ref for the current version.
  • A missing, duplicate, misplaced, empty, malformed, unknown or retired marker (or one with an unclosed quote) fails the call with invalid_input. The message lists the valid themes with their versions and the correct snippet.
  • A themed page with more than 3 KiB (3072 bytes) of its own <style> CSS gets a warning in warnings. It is never an error, and unthemed pages never get it. Usually the theme already covers what the CSS does; see get_authoring_guide.

Access modes

visibilityWho can open
public (default)Anyone with the link; no sign-in required. It is not listed, and it asks search engines not to index it. unlisted is accepted as an alias with the same access
privateThe workspace owner only (signed in)
allowlistSigned-in users whose primary email is in allowed_emails (required, max 50). Recipients need an Artifact Publisher account and find the page under Shared with me in their account; use public for accountless recipients

Fixed limits

A personal workspace on the free plan can hold 5 active artifacts and 50 versions across them. A paid personal workspace can hold 10,000 active artifacts and 1,000 versions; some paid plans allow more. Paid plans have no monthly limit. A version is made by every publish and every update with html. On the Free plan you can create up to 100 new versions a month. At the artifact limit, publish_artifact fails with a quota_exceeded error whose details name active_artifacts, the limit and the number in use. At the version limit, publish_artifact and update_artifact fail with a quota_exceeded error whose details name versions, the limit and the number in use. Revoked and expired artifacts do not count, neither as artifacts nor for their versions nor for storage bytes, so revoking one or letting it expire frees all three. Storage is also bounded on the content we retain: the bytes of every version, deleted ones included, are capped at 5 times the storage allowance on the free plan and 10 times on a paid plan, and publishing or updating past that returns a quota_exceeded error whose details name retained_storage. Retained content may be purged later. Renames, access changes, revokes, views and existing links are not affected. A workspace that is already above a cap keeps everything it has and is refused only new items over the cap. On the free plan, pages you share by link show a small Made with Artifact Publisher link in the corner of the viewer, outside your artifact. On paid plans it is off by default, and the workspace owner can turn it on from the account page.

Clients cannot change platform limits such as the document size cap or rate limits. Naming one in a settings patch is rejected, and the error lists the keys.

Tips for agent authors

  • Call publish_artifact / update_artifact once from the orchestrating agent with the full html string. On a validation error, read the error text; it names the bad fields.
  • Inline everything. The sandbox has no network access, so external fonts, images, and APIs will not load. Plain links still work: a click opens them in a new tab.
  • Update instead of publishing a second link.
  • For styling, add <meta name="ap-theme" content="firn"> and call get_authoring_guide rather than writing a large stylesheet.
  • For shared interactive state, use the injected ArtifactRelayState helper (or deprecated ArtifactRelayBoard) on the review link. Both are legacy names from the original codename, Artifact Relay, kept for compatibility.
  • Feedback is pull-only (get_feedback); there are no webhooks.

get_feedback returns 50 comments by default. Set limit from 1 to 100 and pass next_cursor back as cursor with the same artifact, version, unresolved_only and since. unresolved_only and since filter the comments. version (default current) only picks the version anchor_status is checked against, so comments made on every version are returned. Comments are ordered oldest first by creation time and ID. A null next_cursor means the page ends the result. unresolved_count counts all open comments on the artifact, independent of page size and filters.

The browser comments endpoint uses the same limit, cursor and next_cursor fields. It also accepts comment_id to fetch one thread, for comment permalinks. The reader starts with 50 comments and offers Load more comments. It refreshes one loaded page every 20 seconds while visible, rotating through loaded pages and individually fetched threads, so replies on later loaded pages can take longer to refresh. Unloaded comments do not contribute highlights or filter counts until loaded. Newly submitted comments appear immediately. Access, expiry and revocation checks run on every page request.

Text-quote anchors remain pinned to the version where the comment was created. get_feedback re-matches them at read time against version, returning resolved, fuzzy, or orphan (n/a for page comments, and for quotes that could not be assessed, see below). orphan means no match was found on that version; fuzzy matching is also skipped when its work budget runs out. Rewrites may orphan quotes; neither the anchor nor its original version is changed. Publisher replies use reply_to_feedback as before.

Re-matching has fixed limits, so it stays fast on every read. A quote comes back n/a (it stays in the drawer, but is not highlighted) when it could not be assessed: a read with more than 200 distinct quotes, an unexpected matcher error, or a page that is not matched at all, which is the case when the HTML is over 1 MiB or the body text is over 400,000 characters. The same happens when the markup is very dense: more than 150,000 tags, very deep nesting, many <template> tags, tags with hundreds of attributes or over 8 KiB outside quoted attribute values, or an unclosed comment or <script>. A quoted attribute value with no whitespace is not counted, so a normal inline data: image is fine. Values with whitespace are counted: base64 wrapped across lines, long SVG path data and long alt text can still make a page unreadable. Minified code inside <script> and <style> is not counted as tags. The comments themselves are unaffected and stay visible in the drawer.

Retry recovery

For publish_artifact, send Idempotency-Key on the HTTP request carrying the tool call, not in its tool arguments. Choose a fresh key for each intended publish and reuse it only when retrying that publish. A key must contain 8 to 128 characters from [A-Za-z0-9_-]. An empty header is treated as absent. Without a key, each successful call creates a new artifact.

Within 24 hours, the same key and input return the original result, even if the artifact later changes or is revoked. Reusing the key with different input gives a conflict with details reason idempotency_key_reused. Scope includes the workspace and publisher key. After 24 hours the key can create a new artifact. Clients must support setting the header on each publish request.

For publisher key creation, send idempotency_key in the JSON body of POST /api/account/keys, alongside name and optional scopes. It uses the same character rules and 24 hour window, scoped to the workspace and signed in subject. A repeated mint returns HTTP 409 with details reason key_already_minted, the original key_id and token_hint, never the token. If the first token was lost, revoke that key and mint again with a new idempotency key. Different input with the same key gives idempotency_key_reused. An empty JSON key is invalid; omit the field to mint without replay protection.