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 argument —
MCP 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 expose —
MCP 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:
| Type | Key fields |
|---|---|
Site | id, name, slug, projectId, framework (auto-detected), repositoryUrl |
Deployment | id, siteId, target (preview / staging / production), status, previewUrl?, createdAt |
plan | the create_change_plan proposal: a list of intended file edits plus a human-readable summary; not applied until apply_site_patch |
checks | per-check results from the workspace check matrix (lints, types, custom CI hooks) |
summary | a 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.