MCP reference
The appilot-mcp server is the tool surface behind the app-configurator skill.
It is a standard stdio MCP server, so any MCP-capable agent can mount it.
Environment
| Variable | Required | Meaning |
|---|---|---|
APPILOT_BASE_URL | yes for live calls | Backend URL, cloud (https://api.appilot.space) or on-premise |
APPILOT_PAT | for writes and the server-side echo | A scoped service token (appilot_pat_...) |
APPILOT_APP_ID | optional | Default app id for tools that omit one |
APPILOT_SOAK_STORAGE_STATE | optional | Path to a Playwright storageState JSON for an authenticated site session |
The server is endpoint-agnostic: the same binary serves cloud and on-premise
instances. Nothing is hard-coded to a host. It can start and expose tools before
these variables are supplied, but live calls report that APPILOT_BASE_URL is
not configured.
Tools
| Tool | Input | What it does |
|---|---|---|
capabilities | None | Instance version, migration level, schema versions, configurable entities, and the closed vocabularies those entities accept (entityVocabularies). Call first, and take enum values from it rather than guessing them. |
read_config | appId?, locales? | Normalized snapshot of the app's content model: views, controls, forms, tools, zones, action plans, knowledge. A gaps array names any entity the credential could not read, so a partial snapshot cannot be mistaken for an empty app. Locales default to the languages the app's domains declare. |
validate_config | appId?, locales? | Severity-ranked findings against the health contract (runs locally, plus a server-side plan echo). |
entity_template | kind, appId? | A valid skeleton for one entity kind: every container field present, the fields the write path refuses without, the closed enums this instance accepts, and what to get right. Writes nothing and needs no scope. |
create_entity | kind, body | Create a view, control, form, tool, zone, action plan, knowledge article or session template. |
update_entity | kind, id, patch | Patch any of the eight kinds by the row id read_config returns. The server re-validates the marker and identifier trust boundary. |
delete_entity | kind, id | Delete by row id. Refused with 409 and the dependents named when something still references it. To retire without removing, patch is_active to false instead. |
validate_action_plan | sections, formValues?, appId? | Run the server-side marker trust boundary over draft sections without persisting. Use it before writing a plan. |
inspect_page | url?, html? | Read a page and rank locator candidates by whether they survive the next render, with the forms, their fields and the submit control. url loads it in a browser; html scans pasted markup where no browser is available. The candidate type is an Appilot locator_type, ready to paste into a control body. |
export_config | appId? | Export the whole configuration as a portable, versioned bundle (backup/clone artifact). Secrets never leave; the bundle carries references only. |
import_config | appId?, bundle, mode, dryRun?, expectedCurrentHash?, allowUnhealthy? | Import a bundle. Always dry-run first (the default): review the per-entity diff, then commit merge (upsert, never deletes) or replace (exact match, incl. deletions) with the dry-run's confirm hash. Every commit stores an automatic pre-restore snapshot, so any import can be undone. |
soak_selectors | url, appId? | Headless-browser check that each control selector resolves on the live page. Needs Playwright. |
report_feedback | kind, title, body?, failingTool?, errorCode? | Report a gap or a defect in Appilot, or in this configuration. The answer states whether this organization's plan includes a reply. A repeat of the same problem increments a counter instead of opening a second report. Needs feedback:write. |
list_feedback | status?, kind?, limit? | This organization's reports, with status and occurrence count. |
Provisioning and integration
These create and wire up apps rather than editing the content model of one that
already exists. They need a provision:write token, and apply_manifest also
needs config:write when the manifest carries a config bundle.
| Tool | Input | What it does |
|---|---|---|
whoami | None | Which organization the connected credential reaches, which app it is narrowed to, and which scopes it holds. Call it before any write so a mistyped or revoked token fails here instead of as an opaque 401 halfway through a task. Returns no secret. |
list_apps | None | The organization's apps with their domains, verification status and widget keys. What to call before create_app so an agent works on the app that exists rather than provisioning a second one. |
create_app | name, description?, domains?, widgetKeyName?, isTestKey?, dryRun? | Provision an app, the domains it runs on, and optionally a widget key, plus the script tag and boot snippet to paste into the host application. Idempotent: re-running converges on the existing app instead of creating a second one. A live key needs a verified domain, so on a new hostname it comes back blocked with the TXT record to publish; pass isTestKey to get a test key immediately. The widget key secret is returned exactly once, at creation, and belongs only in the host backend. |
plan_manifest | manifest | Diff an appilot.app-manifest against the live instance and return what would change: provisioning actions per app, domain and key, the config-bundle import diff, and the health findings over the resulting state. Writes nothing, and returns a planToken. |
apply_manifest | manifest, planToken, expectedCurrentHash?, mode? | Provision and configure an app from the manifest. Requires the planToken from plan_manifest for the same manifest, so an apply always follows a preview of exactly what is being applied; a mismatch means the manifest changed since it was previewed and the apply is refused. |
verify_domain | domain or domainId | Report a domain's verification status with the exact TXT record to publish, and re-check it. The check reads live DNS, so it costs nothing to re-run while the record propagates. |
integration_snippet | appId?, widgetKey? | The script tag and boot call for an app that already exists, without provisioning anything. What to call when the integration was lost rather than never written. |
scaffold_integration | framework, widgetKey?, widgetScriptUrl?, idNamespace? | Return the source a host application needs: the identity relay for its backend (the one security-critical piece), the widget boot call, and a client-action example. Frameworks: next, express, fastify, hono, remix, sveltekit, and other for a framework-agnostic relay you adapt. Returns file contents for you to write into your repository; this server never touches your filesystem. |
scaffold_agent_first | capability, slug, appId?, endpoint?, clientSide? | The four artifacts one capability needs so a user could complete it through the assistant alone: the HTTP-proxy tool, the client action when the operation belongs in the page, the action plan that is the procedure, and the knowledge article that carries the meaning. Also returns the order to create them in. |
verify_integration | url, appId?, tokenEndpoint? | Load the real page and report what is actually true: whether the instance is reachable, whether the page's domain resolves to a tenant, whether the widget is referenced and boots, whether the identity relay answers correctly for an unauthenticated caller, and whether Appilot logged console errors. Catches the failures no static check can see, such as a stale bundle, a rotated key, or a domain resolving to the wrong tenant. The browser half is skipped rather than failed without Playwright. |
The manifest is a file you keep in your own repository under version control, so
an app's provisioning and its configuration are reviewed the way code is. Plan
before apply is structural here, not advisory: apply_manifest will not run
without a planToken derived from the manifest bytes it is given.
Remote endpoint
Clients that cannot start a local program (ChatGPT, claude.ai) connect to a deployed endpoint instead. One deployment serves one Appilot instance and holds no credential of its own: each person who connects supplies their own service token, once, on a consent screen.
Ask us for the container image, then run it with three variables:
docker run -p 8080:8080 \
-e APPILOT_BASE_URL=https://appilot.internal.example.com \
-e APPILOT_MCP_PUBLIC_URL=https://mcp.example.com \
-e APPILOT_MCP_OAUTH_SECRET="$(openssl rand -base64 32)" \
appilot-mcp
| Variable | Meaning |
|---|---|
APPILOT_BASE_URL | The instance this endpoint serves |
APPILOT_MCP_PUBLIC_URL | The public origin people reach, used in OAuth metadata and redirects |
APPILOT_MCP_OAUTH_SECRET | At least 32 characters. Rotating it disconnects every client |
APPILOT_MCP_ALLOWED_HOSTS | Optional extra Host values, comma separated, behind a proxy |
Clients discover everything else themselves: the MCP endpoint is
https://<host>/mcp, and the OAuth metadata sits at the usual well-known paths.
What a person sees is one screen asking for a service token, which the endpoint checks against your instance before granting anything. The scopes granted are the ones that token already carries, never more. To cut a connection off, revoke the token in the Backoffice: the next call fails.
The live soak tool is not available over the remote endpoint. Driving a real browser belongs on your own machine, over the local connection.
To connect ChatGPT to an instance with no public endpoint at all, use OpenAI's Secure MCP Tunnel against the local server instead. The tunnel dials out from inside your network, so nothing needs to listen on the internet.
From the command line
The same package is a command, so the manifest workflow runs in CI without an MCP client in the loop:
npx -y appilot-mcp --help # every subcommand
npx -y appilot-mcp install-skill # install the app-configurator skill
npx -y appilot-mcp plan appilot.manifest.json # the diff, and a plan token
npx -y appilot-mcp apply appilot.manifest.json --plan <token>
plan and apply are the same calls the MCP tools make, with the same rule:
an apply needs a plan token derived from the exact manifest bytes that were
previewed. It reads APPILOT_BASE_URL and APPILOT_PAT from the environment,
so a CI job holds a config:write token in its secret store and nothing else.
Creating entities
entity_template(kind) first, then create_entity. The template returns a body
with every container field present, the fields the write path refuses without,
and the closed enums this instance accepts, which removes the two failures that
otherwise repeat: a missing empty array or object, and a guessed enum. A control
locator is id, class_text, aria, xpath or semantic; a CSS selector list
goes under class_text.
Order is not a preference. An entity that names another comes second: controls
before the form that names them, the tool with its credential before a session
template whose preflight calls it, and validate_action_plan before a plan is
written. delete_entity refuses and names the dependents rather than leaving a
plan pointing at nothing.
Before authoring a control, look at the page. inspect_page ranks candidates by
how likely they are to still resolve after the next deploy and marks a
framework-generated id as fragile, which is the defect that passes every static
check and then finds no element. A scan of pasted markup is not proof: run
soak_selectors before trusting a locator.
export_config and import_config remain the backup, clone and promotion path.
Do not round-trip a whole bundle to add one article.
Service-token scopes
| Scope | Grants |
|---|---|
config:read | Read, validate, and export configuration |
config:write | Apply fixes (create/update controls, forms, plans, knowledge) and import/restore bundles |
provision:write | Create apps, claim domains, and mint widget keys (create_app, apply_manifest) |
feedback:write | Send reports to Appilot (report_feedback). The only scope that sends anything out of your organization |
provision:write is deliberately separate from config:write: a CI token whose
job is keeping content in sync should not also be able to claim a domain and mint
credentials. All four scopes are mintable from the Backoffice preset; the two writing presets include feedback:write.
Over the remote endpoint the scopes you approve are the scopes the connection holds. A tool outside them is refused by the endpoint itself, not only by the instance, so approving read-only really is read-only even when the token behind it could write.
Which scopes get offered is up to the client. A connector that asks for none is treated as asking to read, so the connection comes out read-only however wide your token is, and the first write answers with the scopes it holds and the one it needs. Reconnect and approve that scope; the confirmation screen lists what the connection will carry before it is granted.
Mint tokens in the Backoffice (Admin → Service tokens). Only the hash is stored; the raw token is shown once. Tokens are org-scoped, optionally app-scoped, revocable, and can carry an expiry. A write attempt with a read-only token returns a clear scope error. Mint a write-scoped token rather than working around it.
Local vs live checks
- Static gate (
validate_config) runs entirely locally, with no public network required, reusing Appilot's own validators so it never drifts from the runtime. - Server echo re-runs the plan trust boundary without persisting.
- Live soak (
soak_selectors) drives the real page; it is the only check that catches an unstable selector before your users do.
Health contract
validate_config checks: plan actionability (a create flow must enter a value
and submit; a plan that stops at opening an element fails), single-executable-marker
steps, marker resolution, selector stability, scope/view_path consistency, i18n coverage,
knowledge scope and hygiene, procedure-not-in-KB, identifier hygiene, and plan
discovery. Findings are ranked critical → low; a configuration passes when it has
no critical or high findings.