Description
Create a Merchant Center Custom Application and get it into the main nav: asks for your details in chat, scaffolds the app in its own git repo, deploys it to a host such as Vercel, registers it, installs it in the Organizations you choose (all projects or an allowlist), and opens the app. Use when the user wants a custom app or nav item, or it only works on localhost. Not for Custom Views, or for Connect service, event and job connectors (see commercetools-connect).
Installation
In any Claude Code session, run the following commands one at a time:
-
Add the commercetools AI marketplace:
/plugin marketplace add commercetools/commercetools-ai-plugins -
Install the commercetools AI plugin (Skills + Subagents + MCP):
/plugin install commercetools@commercetools
If you've updated the plugin or installed it in another window and need the current session to pick up the latest version:
/reload-plugins
commercetools/commercetools-ai-plugins. Then, click on the plugin and click Install.Instructions Included
Merchant Center Custom Application, zero to nav
End state: a Custom Application on a static host, registered in the user's Organization, Ready, installed in every Organization the user chose (all projects, or an allowlist per Organization), and in those Projects' main menu.
Administrators Team of an Organization. "Nothing configured" means no custom app yet, not no account. No Project: stop and say so.How this skill works
scripts/, not here. Registration is a dozen order-dependent calls, and its failures (wrong applicationId in the build, URL mismatch, Draft state) show up far from their cause. So the scripts read the real Merchant Center state, pick the next step, and act. A rerun after any interruption resumes. Your job: gather inputs, run the scripts in order, and handle what scripts cannot: the email click and, with deployment help off, the deploy.ok, error, stoppedAt, and checks[].detail; never assume a step worked. Run them as node <skill-dir>/scripts/<name>.mjs.entryPointUriPath may be, which permissions a Team needs, what a host must do) come from the documentation, so:-
Docs search (required, run first) โ The first time you use this skill in a session you must run this before answering. It gathers the latest verified documentation as your primary grounding, filtered to the products this skill covers. Use this script for documentation search while working with this skill; the Knowledge MCP covers everything else. Always confirm details against retrieved documentation rather than the skill text alone:
node "<this skill's directory>/scripts/docs-search.mjs" \ --query "Merchant Center Custom Application <the user's situation: register, deploy, install, permissions, host>" \ --app-name "<host app: claude-code, claude-chat, cursor, codex, copilot โ or the host's own name>" \ --model "<current-model>" \ --commercetools-project-key "<if known>" \ --commercetools-region "<if known>" \ --limit 5Pass the commercetools project key and region (as inapi.{region}.commercetools.com) only if already in your context; otherwise omit both. Never search files or ask the user for them. -
Then run Step 0. Headless is the only route, always, whether or not the user has a terminal. The manual steps are for redoing one piece by hand, or for when you cannot run commands.
Step 0: run headless (the whole flow, from nothing)
headless.mjs and the user answers in chat, whether or not they have a terminal. Do not offer or run onboard.mjs. The protocol (exit codes, the plan file shape, what each step may ask for) is in headless.md; read it first.Start by asking the user for the form data. Nothing touches their account until they have answered it and confirmed the last screen, and no answer is filled in for them.
- Ask the user for the app name first: the entry path, menu label and folder default from it. Then get the questions with
node <skill-dir>/scripts/intake.mjs form --app-name <name> --email <email>and ask them as written: region, project key, entry path, menu label, contact email, folder, language, hosting. Ask the deployment help question too (--deploy-help on|off, below). Validate the answers withintake.mjs check --answers <file>. - Run
scripts/headless.mjswith the answers as flags. It checks the tools and runs the Merchant Center sign-in: it prints[session] https://...on stderr and waits for the credentials file, so run it where you can read stderr and give the user that URL (exit 3 means it gave up waiting: rerun for a fresh URL). It then exits 4 with aneedsobject listing the Organizations and their projects. This is the Organization picker: ask which Organizations, and all projects or an allowlist for each, write the plan file (installPlan), and rerun with--install-plan <file>. - Exit 0 with
stage: "review"prints the review: the app's facts, the install targets, the owner Organization and the deployment help setting. Show it whole, and say plainly that the yes covers everything step 4 does: running thecreate-mc-applauncher, installing dependencies,git init, creating or linking the host project and deploying (with Connect: a GitHub push and a private Connector published and deployed), and registering and installing in the Merchant Center. Get an explicit yes. With several Organizations it exits 4 withneeds.ctxorgId: ask which one owns the app, rerun with--org-id <id>. Nothing in the account changes before the yes. - Rerun with
--confirmed. Do not pass it before the yes. The work, with live progress on stderr: create the app (create-mc-app),git init -b mainin the app folder (the repository root with Connect) with.mc-onboarding/gitignored, install dependencies and build, register in Draft (the verification email: ask the user to click it, and rerun after exit 3), deploy, host check, finish registration (Ready, installed everywhere chosen), open the browser. The host check polls on its own while the deploy propagates; a 401/403 or a certificate this machine cannot verify fails at once with what to do. A rerun resumes where it stopped. A step that needs a value the user must give (a team, a token, the production URL) exits 4 withneeds(ctx,label,optionsorenv): ask, then rerun with--set <ctx>=<value>,--production-url <url>, or the variable named inneeds.env(tokens and secrets go in the environment only). The provider files underreferences/hosting/say what each host asks and runs.
Deployment help on (the default for Vercel, Netlify, Cloudflare Pages and Connect) creates or links the site, builds locally and deploys, or for Connect pushes a tagged GitHub repository and publishes and deploys a private Connector; for Connect the folder named in the form is the repository root and the app is created one level down. Off, the user deploys and you pass the production URL.
--deploy-help on|offsets deployment help; ask, do not default it.--app-dir <dir>pointing at an existing app skips creating it and resumes from wherever things stand; repeat--hosting,--deploy-helpand--org-id, which are not stored.- Every command's output goes to
<app-dir>/.mc-onboarding/session.log;--helplists every option.
needs for the user.When to use the manual steps
Failure reading
| Symptom | Cause | Fix |
|---|---|---|
| App page reloads forever | entryPointUriPath in the Merchant Center differs from the config, or applicationId is wrong in the build (proxy router) | Compare window.app.entryPointUriPath in devtools with the URL; rebuild with the right applicationId. |
FORBIDDEN from the settings API | User is not in Administrators for the Organization | Add them, or use an Organization where they are. |
| Blank page after install | Host does not rewrite all paths to index.html | Run scripts/verify.mjs; it says when the rewrite is missing. The rule per provider is in references/hosting/. |
Works on localhost but not in the nav | Local dev bypasses registration | Check Ready state and install with register.mjs, then Step 7 of the manual steps. |
| Installed but not in the nav | Non-admins need the app's View permission on their Team | Grant it on the Team. Admins see it at once. |
register.mjs stops at INSTALL_MISMATCH | The app is installed in that Organization in the other mode (all projects vs allowlist) | The user switches the mode in the Merchant Center (Update Projects access) or uninstalls and reruns. Team permissions may need granting again after a reinstall, so ask first. |
register.mjs reports "No progress" | An action ran but state did not change | Inspect the state of the step named in the error. Do not loop. |
Checklist
- The user has a Project and is in the
AdministratorsTeam of the owner Organization - Every input came from the user: no guessed Organizations, owner, project key, hosting, or deployment toggle
- The headless run ended with the app Ready (exit 0 with
productionUrlandopened), or manual Steps 1 to 6 ended withdone: truefromregister.mjs - The app folder is its own git repository (
git init -b mainran there; the folder has a.git) -
scripts/verify.mjspassed against the production URL (the app path and a deeper path both answer 200) - The app was seen in the main nav of a deployed Project, or the user confirmed it and the report says the nav was not seen
- The config change (
applicationId,url) is committed so the next deploy matches what is registered
References
The headless run
scripts/headless.mjs is the only route, with no screens. The user never opens a shell. You gather every input in chat, run the script, read one JSON object from stdout, and act on the exit code. Step events go to stderr as [<step-id>] <type> <detail>. Every command's full output is in <app-dir>/.mc-onboarding/session.log (log in the result); read it when a step fails. Rerunning the same command resumes: finished steps are skipped from what exists on disk and in the Merchant Center.The sequence
-
Start by asking the user for the form data, in chat, before running anything that touches their account. Ask the app name first: the other defaults derive from it. Then, as in Step 1 of the manual steps:
node <skill-dir>/scripts/intake.mjs form --app-name <name>gives the batches,intake.mjs check --answers <file>validates the answers. Hosting is whatever the user already uses; Vercel, Netlify, Cloudflare Pages and Connect have deployment help. -
Sign in and list the install targets. Run the script with the form's answers and no plan:
node <skill-dir>/scripts/headless.mjs --app-name <name> --region <region> --project-key <key> \ --entry-point <path> --menu-label <label> --contact-email <email> --app-dir <dir> \ --template <starter-typescript|starter> --hosting <provider> [--deploy-help on|off]It checks the tools and the session. With no valid session it starts the login, prints[session] https://...on stderr and waits for the credentials file: give the user that URL and let the run wait. Then it exits 4 withneeds.ctxset toinstallPlanandneeds.options, the Organizations the user administers with their projects. -
Ask the user where to install: which Organizations, and all projects or an allowlist for each. Write the plan file, a JSON array with one entry per Organization:
orgId,orgName,allProjects(true or false),projectKeys(empty whenallProjectsis true, at least one key otherwise). -
Get the review: the same command plus
--install-plan <file>. Exit 0 withstage: "review". Showreviewto the user in full: the app (name, entry point, region, folder, template, project key, menu label), contact email, hosting, the plan, the owner Organization, deployment help. When the plan names several Organizations and no--org-idwas given, it exits 4 withneeds.ctxset toorgIdand the options: ask, rerun with--org-id <id>. Nothing in the account has changed yet. -
Get an explicit yes to that review. Then the same command plus
--confirmed. It creates the app, runsgit init -b mainin the app folder (the repository root with Connect; init only, no commit or remote), builds, registers, deploys, checks the host, finishes registration, and ends withproductionUrlandopened. -
Act on the exit code (table below). A need is answered with
--set <field>=<value>, or the named environment variable for a secret, and the same command again. -
Finish as in Step 7 of the manual steps:
openedis the Merchant Center URL of the app inside a Project. Open it, or hand it to the user, and confirm the menu item. Do not report success from exit 0 alone.
--confirmed before the user has seen the review and said yes. Do not choose Organizations, the owner, the hosting or the deployment help setting (--deploy-help on|off, default on for hosts that have it) for the user.Exit codes
| Code | Meaning | What to do |
|---|---|---|
| 0 | Done, or the review was printed (no --confirmed). | Show the review, or finish with opened. |
| 1 | A step failed: failedAt and error (or error alone for an unexpected failure). | Read the error and the log, fix the cause, rerun. |
| 2 | Bad or missing input: errors keyed by field. | Ask for the field, rerun. |
| 3 | A step was still waiting after --poll-limit polls: at. | Rerun; it resumes. Usual causes: the verification email (ask the user to click the link) or a login not finished (the run ends that login; the rerun starts a new one and prints a new URL). |
| 4 | A step needs a value: needs with ctx, label, and options, env or hint. | Ask the user, rerun with the answer. |
What a step may need
needs.ctx | When | Answer with |
|---|---|---|
installPlan | No plan file yet. | Write the file, add --install-plan <file>. |
orgId | The plan names several Organizations. | --org-id <id> from options. |
productionUrl | Deployment help off: the user deployed. | --production-url <https url>. |
vercelTeam | The Vercel login has several teams. | --set vercelTeam=<id> from options. |
vercelToken | The Vercel CLI is not signed in. | VERCEL_TOKEN |
netlifyTeam | The Netlify account has several teams. | --set netlifyTeam=<slug> from options. |
netlifyToken | The Netlify CLI is not signed in. | NETLIFY_AUTH_TOKEN |
cfAccount | The Cloudflare login has several accounts. | --set cfAccount=<id> from options. |
cfProject | Cloudflare refused the Pages project name. | --set cfProject=<name> |
cfToken | Wrangler is not signed in. | CLOUDFLARE_API_TOKEN |
ctClientId | Connect: the API client for the Project. | CTP_CLIENT_ID (or --set ctClientId=<id>) |
ctClientSecret | Connect: its secret. | CTP_CLIENT_SECRET |
ghVisibility | Connect: the GitHub repository to create. | --set ghVisibility=public or private. |
--set for vercelToken, netlifyToken, cfToken and ctClientSecret (exit 2), and no token appears in a subprocess command line or in the log. Polling waits (the verification email, the host check after a deploy, Connect publishing and deploying) poll by themselves every --poll seconds, up to --poll-limit times per run. The sign-in polls every 2 seconds regardless, so the default limit of 60 gives the user about two minutes; pass a larger --poll-limit when they may be slower.--app-dir <dir> pointing at an existing app resumes an interrupted run from wherever it stands. Region, entry point, project key, name and menu label are read from its config. Repeat --hosting, --deploy-help and --org-id (and for Connect, the repository folder as --app-dir), because they are not stored. --install-plan may be omitted when <app-dir>/install-plan.json exists. Add --confirmed. node <skill-dir>/scripts/headless.mjs --help lists every option.Cloudflare Pages
--help) with a direct upload of the local build. Follow other.md for the procedure, including the confirmation before the deploy. If a command differs from what the CLI prints, trust the CLI.- Production URL:
https://<project>.pages.dev, or a custom domain attached to the project. The project domain is stable; every deploy also gets a hashedhttps://<hash>.<project>.pages.devURL, which is never used. - Build:
pnpm build, output inpublic. - Rewrite: none to write. Without a
404.htmlin the output, Pages serves the root for every path that is not a static asset, which is the single-page behaviour the Merchant Center proxy needs. The host check proves it on the deeper path. - Deploy:
wrangler pages deploy public --project-name <project> --branch main --commit-dirty=true, to the production branch. - Env vars: only when the config uses
${env:...}placeholders. The build runs locally here and the upload is direct, so set them in the shell or a dotenv file beforepnpm build. The Pages project's environment variables only apply to builds Cloudflare runs from Git.
What the session runs with deployment help on
wrangler whoami --json. Not signed in: the session asks for an API token with Pages edit rights, carried asCLOUDFLARE_API_TOKENin the environment, or the user runswrangler loginin another terminal and presses Enter. With several accounts the user picks one; its id travels asCLOUDFLARE_ACCOUNT_ID, because Wrangler refuses to choose without a terminal.wrangler pages project list --json. The project named after the app folder is reused, with its.pages.devdomain as the production URL. Otherwisewrangler pages project create <name> --production-branch main, and the URL is read from the "It will be available at" line. A name Cloudflare refuses is asked for again.- Patches the config with the project URL, builds, deploys. One deploy is enough: the domain is known before the build.
- A redirect to a Cloudflare Access login page, or a 401/403, means Cloudflare Access covers the domain; the host check fails at once and says where to remove the Access application.
commercetools Connect
What is different from a static host
- Layout. The folder named in the form is the repository root. It holds
connect.yamland the app one level down, in a folder named like thedeployAsentry (the app folder name). Connect requires that match. A resume may name either level. - Package manager. Connect builds Node projects with npm or yarn only, so the app is scaffolded with
--package-manager npmand built locally withnpm run build. The pnpm build-script step does not apply. - URL. Connect provides
APPLICATION_URLat build time, so the config holdsurl: '${env:APPLICATION_URL}'instead of a literal. The local build sets a placeholder for it;register.mjssets it to the real URL when it syncs. The literal never needs to be committed. - Identity.
connect.yamldeclaresCUSTOM_APPLICATION_ID,ENTRY_POINT_URI_PATHandCLOUD_IDENTIFIER; the session passes the registeredapplicationId, the entry point and the cloud identifier as the Deployment's configuration. That is why registration (Draft, which yields the id) comes before the deploy, as on every host. - Repository. Connect fetches a public repository over HTTPS. A private one needs the SSH URL and read access for the
connect-mumachine user; the session grants it as a collaborator throughgh api. If that invitation is still pending when Connect fetches, the publish report fails at its "Fetch Connector repository" step: accept the invitation forconnect-mu(or make the repository public) and press r. - Deployment type. The session creates a
sandboxDeployment: it needs no production Project and is the type Connect deploys by default. A production Deployment is a later, separate decision: it needs a non-trial Project, and before the first one the user contacts their Customer Success Manager or commercetools support.
connect.yaml
deployAs:
- name: <app folder>
applicationType: merchant-center-custom-application
configuration:
standardConfiguration:
- key: CUSTOM_APPLICATION_ID
description: The Custom Application ID
required: true
- key: ENTRY_POINT_URI_PATH
description: The Application entry point URI path
required: true
- key: CLOUD_IDENTIFIER
description: The cloud identifier
default: '<cloud identifier>'
What the session runs with deployment help on
- API client. Connect needs an API client for the Project with the scopes
manage_connectorsandmanage_connectors_deployments. The session asks for its id, then its secret (kept in memory, never logged). A rejected secret is asked for again. - Repository and tag.
git init -b mainif needed,git add -A, a commit,gh auth status(the GitHub CLI must be signed in), and if there is noorigin, the user picks public or private and the session runsgh repo create <name> --<visibility> --source <root> --remote origin --push. Then a new tagv0.1.<n>is made and pushed. Every run makes a new tag, because Connect builds a tag, not a branch. - Connector. A
ConnectorStagedkeyed after the app folder is created with the repository and tag, the region, and the Project inprivateProjects, or repointed at the new tag withsetRepository. It is published for private use (certification: false). The step polls every 15 seconds until the status isPublished;Failedstops with the publishing report. - Deployment. A sandbox Deployment keyed
<name>-sandboxis created with the configuration above, or redeployed withupdateConnectorafter a republish. The step polls untilDeployed(up to 15 minutes) and takesapplications[0].urlas the production URL;Failedstops with the report. - The host check and the finish step then run as for any host.
When a publish fails on a fresh starter
Netlify
--help) so the settings live in the repo. Follow other.md for the procedure, including the confirmation before the deploy. If a command differs from what the CLI prints, trust the CLI.-
Production URL:
https://<site>.netlify.app, or a custom domain. The site name is known once the site exists, so the site is created or linked first. The CLI renames a site when the name is taken (a numeric suffix), so the URL is read from its output, never assumed. -
Build:
pnpm build(the starter'sbuildscript ismc-scripts build), publish directorypublic. -
Rewrite:
netlify.tomlin the project root:[build] command = "pnpm build" publish = "public" [[redirects]] from = "/*" to = "/index.html" status = 200Thestatus = 200is what makes it a rewrite and not a redirect. -
Deploy:
netlify deploy --prod --no-build --dir public --site <name> --jsonafter building. Theurlkey of the JSON is the production URL;deploy_urlis the per-deploy one and is never used. -
Env vars: only when the config uses
${env:...}placeholders. The build runs locally here and the deploy uploads it as is, so set them in the shell or a dotenv file beforepnpm build. The site's build environment settings only apply to builds Netlify runs itself.
What the session runs with deployment help on
netlify status --json. Not signed in: the session asks for a personal access token, carried asNETLIFY_AUTH_TOKENin the environment (never on the command line), or the user runsnetlify loginin another terminal and presses Enter.- Linked site (
linked: true): itssite-nameandsite-urlare used. Otherwisenetlify teams:list --json; with several teams the user picks one (the CLI would take the default by itself, which is exactly what must not happen), thennetlify sites:create --name <app folder> --account-slug <team> --json. - Writes
netlify.toml, patches the config with the site URL, builds, deploys. If the deploy lands on a different URL than planned, the config is patched to that URL and deployed once more. - A 401 or 403 from the host is Netlify site protection (a password or visitor access control); the host check fails at once and says where to turn it off.
Hosting contract (any provider)
A host qualifies if it meets these three requirements. The other files in this folder are how one provider meets them.
- A stable
httpsURL, known before the build.env.production.urlfeeds the compiled Content-Security-Policy, so the URL must be in the config beforemc-scripts buildruns. Use the production domain or a custom domain, never a per-deployment URL that changes on every build. If the host only reveals the URL after the first deploy, create or reserve the site first (most CLIs have a create or link command) and read the URL from it. - A rewrite of every path to
index.htmlwith status 200. A custom app is a single-page app and the Merchant Center proxy forwards/:projectKey/:entryPointUriPath/*to the host. A redirect is not a rewrite: the proxy does not follow it. - The
public/directory as the published output, produced bymc-scripts build(pnpm buildin the starter).mc-scripts buildalso compilesindex.html.
Procedure
- Get the production URL (requirement 1).
node <skill-dir>/scripts/patch-config.mjs --app-dir <dir> --url <url>- Add the provider's rewrite rule (requirement 2) to the repo, so it is code and not a dashboard setting.
- Build. Then show the user the deploy command and get their explicit confirmation before running it: it publishes the app. Deploy with the provider's tooling or Git integration.
node <skill-dir>/scripts/verify.mjswith the flags in Step 7 of the manual steps, and readhostCheck.problems. It tests the app path and a deeper path, so it catches a missing rewrite on any host.- Pass the URL to
register.mjs --production-url.
Providers with a file here
vercel.md, netlify.md, cloudflare.md and connect.md are the hosts with deployment help: the session runs their file for the user. surge.md is a manual host. For anything else, the commercetools Deployment overview has a guide per host.Surge
other.md for the procedure, including the confirmation before the deploy.- Production URL: you choose it:
https://<project>.surge.sh. It is known before deploying, so requirement 1 ofother.mdneeds no create or reserve step. - Build: as in
other.md, output inpublic. - Rewrite: Surge serves
200.htmlfor unknown paths. Copy the built file rather than renaming it as the guide does, soindex.htmlstill serves the root:cp public/index.html public/200.html. Re-copy after every build. - Deploy:
surge public https://<project>.surge.sh
Deploy to Vercel
register.mjs has written env.production.applicationId. The procedure and requirements are in other.md; this file is how Vercel meets them. Source: Deploy to Vercel, which uses the Git integration and dashboard settings; this file uses the CLI (63.1.2, flags from its --help) so the settings live in the repo. If a command differs from what the CLI prints, trust the CLI.1. Build settings as code
vercel.json so the settings are in the repo and not a dashboard checkbox:{
"framework": null,
"buildCommand": "pnpm build",
"outputDirectory": "public",
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
"framework": null with an explicit rewrite replaces the docs' Create React App preset: the preset guesses the rewrite, the file states it. The rewrite is required (requirement 2 in other.md).2. Link first, to learn the production URL
vercel link --yes creates the Vercel project. Show it and get the user's explicit yes first (the session's review screen is that yes on the session path).vercel link --yes
This creates the Vercel project without deploying.
- Several teams. Without a terminal the CLI refuses to pick ("Multiple teams found"). List them with
vercel teams ls, let the user pick, and add--scope <team>to every Vercel command. - Not signed in ("No existing credentials found"). Either
vercel loginin another terminal, or a token from the Vercel account settings passed as--token. - Production domain.
https://<project-name>.vercel.app, where the name is what the CLI reports for the linked project (.vercel/project.json, orvercel project inspect). When the name is taken in another team, Vercel appends a suffix (<name>-pi.vercel.app), so theAliasedline of the deploy output is the source of truth. Never use thehttps://<project>-<hash>-<team>.vercel.appdeployment URL: it changes on every deploy. - Deployment Protection. In its default (Standard) mode it covers deployment URLs and previews, not the production domain. If the production domain answers 401 or 403, protection covers all deployments and must be turned off in the project settings, or the Merchant Center proxy cannot reach the app.
3. Write the URL into the config, build locally, deploy that build
node <skill-dir>/scripts/patch-config.mjs --app-dir . --url https://<project-name>.vercel.app
vercel pull --yes --environment production
vercel build --prod
vercel deploy --prebuilt --prod
env.production.url feeds the compiled Content-Security-Policy. vercel pull fetches the project settings and production environment into .vercel/, which vercel build needs. Building locally with vercel build and uploading with --prebuilt keeps the pnpm version and the allowBuilds decisions on the user's machine, where they are already proven, instead of trusting a remote build. Outside the session's review screen, show the user the deploy command and get their confirmation before running it: it publishes the app. If the Aliased line of the deploy names a different domain, patch the config to that domain and deploy once more. Environment variables only matter if the config uses ${env:...} placeholders in production; they resolve at the local build. The starter's CTP_INITIAL_PROJECT_KEY is development only.4. Check the host
scripts/verify.mjs (see other.md). If the Aliased line showed a different domain than the one in the config, stop: patch the config to match, and redeploy.5. Hand back to register.mjs
--production-url in Step 6 of the manual steps. The config already holds it, so the sync only has to push it to the Merchant Center.What the session runs with deployment help on
vercel link --yes (several teams become a list to pick from; a missing login asks for a token as a secret input), write vercel.json, patch the config with the planned domain, vercel pull, vercel build --prod, vercel deploy --prebuilt --prod, then a rebuild with the Aliased domain when it differs from the planned one. A 401 or 403 from the production domain fails the host check at once with the Deployment Protection explanation above.Manual steps
<skill-dir> is where this file's parent SKILL.md lives. Every script prints JSON: read ok, error, stoppedAt, and checks[].detail. If you can run commands, use the headless run (headless.md) instead. Stay here when you cannot run commands at all (the user runs each one) or when one piece must be redone by hand.Step 1: gather inputs (manual)
Walk the user through the form one batch at a time:
node scripts/intake.mjs form --app-name <name> [--hosting <provider>] [--email <email>]
--hosting and --email only when known; a known provider drops that question. Ask each batch in batches in order, unchanged, as multiple-choice questions where the agent supports them. Free-text fields offer a suggestion plus "Type it myself"; accept what the user types. Write the answers to a JSON file keyed by question id, then check them:node scripts/intake.mjs check --answers <file>
errors by re-asking only that question, until ok: true. next is the Step 2 command. If the user has no Project, stop. Guess nothing.| Input | Notes |
|---|---|
| Project key | A Project in the owner Organization, used as --initial-project-key when scaffolding. Install targets are chosen in Step 4. |
| Region | Cloud identifier: gcp-eu, gcp-us, gcp-au, aws-eu, aws-us. Must match the Project. |
entryPointUriPath | Unique per region, with reserved names rejected. It also names the user permissions, so it is hard to change later. |
| Menu label | What the nav shows. Defaults to the app name. |
| Maintainer contact email | Required once per Organization. Gets a verification email. |
| App directory | Where to scaffold. |
| Hosting provider | Whatever the user already uses. If none, suggest one that is written up (Vercel, Netlify, Cloudflare Pages, Surge, or Connect, which hosts the app itself); do not assume. For Connect read hosting/connect.md before Step 3: it changes the folder layout and the package manager. |
Step 2: preflight
node scripts/preflight.mjs --region <region> --entry-point <path> --hosting <provider>
ok: false before continuing. The usual ones: no or expired session (detail holds the exact npx @commercetools-frontend/mc-scripts login --mc-api-url ... command, which opens a browser), and a missing or logged-out hosting CLI (detail names the install and login command). The user runs each command in their terminal, then preflight runs again. The app does not exist yet, which is why login goes through npx.Step 3: scaffold
This downloads and runs a package and then installs dependencies. Show the command and get the user's explicit yes before running it (on the session path, the review screen is that yes).
npx @commercetools-frontend/create-mc-app@latest <app-dir> --template starter-typescript \
--entry-point-uri-path <path> --initial-project-key <project-key> \
--cloud-identifier <region> --package-manager pnpm --yes
--template starter for plain JavaScript. Then set mainMenuLink.defaultLabel in custom-application-config.mjs to the menu label./graphql with X-Graphql-Target: ctp, via useMcQuery), so it works against the user's Project with no extra wiring. For a different commercetools API, change oAuthScopes in the config to the scopes it needs, and keep them minimal: the Merchant Center grants the app exactly these.create-mc-app can exit with ERR_PNPM_IGNORED_BUILDS on pnpm 10 and later even though the scaffold finished, and every pnpm command in the app fails afterwards: the scaffold has no decision on dependency install scripts. This script denies every undecided one in pnpm-workspace.yaml and proves it by installing again:node scripts/allow-builds.mjs --app-dir <app-dir>
pnpm install. It is safe to rerun, never changes an existing allowBuilds decision, and exits 0 only when pnpm install succeeds. If the user wants a package's install script to run, they set it to true in pnpm-workspace.yaml themselves. Do not approve builds for them.<app-dir>:git init -b main
echo '.mc-onboarding/' >> .gitignore
The second line keeps the onboarding session log out of the repository.
pnpm build in the app directory to confirm it compiles. Do not run pnpm start as a checkpoint: locally the app appears in the nav without registration, so it proves nothing about the deployed result.Step 4: choose install targets, then register (first pass, no URL yet)
Choose where the app gets installed. This is a terminal picker, not a model task, and it needs the Merchant Center session from Step 2:
node scripts/install-plan.mjs --region <region> --out <app-dir>/install-plan.json
--list prints the Organizations and projects as JSON so the plan file can be written by hand; --vim gives modal keys. node scripts/install-plan.mjs --help and node scripts/register.mjs --help print every option and key; read those instead of guessing flags. Do not choose Organizations or projects for the user.node scripts/register.mjs --app-dir <app-dir> --install-plan <app-dir>/install-plan.json --contact-email <email>
This is the first command that changes the user's account (it sets the Organization's contact and creates the app in Draft), so show it with the plan it will apply and get the user's explicit confirmation first; the session's review screen is that confirmation on the session path. Do the same before every run of Step 6, which also changes the account (URL, Ready, installs), and say so when the plan would remove an installed project.
--project-key <key> is a shorthand: it adds that project in the Organization that owns the app and never removes or narrows an existing install. --install-plan matches exactly, so an allowlist that omits an installed project removes it. Do not combine the two.steps and stoppedAt. If the Organization has no verified contact yet, it sets the email and stops with exit code 3 at VERIFY_EMAIL: the user clicks the link in their inbox, then the same command runs again. With a verified contact it skips straight on. Either way it ends by creating the app in Draft through mc-scripts config:sync:ci, writing the returned ID into env.production.applicationId, and stopping at DEPLOY (exit code 3).--org-id. Never pick one for the user.applicationId must be in the config before the production build, because the build embeds it. That is why registration comes before deploy. The registered URL is a placeholder until Step 6.Step 5: deploy
https URL known before the build, a rewrite of every path to index.html (status 200), and public/ as the output. Read hosting/other.md for the procedure and the contract, then the user's provider file if one exists (vercel.md, netlify.md, cloudflare.md, connect.md, surge.md). For any other host, apply the contract with that host's tooling and the commercetools Deployment overview.scripts/patch-config.mjs, then build and deploy. The URL feeds the compiled Content-Security-Policy; deploying before it is set ships the placeholder host in the headers and forces a second deploy. Show the user the deploy command and get their explicit confirmation before running it: it publishes the app.scripts/verify.mjs before touching registration. It fetches the app path and a deeper path and fails with a specific message when the host lacks the rewrite or answers with a redirect. That is the most common hosting failure, and it looks the same on every provider.Step 6: finish registration
node scripts/register.mjs --app-dir <app-dir> --install-plan <app-dir>/install-plan.json --production-url <url>
INSTALL_MISMATCH (exit code 3) and the user decides. Otherwise expect exit code 0 and done: true. The config file now holds the registered URL, so tell the user it changed and that it must be committed and pushed for the next deploy to match what is registered. Leave the commit and push to them: Step 3 made the repository without a remote. If they ask you to do it, show the commit and the push and get their explicit yes first.Step 7: verify in the deployed project
node scripts/verify.mjs --production-url <url> --project-key <key> --entry-point <path> --region <region>
hostCheck.via says which answered. It prints openInBrowser, the Merchant Center URL of the app inside the Project. The workflow ends only when that page has been opened.- With a browser automation tool, navigate to
openInBrowser. If the page lands on the login screen, ask the user to sign in in that window, then continue. Take a snapshot and confirm two things: the menu item (the menu label) is in the main nav, and the app content renders. Report what the snapshot showed. - If browser automation is not available, give the user the
openInBrowserURL, ask them to open it and confirm the menu item, and say plainly that you did not see the nav.
done: true or a passing host check alone.