STAGING
MCP tools

MCP tool reference

Every Showly MCP tool — scope, parameters, return shape, audit behavior.

Every tool the Showly MCP server exposes. Each entry lists the required scope, the input schema in short form, the return shape, and what gets written to the audit log.

All tools require an authenticated MCP client. Tokens are issued from Workspace settings → MCP clients.

When a call is blocked

Every failure Showly returns uses the same envelope — the ones a tool decides and the ones the MCP layer decides before the tool runs (a scope the token lacks, an oversized argument or result). ok is false and error is a string code you can branch on, never an object. A refusal from the MCP layer also sets the MCP isError flag, so a client that branches on it still sees a failed call; the text beside the flag is this same JSON.

Three failures are not this envelope, because they are answered by the MCP SDK before any Showly code runs. All three arrive as isError: true with a plain-text message:

  • A missing or wrong-typed argumentMCP error -32602: Input validation error: Invalid arguments for tool <name>: …. This is the most common failure an integration produces, so parse defensively.
  • A tool name this server does not exposeMCP error -32602: Tool <name> not found.
  • An unexpected crash — a transport fault or a bug.

An argument the tool does not declare is not on that list and is not a failure at all: the SDK drops unknown keys before the call runs, so the tool sees only the arguments it declared.

Treat a body you cannot parse as an unknown error rather than assuming a code.

{
  ok: false,
  error: "insufficient_credits",         // stable machine code
  message: "A production deployment costs 5 credits and this workspace has 2 left.",
  status?: 402,                          // upstream HTTP status, when there was one

  resolvedBy: "agent" | "human",         // who can actually unblock this
  actionUrl?: "https://showly.ai/app/billing#upgrade",
  humanAction?: "Open Plan & Billing and add credits, then tell the agent to retry.",
  agentNext: {
    kind: "retry" | "retry_with" | "call_tool" | "poll" | "wait_for_human" | "stop",
    tool?: "publish_site",
    afterSeconds?: 10,
    note: "After the balance changes, start the publish over from step 1."
  }
}

Read resolvedBy first. "agent" means you can fix it yourself with the tools you already have — pick a different previewSlug, fetch the id you were missing, or call the tool agentNext.tool names. "human" means no sequence of tool calls will help: relay humanAction and actionUrl to the user, then follow agentNext.

actionUrl is absolute and points at a page in the Showly web app, never into an admin-only area. Relay actionUrl as a link whenever it is there, and relay humanAction every time — the sentence is what makes the link usable by the right person. Plan & Billing is the case that matters: it is limited to workspace owners, admins and the billing role, and checkout needs billing:write on top of that, so a member who follows the link is sent back to the dashboard. Showly cannot tell which of the two is reading your message — the page decides from the human's own session, while an MCP token describes the agent — so the link always ships and humanAction carries the caveat: an owner acts on it, anyone else forwards it. The legacy webUpgradeUrl on a plan or credits failure carries the same URL and the same caveat.

The same four fields appear on the one success that needs a person: request_publish returns resolvedBy, actionUrl, humanAction and agentNext at the top level, exactly where a failure puts them, so if (result.actionUrl) works on both. They are repeated inside data as well, next to the older webApprovalUrl, so an integration reading data.actionUrl keeps working.

Two older fields still ship alongside actionUrl and carry the same value, so existing integrations keep working: webVerificationUrl on email_verification_required, and webApprovalUrl on a successful request_publish.

list_projects

Scope: project:read

Lists projects (workspaces) the token can see.

Input:  {}
Output: { ok, data: Array<{ id, name, slug, createdAt }> }
Audit:  mcp.list_projects

list_sites

Scope: site:read

Lists sites in the current workspace. Pass projectId to scope the listing; a project-scoped MCP token forces this filter regardless of the parameter.

Input:  { projectId?: string }
Output: { ok, data: Array<Site>, view: ListSitesView }
Audit:  mcp.list_sites

This is the one tool that answers in two text blocks. The first is a summary written on the server — the site names, which of them are public, and one recommended next step — so it reads the same on every call. The second is the JSON envelope above, unchanged: read it from content[content.length - 1], not from content[0]. The same envelope is also returned as structuredContent, and view is the projection both the summary and the interactive rendering below are built from.

A client that declares the MCP Apps extension io.modelcontextprotocol/ui in its initialize capabilities also receives _meta.ui.resourceUri on this tool, pointing at a ui:// HTML resource the host renders in a sandboxed iframe. A client that does not declare it never sees that metadata, and nothing else about the result changes.

get_site_context

Scope: site:read

Returns the site, its detected framework, routes, referenced env var names, the latest preview URL, and the last production deployment id for a given siteId. See the Site shape in [Common types](#common-types).

Input:  { siteId }
Output: { ok, data: { site, framework, routes, envReferences, latestPreviewUrl, lastProductionDeploymentId } }
Audit:  mcp.get_site_context  (records siteId)

create_change_plan

Scope: site:read

Produces a change plan proposal. Does _not_ modify any files. The agent typically reads the returned plan, asks the user for confirmation, then calls apply_site_patch.

Input:  { siteId, request: string }
Output: { ok, data: { siteId, request, plan, nextStep } }
Audit:  mcp.create_change_plan

apply_site_patch

Scope: site:write

Stages file edits for a site. The staged changeset is temporary and must be materialized by create_preview.

Input:  { siteId, files: Array<{ path, content }>, message: string }
Output: { ok, data: { changesetId, siteId, fileCount, ttlSeconds, nextStep } }
Audit:  mcp.apply_site_patch  (records siteId + changesetId + file count)

create_preview

Scope: preview:create

Builds the patched workspace and produces a preview URL. The user owns the private-access decision: omit access for a short server-generated XXX-XXX password, pass a custom 6–128 character password, or select organization (Pro+) or organization_or_password. Generated plaintext is returned once and cannot be retrieved later. A Preview cannot be public; publish it Live when it should be visible to everyone. previewSlug optionally chooses a separate one-label <previewSlug>.showly.site address for an existing site.

Input:  { changesetId?, siteId?, files?, previewSlug?, access?: { mode, password? } }
Output: { deploymentId, previewUrl, framework?, fileCount?, access: { mode, passwordConfigured, password? } }
Audit:  mcp.create_preview

create_github_preview

Scope: preview:create

Builds a private Preview from the latest commit on a site's connected GitHub branch. The installation credential stays inside Showly. Omit access to get a short server-generated XXX-XXX password returned once, pass a custom 6–128 character password, or choose organization-member access on Pro+. This tool never publishes Live. Poll get_preview_status with the returned deploymentId.

If the site has no active GitHub App repository, connect it in Showly Web first. Repository auto-build currently supports static deployment targets; a dynamic container target returns repository_build_target_unsupported before anything is queued. An unverified Showly email returns email_verification_required with a Web URL to complete verification.

set_preview_access

Scope: preview:create

Changes an existing Preview or published Live deployment's access policy without changing its URL. Use the ready production id from list_deployments to protect a published site; the tool keeps its historical name for client compatibility. Saving a password mode rotates the password; pass a 6–128 character value or omit password to generate a short XXX-XXX sharing code server-side. The response shows the new plaintext password once alongside previewUrl. Every policy change invalidates previously issued preview-access cookies.

Organization modes verify the visitor's active Showly organization membership, so teammates sign in instead of sharing a password. organization_or_password keeps that internal flow while allowing an external reviewer to use a password.

Input:  { deploymentId, access: { mode: "password" | "organization" | "organization_or_password", password? } }
Output: { deploymentId, target, previewUrl, access: { mode, passwordConfigured, password? }, policyVersion, advancedDeploymentControls }
Audit:  mcp.set_preview_access

Example custom password rotation:

{
  "deploymentId": "00000000-0000-4000-8000-000000000000",
  "access": { "mode": "password", "password": "ABC-123" }
}

retry_deployment

Scope: preview:create

Rebuilds a failed or canceled preview deployment. Re-supply the same files you gave create_preview — the source is not retained server-side, so files is required. Mints a new deploymentId (the failed one stays as history) in status: "building" and returns it for polling. Each retry counts against your monthly deploy quota — it is a fresh build. Returns 409 not_retryable if the deployment is still building or already ready, 404 if it isn't visible to your token, 402 if you're over quota.

Input:  { deploymentId, files: [{ path, content }] }
Output: { ok, data: { deploymentId, status: "building", pollUrl, retriedFrom } }
Audit:  mcp.retry_deployment

run_checks

Scope: checks:run

Runs the workspace's check matrix (lints, types, custom CI hooks) against a preview. checks is a list of { id, status } rows (lint / typecheck / build / audit-gate); summary is a one-line string such as "3 passed / 1 pending".

Input:  { deploymentId }
Output: { ok, data: { deploymentId, checks: [{ id, status }], summary } }
Audit:  mcp.run_checks

request_publish

Scope: publish:request

Opens an approval request for a ready preview deployment. The response includes a webApprovalUrl deep link; surface that link so the user can review the exact Preview and complete any plan-required second-person approval. Publishing does not require OTP/MFA enrollment. The user bound to the MCP token must have a verified Showly account email. If the tool returns email_verification_required, send the user to webVerificationUrl to resend and complete verification before retrying.

Input:  { deploymentId, message: string }
Output: { approvalId, deploymentId, state: "pending", expiresAt, reused, webApprovalUrl }
Audit:  mcp.request_publish

publish_site

Scope: publish:confirm

Publishes a ready preview deployment to production directly from the conversation — for solo workspaces and plans without approval workflows. The user bound to the MCP token must have a verified Showly account email. If the tool returns email_verification_required, send the user to webVerificationUrl, wait for them to complete verification, and then restart the two-step flow; the site is not live yet. Two-step human-confirmed: call with siteId + deploymentId (no confirmationToken) to get a summary + short-lived confirmationToken; show the user what is about to go live, then call again with the token. Step 2 returns 202 publishing. On plans with approval workflows enabled, use request_publish instead — this tool routes you there.

Step 1: { siteId, deploymentId }                        → { confirmationToken, summary }
Step 2: { siteId, deploymentId, confirmationToken }      → { siteId, deploymentId, status: "publishing" }
Audit:  mcp.publish_site

Hosts that render MCP Apps can skip step 1's round trip: the ready-Preview card carries a Publish live button, and a click sends the user's confirmation into the conversation as a message. Treat that message as the confirmation — run the preflight and the confirmed publish back to back and answer once, with the outcome. Nothing else changes: the same two calls, the same server-issued token, the same blockers, and the same credit cost as any production deployment.

get_preview_status

Scope: preview:read

Returns the current status of a preview deployment. Optionally long-polls (default 30s, up to 60s) until the status transitions away from a known value — useful after request_publish while waiting on a reviewer. When status is failed or canceled, the result also carries errorCode, errorMessage, stage, and a short logTail explaining why the build failed; pair with retry_deployment to rebuild.

Input:  { deploymentId, waitForChange?: boolean, currentStatus?: string, timeoutMs?: number }
Output: { ok, data: Deployment & { productionUrl?, errorCode?, errorMessage?, stage?, logTail? }, changed?, timedOut? }
Audit:  mcp.get_preview_status

get_deployment_logs

Scope: logs:read

Returns the build log tail for a deployment (the last lineCount lines, default 200, of the captured build output). source discriminates db (real log lines), pending (deployment exists but no log captured yet — still building or no tail), or not-found (no such deployment for this token).

Input:  { deploymentId, lineCount?: number }
Output: { ok, data: { deploymentId, lineCount, source, lines } }
Audit:  mcp.get_deployment_logs

diagnose_deployment

Scope: logs:read

Self-diagnose one of your own deployments. Returns a single structured, AI-consumable diagnostic bundle so the agent can reason about why a build failed in one call — then fix the source and retry_deployment — rather than stitching together separate get_preview_status / get_deployment_logs reads. The bundle aggregates: the build failure (stage, errorCode, errorMessage, a logTail), related runtime errors from Sentry (correlated by commit SHA + environment + a window around the deploy, fail-soft), the deploy's ops-job status, the org's quota status, any agent-pushed clientLogs (redacted + capped), and deterministic hypotheses — likely root causes with a confidence (e.g. quota_exceeded / build_install_failed), derived by rules (not AI) as a high-quality starting point.

Tenant-isolated: you can only diagnose deployments in your own org. A deployment id that isn't visible to your token returns 404 (indistinguishable from a non-existent id — no cross-org existence leak). This is the agent-facing twin of the staff Diagnostics Center bundle; both share one backend aggregator. Calls GET /deployments/:deploymentId/diagnostics.

Input:  { deploymentId }
Output: { ok, data: { deployment, failure, jobRun, quota, sentry, clientLogs, hypotheses } }
Audit:  mcp.diagnose_deployment

list_templates

Scope: template:read

Lists Showly site templates available to the current token. Pair with create_site_from_template to onboard a new site without a Git repo.

Input:  { framework?: string }
Output: { ok, data: Array<{ slug, displayName, description, framework, screenshots }> }
Audit:  mcp.list_templates

create_site_from_template

Scopes: template:create, site:write

Materialises a new Showly-managed site from a template and builds its first private Preview. siteSlug is the stable <siteSlug>.showly.site address for both this first Preview and later Live. Omitted access generates a server-owned password returned exactly once with initialPreviewUrl; organization modes require Pro.

Input:  { projectId, templateSlug, name, siteSlug, variables?: Record<string, unknown>, access?: { mode, password? } }
Output: { ok, data: { siteId, projectId, initialVersionId, initialPreviewDeploymentId, initialPreviewUrl, access, templateSlug, createdAt } }
Audit:  mcp.create_site_from_template

create_site_from_html

Scopes: site:write, preview:create

Creates a new Showly-managed site directly from plain HTML/CSS/JS files — no template, no framework, no Git repo. Pass either the files inline (index.html required; encoding: "base64" for binary assets), or a sourceBundleId for a large source you uploaded out-of-band via request_upload_url (exactly one of files / sourceBundleId). The site is created and its first preview is built in one call; poll the returned deploymentId with get_preview_status. siteSlug becomes the shared <siteSlug>.showly.site Preview/Live address. Production stays on the publish flow.

Input:  { projectId, name, siteSlug, files?: Array<{ path, content, encoding?: "utf8" | "base64" }>, sourceBundleId?, framework?, access?: { mode, password? } }
Output: { ok, data: { siteId, deploymentId, status | previewUrl, access, ... } }
Audit:  mcp.create_site_from_html

request_upload_url

Scopes: site:write, preview:create

Mints a short-lived, single-use upload URL for a large site source that should not be passed through the model. PUT a tar archive to the returned uploadUrl with Content-Type: application/x-tar, then call create_site_from_html with the returned sourceBundleId instead of files.

Input:  {}
Output: { ok, data: { uploadUrl, sourceBundleId, contentType, expiresInSeconds } }
Audit:  mcp.request_upload_url

request_download_url

Scope: site:read

The read counterpart to request_upload_url. Pass a visible deploymentId to receive a short-lived, single-use downloadUrl for its retained source archive. Download and edit the archive locally, then upload the new source with request_upload_url and pass its sourceBundleId to create_preview. Returns source_not_retained (422) when no source archive is available; use get_site_files for small sites.

Input:  { deploymentId }
Output: { ok, data: { downloadUrl, expiresInSeconds } }
Audit:  mcp.request_download_url

claim_trial_site

Scopes: site:write

Claims a site created through Showly's public trial flow into the current authenticated account, so it stops expiring and becomes permanent. Pass the server-provided trialId + guestToken. Fails if the trial already expired (start a fresh public trial) or the workspace has an explicit active-site override (delete an unused site or contact Showly Support, then retry). Free and Pro both allow unlimited sites and published versions by default; upgrading does not add site slots.

Input:  { trialId: string, guestToken: string }
Output: { ok, data: { trialId, siteId, claimed: true } }
Audit:  mcp.claim_trial_site

delete_preview

Scope: preview:create

Soft-deletes a preview deployment by id. Returns deletedAt. Idempotent — deleting an already-deleted preview returns 404 preview_not_found. Only previews are deletable here; production is unaffected.

Input:  { deploymentId }
Output: { ok, data: { deploymentId, deletedAt } }
Audit:  mcp.delete_preview

delete_site

Scope: site:delete

Soft-deletes a site and cascades to its deployments, versions, and custom domains. Two-step human-confirmed: call with siteId (no confirmationToken) to get a summary (the slug + how many deployments cascade) plus a short-lived confirmationToken; show the user, then call again with the token to delete. Recoverable only from backup.

Step 1: { siteId }                          → { confirmationToken, summary: { siteSlug, cascade: { deployments } } }
Step 2: { siteId, confirmationToken }        → { siteId, deletedAt }
Audit:  mcp.delete_site

list_site_domains

Scope: site:read

Lists the custom domains attached to a site, including the current guided step, DNS records, certificate state, recovery CTA, management page, and live URL. Results default to 50 rows and accept up to 100. When pagination.nextCursor is not null, pass it back unchanged as cursor; cursors are opaque and bound to one site.

For a non-empty result, follow each target row's domains[].journey; there is no top-level journey. A top-level journey is returned only for an empty list, where it guides the first domain connection. Poll a target domain only while its phase is setting_up_https, and stop for needs_attention.

Input:  { siteId, limit?, cursor? }
Output: { ok,
          journeyGuide: { steps, whatShowlyGivesYou },
          domains: [{ id, hostname, status, isLive, certStatus, liveUrl,
                      manageUrl, dnsRecords, proxyNote, apexNote?,
                      journey: { phase, currentStep, stepStatuses,
                                 whereYouAre, userAction, agentAction,
                                 actionUrl }, recovery? }],
          pagination: { count, total, nextCursor },
          journey? }
Audit:  mcp.list_site_domains

add_custom_domain

Scope: site:write · Available on every plan

Attaches a customer's own domain to a site and returns the DNS records the user must publish at their domain provider.

You cannot complete this step for them. The claim creates a pending record and routes no traffic; the domain only becomes real once the customer edits DNS at whoever they bought it from. Hand them the records, say plainly that nothing happens until they add them, and wait. On a root domain the response carries an apexNote — surface it, because a plain CNAME is not valid at a zone apex. Every response also carries a proxyNote: relay it, because the CNAME must be published unproxied (on Cloudflare, a grey cloud — new records are orange) or the certificate can never be issued, and the TXT record verifies either way so nothing else in the flow will catch it.

Keep the returned verificationToken; verify_custom_domain needs it and it is shown only once.

Input:  { siteId, hostname }
Output: { ok, domain: { id, hostname, status, dnsRecords, proxyNote, apexNote?, verificationToken }, nextStep }
Audit:  mcp.add_custom_domain

verify_custom_domain

Scope: site:write · Available on every plan

Re-checks DNS for a pending domain. Call it after the user says they have added the records. It succeeds only if the record is actually published and propagated — a failure usually means "not yet", not "broken", so wait a few minutes and retry rather than reporting an error.

On success the TLS certificate is requested automatically and the domain goes live within the hour. Poll list_site_domains for isLive.

Input:  { siteId, domainId, token }
Output: { ok, domain: { id, hostname, status, isLive, ... } }
Audit:  mcp.verify_custom_domain

Removing a domain is deliberately not available to agents. Archiving a live domain takes the customer's site offline at an address they have advertised, instantly and with nothing outside Showly needing to agree — so it stays a person's action in the dashboard. See ADR 0015.

list_site_versions

Scope: site:read

Lists a site's version history (newest first): id, source, changeSummary, author, createdAt. Pair with get_site_files (passing a versionId) to read that version's content.

Keyset-paginated. limit caps at 100 per page; to reach older versions, pass the previous response's pagination.nextCursor back as cursor. A nextCursor of null means you have reached the end of the history. The cursor is opaque and bound to one site — it pins the catalog to the instant of your first page, so versions created while you page cannot shift rows onto a page you already read.

hasMore is deprecated and mirrors pagination.nextCursor !== null; prefer pagination.

Input:  { siteId, limit?, cursor? }
Output: { ok, data: {
          versions: [{ id, source, changeSummary, authorUserId, createdAt }],
          hasMore,
          pagination: { limit, nextCursor: string | null }
        } }
Audit:  mcp.list_site_versions

list_deployments

Scope: site:read

Lists a site's deployments (newest first): id, target (preview / staging / production), status, url, createdAt. This is where the deploymentId comes from — use the returned id with retry_deployment, delete_preview, request_publish, or publish_site.

Input:  { siteId }
Output: { ok, data: [{ id, siteId, target, status, url, createdAt }] }
Audit:  mcp.list_deployments

get_site_files

Scope: site:read

Reads a site version's file tree (path → content) so you can see the current content before editing. Pass siteId + versionId (from list_site_versions). Large/manifest-backed versions return files: null plus a note.

Input:  { siteId, versionId }
Output: { ok, data: { versionId, source, changeSummary, files: Record<string,string> | null, note? } }
Audit:  mcp.get_site_files

diff_site_versions

Scope: site:read

Compares two versions and returns exactly what changed: per-file status (added / removed / changed) plus line-level add / remove / context. Pass siteId + versionA (the older "before") + versionB (the newer "after"), both from list_site_versions. Answers questions like "what changed between yesterday and today". Large/manifest-backed versions can't be diffed and return an error.

Input:  { siteId, versionA, versionB }
Output: { ok, data: {
          versionA: { id, source, changeSummary, createdAt },
          versionB: { id, source, changeSummary, createdAt },
          summary: { filesChanged, filesAdded, filesRemoved, linesChanged },
          files: Array<{ path, status, lines: Array<{ type, text }> }>
        } }
Audit:  mcp.diff_site_versions

rollback_to_version

Scope: rollback:confirm

Rolls PRODUCTION back to an older version and publishes it WITHOUT a preview — the most consequential tool here. Two-step human-confirmed: call with siteId + versionId (no confirmationToken) to get a warning + summary + confirmationToken; show the user the warning, then call again with the token. Step 2 returns 202 building — Showly builds a preview of that version and auto-promotes it to production. Recoverable by rolling forward to a newer version.

Step 1: { siteId, versionId }                       → { confirmationToken, warning, summary: { changeSummary, versionCreatedAt, previewed: false } }
Step 2: { siteId, versionId, confirmationToken }     → { siteId, versionId, deploymentId, status: "building" }
Audit:  mcp.rollback_to_version

Production tools (not MCP-exposed)

Not MCP-exposed — requires Web/API approval flow.

The legacy rollback_deployment action is not available through MCP. It does not appear in tools/list and cannot be called with an MCP token; use the applicable Showly web approval surface instead.

Production publishing is MCP-callable: publish_site (two-step confirmation, above) publishes directly, and rollback_to_version rolls production back — both kind: "confirm-publish"-style tools that never act on the first call.

Common types

The reference uses a few named shapes above. The concrete field set is shown in the end-to-end flow, which walks a full session with real JSON. Quick summaries:

TypeKey fields
Siteid, name, slug, projectId, framework (auto-detected), repositoryUrl
Deploymentid, siteId, target (preview / staging / production), status, previewUrl?, createdAt
planthe create_change_plan proposal: a list of intended file edits plus a human-readable summary; not applied until apply_site_patch
checksper-check results from the workspace check matrix (lints, types, custom CI hooks)
summarya roll-up of checks (counts of passed/failed) returned by run_checks

status is one of queued, building, ready, failed, canceled. ready is the terminal success state for both preview and production deployments. framework is auto-detected at build time and is one of astro, vite, next-export, static-html, custom, or unknown — it is not something you set on the manifest.

Versioning

Tool schemas follow semver via the MCP version field. Breaking changes ship a new tool name (apply_site_patch_v2); the old name continues to work for at least one release cycle with a deprecation note in notes.