Merchant Center Custom Application, zero to nav

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

Recommended: install the full commercetools plugin. It includes this Skill, every other commercetools Skill, our pre-tuned Subagents, and the commercetools Knowledge MCP โ€” which gives AI live access to the commercetools docs, GraphQL/OpenAPI schemas, and query validation. You only install once; every Skill on this site becomes available in every session.
Install the plugin

In any Claude Code session, run the following commands one at a time:

  1. Add the commercetools AI marketplace:

    /plugin marketplace add commercetools/commercetools-ai-plugins
    
  2. Install the commercetools AI plugin (Skills + Subagents + MCP):

    /plugin install commercetools@commercetools
    
Reload plugins

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
Claude Desktop
Customize -> Personal plugins -> Create plugin -> Add marketplace -> Add commercetools/commercetools-ai-plugins. Then, click on the plugin and click Install.

Instructions Included

SKILL.md

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.

Assumed: the user has a commercetools Project and is in the 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

The procedure lives in 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.
Every script prints JSON. Read ok, error, stoppedAt, and checks[].detail; never assume a step worked. Run them as node <skill-dir>/scripts/<name>.mjs.
The scripts carry the procedure; the Merchant Center facts around it (what an entryPointUriPath may be, which permissions a Team needs, what a host must do) come from the documentation, so:
  1. 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 5
    
    Pass the commercetools project key and region (as in api.{region}.commercetools.com) only if already in your context; otherwise omit both. Never search files or ask the user for them.
  2. 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 is the only route. You run 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.

  1. 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 with intake.mjs check --answers <file>.
  2. Run scripts/headless.mjs with 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 a needs object 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>.
  3. 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 the create-mc-app launcher, 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 with needs.ctx orgId: ask which one owns the app, rerun with --org-id <id>. Nothing in the account changes before the yes.
  4. 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 main in 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 with needs (ctx, label, options or env): ask, then rerun with --set <ctx>=<value>, --production-url <url>, or the variable named in needs.env (tokens and secrets go in the environment only). The provider files under references/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|off sets 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-help and --org-id, which are not stored.
  • Every command's output goes to <app-dir>/.mc-onboarding/session.log; --help lists every option.
Do not answer the form or choose Organizations, the owner, or the deployment toggle for the user, and do not answer a needs for the user.

When to use the manual steps

The same work exists as seven separate commands in manual-steps.md: inputs, preflight, scaffold, install targets and registration, deploy, finish, verify. Read it when one piece must be redone by hand, or when you cannot run commands at all and the user runs each one. Otherwise run Step 0. Hosting is a contract, not a provider: hosting/other.md states it, and the provider files say how Vercel, Netlify, Cloudflare Pages, Surge and Connect meet it.

Failure reading

SymptomCauseFix
App page reloads foreverentryPointUriPath 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 APIUser is not in Administrators for the OrganizationAdd them, or use an Organization where they are.
Blank page after installHost does not rewrite all paths to index.htmlRun 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 navLocal dev bypasses registrationCheck Ready state and install with register.mjs, then Step 7 of the manual steps.
Installed but not in the navNon-admins need the app's View permission on their TeamGrant it on the Team. Admins see it at once.
register.mjs stops at INSTALL_MISMATCHThe 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 changeInspect the state of the step named in the error. Do not loop.

Checklist

  • The user has a Project and is in the Administrators Team 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 productionUrl and opened), or manual Steps 1 to 6 ended with done: true from register.mjs
  • The app folder is its own git repository (git init -b main ran there; the folder has a .git)
  • scripts/verify.mjs passed 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

headless.md

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

  1. 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.
  2. 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 with needs.ctx set to installPlan and needs.options, the Organizations the user administers with their projects.
  3. 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 when allProjects is true, at least one key otherwise).
  4. Get the review: the same command plus --install-plan <file>. Exit 0 with stage: "review". Show review to 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-id was given, it exits 4 with needs.ctx set to orgId and the options: ask, rerun with --org-id <id>. Nothing in the account has changed yet.
  5. Get an explicit yes to that review. Then the same command plus --confirmed. It creates the app, runs git init -b main in the app folder (the repository root with Connect; init only, no commit or remote), builds, registers, deploys, checks the host, finishes registration, and ends with productionUrl and opened.
  6. 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.
  7. Finish as in Step 7 of the manual steps: opened is 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.
Do not pass --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

CodeMeaningWhat to do
0Done, or the review was printed (no --confirmed).Show the review, or finish with opened.
1A step failed: failedAt and error (or error alone for an unexpected failure).Read the error and the log, fix the cause, rerun.
2Bad or missing input: errors keyed by field.Ask for the field, rerun.
3A 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).
4A 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.ctxWhenAnswer with
installPlanNo plan file yet.Write the file, add --install-plan <file>.
orgIdThe plan names several Organizations.--org-id <id> from options.
productionUrlDeployment help off: the user deployed.--production-url <https url>.
vercelTeamThe Vercel login has several teams.--set vercelTeam=<id> from options.
vercelTokenThe Vercel CLI is not signed in.VERCEL_TOKEN
netlifyTeamThe Netlify account has several teams.--set netlifyTeam=<slug> from options.
netlifyTokenThe Netlify CLI is not signed in.NETLIFY_AUTH_TOKEN
cfAccountThe Cloudflare login has several accounts.--set cfAccount=<id> from options.
cfProjectCloudflare refused the Pages project name.--set cfProject=<name>
cfTokenWrangler is not signed in.CLOUDFLARE_API_TOKEN
ctClientIdConnect: the API client for the Project.CTP_CLIENT_ID (or --set ctClientId=<id>)
ctClientSecretConnect: its secret.CTP_CLIENT_SECRET
ghVisibilityConnect: the GitHub repository to create.--set ghVisibility=public or private.
A secret travels in the environment of the command, never in a flag: the script refuses --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.
hosting/cloudflare.md

Cloudflare Pages

Source: Deploy to Cloudflare Pages, which uses the Git integration and dashboard settings; this file uses Wrangler (4.x, flags from its --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 hashed https://<hash>.<project>.pages.dev URL, which is never used.
  • Build: pnpm build, output in public.
  • Rewrite: none to write. Without a 404.html in 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 before pnpm build. The Pages project's environment variables only apply to builds Cloudflare runs from Git.

What the session runs with deployment help on

The session's review screen is the consent for everything below; on the manual path, show each command and get the user's explicit yes before running it, as in step 4 of other.md.
  1. wrangler whoami --json. Not signed in: the session asks for an API token with Pages edit rights, carried as CLOUDFLARE_API_TOKEN in the environment, or the user runs wrangler login in another terminal and presses Enter. With several accounts the user picks one; its id travels as CLOUDFLARE_ACCOUNT_ID, because Wrangler refuses to choose without a terminal.
  2. wrangler pages project list --json. The project named after the app folder is reused, with its .pages.dev domain as the production URL. Otherwise wrangler 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.
  3. Patches the config with the project URL, builds, deploys. One deploy is enough: the domain is known before the build.
  4. 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.
hosting/connect.md

commercetools Connect

Source: Deploy to Connect and the Connect API. Connect is not a static host: it builds the app itself from a Git tag of a GitHub repository and serves it, so the three requirements in other.md are met by Connect rather than by a rewrite file and an upload. Choose it when the user does not want to run a host at all. The session's review screen is the consent for everything below (a GitHub repository, a Connector, a Deployment); on the manual path, show each command and get the user's confirmation before it, as in step 4 of other.md.

What is different from a static host

  • Layout. The folder named in the form is the repository root. It holds connect.yaml and the app one level down, in a folder named like the deployAs entry (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 npm and built locally with npm run build. The pnpm build-script step does not apply.
  • URL. Connect provides APPLICATION_URL at build time, so the config holds url: '${env:APPLICATION_URL}' instead of a literal. The local build sets a placeholder for it; register.mjs sets it to the real URL when it syncs. The literal never needs to be committed.
  • Identity. connect.yaml declares CUSTOM_APPLICATION_ID, ENTRY_POINT_URI_PATH and CLOUD_IDENTIFIER; the session passes the registered applicationId, 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-mu machine user; the session grants it as a collaborator through gh api. If that invitation is still pending when Connect fetches, the publish report fails at its "Fetch Connector repository" step: accept the invitation for connect-mu (or make the repository public) and press r.
  • Deployment type. The session creates a sandbox Deployment: 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

  1. API client. Connect needs an API client for the Project with the scopes manage_connectors and manage_connectors_deployments. The session asks for its id, then its secret (kept in memory, never logged). A rejected secret is asked for again.
  2. Repository and tag. git init -b main if needed, git add -A, a commit, gh auth status (the GitHub CLI must be signed in), and if there is no origin, the user picks public or private and the session runs gh repo create <name> --<visibility> --source <root> --remote origin --push. Then a new tag v0.1.<n> is made and pushed. Every run makes a new tag, because Connect builds a tag, not a branch.
  3. Connector. A ConnectorStaged keyed after the app folder is created with the repository and tag, the region, and the Project in privateProjects, or repointed at the new tag with setRepository. It is published for private use (certification: false). The step polls every 15 seconds until the status is Published; Failed stops with the publishing report.
  4. Deployment. A sandbox Deployment keyed <name>-sandbox is created with the configuration above, or redeployed with updateConnector after a republish. The step polls until Deployed (up to 15 minutes) and takes applications[0].url as the production URL; Failed stops with the report.
  5. The host check and the finish step then run as for any host.

When a publish fails on a fresh starter

Connect's validation pipeline scans dependencies and source. A fresh starter template can fail on its own dependencies, which is not a session bug. Read the report in the error (or in the Merchant Center under Connect, the Connector's publish report), update the flagged package or code, and press r: the session tags, pushes and publishes again.
hosting/netlify.md

Netlify

Source: Deploy to Netlify, which uses the Git integration and dashboard settings; this file uses the CLI (netlify-cli 27, flags from its --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's build script is mc-scripts build), publish directory public.
  • Rewrite: netlify.toml in the project root:
    [build]
      command = "pnpm build"
      publish = "public"
    
    [[redirects]]
      from = "/*"
      to = "/index.html"
      status = 200
    
    The status = 200 is what makes it a rewrite and not a redirect.
  • Deploy: netlify deploy --prod --no-build --dir public --site <name> --json after building. The url key of the JSON is the production URL; deploy_url is 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 before pnpm build. The site's build environment settings only apply to builds Netlify runs itself.

What the session runs with deployment help on

The session's review screen is the consent for everything below; on the manual path, show each command and get the user's explicit yes before running it, as in step 4 of other.md.
  1. netlify status --json. Not signed in: the session asks for a personal access token, carried as NETLIFY_AUTH_TOKEN in the environment (never on the command line), or the user runs netlify login in another terminal and presses Enter.
  2. Linked site (linked: true): its site-name and site-url are used. Otherwise netlify 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), then netlify sites:create --name <app folder> --account-slug <team> --json.
  3. 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.
  4. 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/other.md

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.

  1. A stable https URL, known before the build. env.production.url feeds the compiled Content-Security-Policy, so the URL must be in the config before mc-scripts build runs. 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.
  2. A rewrite of every path to index.html with 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.
  3. The public/ directory as the published output, produced by mc-scripts build (pnpm build in the starter). mc-scripts build also compiles index.html.

Procedure

  1. Get the production URL (requirement 1).
  2. node <skill-dir>/scripts/patch-config.mjs --app-dir <dir> --url <url>
  3. Add the provider's rewrite rule (requirement 2) to the repo, so it is code and not a dashboard setting.
  4. 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.
  5. node <skill-dir>/scripts/verify.mjs with the flags in Step 7 of the manual steps, and read hostCheck.problems. It tests the app path and a deeper path, so it catches a missing rewrite on any host.
  6. 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.
hosting/surge.md

Surge

Source: Deploy to Surge. Follow 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 of other.md needs no create or reserve step.
  • Build: as in other.md, output in public.
  • Rewrite: Surge serves 200.html for unknown paths. Copy the built file rather than renaming it as the guide does, so index.html still serves the root: cp public/index.html public/200.html. Re-copy after every build.
  • Deploy: surge public https://<project>.surge.sh
hosting/vercel.md

Deploy to Vercel

Run these in the app directory, after 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

Create 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).
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 login in 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, or vercel project inspect). When the name is taken in another team, Vercel appends a suffix (<name>-pi.vercel.app), so the Aliased line of the deploy output is the source of truth. Never use the https://<project>-<hash>-<team>.vercel.app deployment 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
The URL goes in before the build because 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

Run 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

Pass the URL as --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

The session's review screen is the consent for everything below; on the manual path, show each command and get the user's explicit yes before running it, as in step 4 of other.md.
The same commands, in this order: 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.md

Manual steps

These are what the headless run does, as separate commands. Run them in order from the skill directory; <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>]
Pass --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>
Fix every key in 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.
InputNotes
Project keyA Project in the owner Organization, used as --initial-project-key when scaffolding. Install targets are chosen in Step 4.
RegionCloud identifier: gcp-eu, gcp-us, gcp-au, aws-eu, aws-us. Must match the Project.
entryPointUriPathUnique per region, with reserved names rejected. It also names the user permissions, so it is hard to change later.
Menu labelWhat the nav shows. Defaults to the app name.
Maintainer contact emailRequired once per Organization. Gets a verification email.
App directoryWhere to scaffold.
Hosting providerWhatever 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>
Fix every 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
Use --template starter for plain JavaScript. Then set mainMenuLink.defaultLabel in custom-application-config.mjs to the menu label.
The app is a static bundle: the Merchant Center loads it from the host URL inside the nav, so nothing runs locally and there is no local checkpoint. The starter already fetches data through the Merchant Center API gateway (/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>
Show it and get the user's yes before running it, right after scaffolding: it runs 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.
Make the app folder its own git repository (the repository root with Connect), even when it sits inside another repository. Only initialise: no commit, no remote. From <app-dir>:
git init -b main
echo '.mc-onboarding/' >> .gitignore

The second line keeps the onboarding session log out of the repository.

Then run 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
The user runs it in their own terminal, because it takes keystrokes. It is a tree of Organizations with their projects. Ticking an Organization alone means all projects (the default); ticking projects under it makes an allowlist, which installs only those. An empty allowlist is refused. Without a terminal, --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.
Check 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).
If it reports several Organizations, pass --org-id. Never pick one for the user.
The 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

Hosting is a contract, not a provider: a stable 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.
Order matters: learn the production URL, write it into the config with 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.
Then run 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>
It writes the URL into the config, syncs it, moves the app to Ready, and installs it in every plan target. A target whose allowlist differs from what is installed is updated to match exactly. The script does not switch a target between all projects and an allowlist: it stops at 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>
The same host check as in Step 5, now against the final state. Where Node cannot verify the host's certificate (corporate TLS inspection), the check falls back to curl, which trusts the system keychain; 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 openInBrowser URL, ask them to open it and confirm the menu item, and say plainly that you did not see the nav.
Do not report success from done: true or a passing host check alone.