Layers CLI signs you in and binds this repository to the Layers Growth MCP, so your coding agent can call Layers growth, content, paid-media and managed distribution tools by name. One command does the whole setup, and no credential is written into the repository.
Set up Layers for this product: onboard it, install Layers measurement, then connect Meta and launch ads.
1. Install the CLI: run `npm install -g @layers/cli`.
2. Check that the README opens with a plain sentence saying what this product is, and write one if it does not. Setup sends that single line as the product brief, and it stops with exit code 1 when it cannot find one.
3. Set it up: run `layers setup` from this repository. It prints one line, "Send this to the person: <link> (code <code>)". Send me that link and code; I tap the link, check the code matches, and tap Allow. It also opens a browser on this machine when there is one, and either way finishes the sign-in. The link works for 15 minutes, and running `layers setup` again after a restart resumes the same link. It writes the `.mcp.json` entry and the Layers skill into the repo and stores the session in the OS credential store, so nothing secret lands in the repo. On a machine with no OS credential store, such as a Linux cloud computer, run `layers setup --allow-file-credentials` instead, which keeps the session in a private 0600 file. If it asks for the product's public URL, which is its website, its App Store page, or its Google Play page, pass that question to me. Setup also onboards the product: Layers saves the README line and the public URL as this product's brief.
4. Check the organization: when setup creates the project, its summary names the organization the project is in and says how to put the project in another one. Send me that organization line. If I name another organization, follow those instructions.
5. Read the skill at `.agents/skills/layers/SKILL.md`. If this session cannot load the Layers MCP server, as in a headless run, call a tool with `layers call <tool> '<json>'` and read a resource with `layers call layers://<resource>`; both reach the same tools and resources.
6. Install Layers measurement: call install_growth_measurement with action spec. For an Expo, React Native or Flutter app, send platforms with that framework, such as ["expo"]; the response lists the platforms the spec covers. When the product has a website to promote, send webDomain set to its host name, such as example.com. It returns the SDK dependency, the app id and the init snippet for this app. Add them to the app, run the app once on a simulator, a device or in a browser, then call install_growth_measurement with action verify until it answers reporting.
7. Set the daily cap before any launch: ask me "What is the most you want to spend on ads per day, in US dollars?" and set my answer with set_spend_cap. The cap is a 7-day average, so one day over it is fine.
8. Connect Meta: read layers://workspace. The meta-ads row of paidReadiness.providers carries a signed connect link. Send me that link; I open it and choose the ad account, the Facebook Page the ads run as and, for an app with a store listing, its Meta app. Every Meta ad runs as a Facebook Page. If I have none, the page links to where I create one; when I tell you the Page is ready, send me a new connect link. Wait until that row reads configured.
9. Make the ads: write the primary text and headline and make the images for each market, in that market's language and sized for Meta placements. Save each image inside this repository: launch_ads takes it as media with source local_file and the file's path, and the layers CLI uploads it. An image already at an https address goes as source image_url with that url. Show me the copy and the images before you launch.
10. Launch: call launch_ads with action launch, a dailyBudget within the cap in the ad account's currency, currency, markets and the creatives. An app with a live App Store or Google Play listing gets an App Promotion campaign for installs. A website product, such as a web app, a SaaS, a store or an info product, gets a Sales campaign whose ads link to the website and optimize Purchase on the site's Meta Pixel. An app with no store listing yet launches only through its website, or waits for the listing. launch_ads returns each blocker with its fix; do the fix and call it again. While Meta reviews the ads, the launch answers status running; call launch_ads with action status and its jobId until it finishes.
Let me know when it is ready.
Give those instructions to Claude Code, Codex, Cursor, or any coding agent working in your repository: the agent runs the commands and the tool calls. A person taps the sign-in link, answers the daily ad cap question, and opens the Meta connect link.
This is what the agent sees when a setup run that creates the project finishes (CLI 2.1.21). The two lines under Organization appear on that run only:
$ cd your-app
$ layers setup
✓ Layers Growth MCP is configured and ready for this repository.
Organization Acme (org_…)
Setup put this repository's project here because it is the only organization this account belongs to.
For another organization: Delete /…/your-app/.layers/project.json, then run `layers setup --new --org <org-id>`. `layers org list` shows the organization ids, and `layers org use <id-or-name>` makes one this machine's default.
Project prj_…
Endpoint https://mcp.layers.com/mcp
Repository files
.layers/project.json
.mcp.json
.agents/skills/layers/SKILL.md
.claude/skills/layers -> ../../.agents/skills/layers
These files are non-secret. The MCP client spawns `layers mcp`; the session stays in the credential store.
Layers CLI on_path
Codex registered
Claude Code project_config_pending_approval
Readiness readyFrom there the agent reads the pinned skill pointer, loads the server-owned skills for the job at hand, and calls the tools.
Any platform with Node:
npm install -g @layers/climacOS:
brew install layers/tap/layersLinux / macOS:
curl -fsSL https://layers.com/install.sh | shThe installer puts the binary in $HOME/.local/bin when that directory is on
your PATH, and in /usr/local/bin otherwise. Pass --prefix (or set
LAYERS_INSTALL_DIR) to choose the directory yourself, which is what a
container without sudo needs:
curl -fsSL https://layers.com/install.sh | sh -s -- --prefix "$HOME/.local/bin"sudo runs only when the target directory needs it.
Windows:
scoop bucket add layers https://github.com/layers/scoop-bucket
scoop install layersDirect download: GitHub Releases
For people who prefer a shell:
# 1. Sign in and wire up this repository (prints a link and opens a browser)
cd your-app
layers setup
# 2. Call a tool right now, in this same shell: the Layers SDK install spec
layers call install_growth_measurement '{"action":"spec"}'
# 3. Restart your coding agent, or run /mcp, so it loads the tools nativelyStep 2 and step 3 are the same tools over the same sign-in. MCP clients read
.mcp.json at startup, so layers call is how you work in the session where
setup just ran.
| File | What it is |
|---|---|
.layers/project.json |
the public organization and project this repository is bound to |
.mcp.json |
the layers MCP server, merged alongside servers already there |
.agents/skills/layers/SKILL.md |
a thin pointer to the server-owned Layers skills |
.claude/skills/layers |
a relative symlink to that pointer, where symlinks are supported |
The .mcp.json entry is the local command and nothing else:
{
"mcpServers": {
"layers": { "command": "layers", "args": ["mcp"] }
}
}Setup is idempotent. Re-running it repairs Layers-owned entries, leaves every
other byte of .mcp.json alone, and never creates a second project. It also
removes the retired growth server entry when that entry is exactly the one an
older layers setup wrote, and refuses to touch one that was modified.
.layers/project.json holds public selectors, not authority. The MCP edge
revalidates the session and its membership on every call.
layers mcp forwards to https://mcp.layers.com/mcp. For local development,
set LAYERS_MCP_HOST (or pass --host to layers setup) to another
deployment:
LAYERS_MCP_HOST=https://localhost:8787 layers setup--host records the choice in the machine's global Layers config, so the
layers mcp the client spawns reaches the same place without an environment
variable. The endpoint is machine-owned in both cases: nothing in a repository
can select one, because a repository is shared and a credential destination is
not. Only the production host, an https://*.layers.com host, loopback, and the
host named in LAYERS_MCP_HOST are accepted.
layers login stores your session in the operating system keyring: Keychain on
macOS, Credential Manager on Windows, gnome-keyring or kwallet on Linux.
The credential subsystem also maintains its mode-0600 machine record under the
global Layers config directory for refresh durability. The Layers Growth MCP will not
accept that record without the matching OS-keyring session unless the operator
explicitly records --allow-file-credentials in the 0600 machine config.
layers mcp and layers call are the credential's only readers. An MCP client
spawns the proxy, which forwards JSON-RPC messages with that session. The
repository holds the command name and the public binding only, so grep -r of
the project finds no credential.
There is no API key in MCP setup, and no command that mints one. Partner API keys remain available for REST integrations and are issued from the Layers app.
Prices are the server's, and the catalog carries them. Every priced tool accepts a cost check that returns the exact price of those arguments without running the call, and every result carries what it cost and what is left. This binary holds no price, plan, or trial fact.
A workspace layers setup just created has no subscription, so charged calls
are refused until someone chooses a plan. Setup reads layers://account after
it binds, and when that workspace is not spend-entitled it prints the plan page
alongside the connect-account step:
Next steps for your coding agent
Choose a plan: https://app.layers.com/?settings=plan
The line names the page and nothing else. What a plan costs, what it includes,
and how it starts are on that page and in layers://account.
layers setup --json carries the same step in human_actions:
{
"human_actions": [
{
"kind": "choose_plan",
"url": "https://app.layers.com/?settings=plan",
"message": "Choose a plan: https://app.layers.com/?settings=plan"
}
]
}The key is always present; it is an empty array once the workspace can spend.
layers call prints the tool's structured envelope verbatim on stdout, which is
what makes it safe to pipe into an agent. On a tool error it exits 1 and writes
the code and message to stderr, while the full error envelope still goes to
stdout so recovery_tool and nextSteps stay readable.
Every other command takes --json too, and answers with one JSON object on
stdout, so an agent reads a result instead of parsing a table:
$ layers setup --json
{
"root": "/Users/you/your-app",
"status": "ready",
"endpoint": "https://mcp.layers.com/mcp",
"organization_id": "org_…",
"project_id": "prj_…",
"written": ["…/.layers/project.json", "…/.mcp.json", "…/.agents/skills/layers/SKILL.md"],
"readiness": { "status": "ready" }
}layers setup --new has a second result that also exits zero. Onboarding can
stop on one fact no source can provide — most often the product's public URL —
and that is a question rather than a failure. Branch on status; a ready
object is the one above, and this one is the stop:
$ layers setup --new --org org_… --json
{
"root": "/Users/you/your-app",
"status": "needs_answer",
"endpoint": "https://mcp.layers.com/mcp",
"rerun": "layers setup --new --org org_… --url <their URL>",
"rerun_no_public_url": "layers setup --new --org org_… --no-public-url",
"workspace": {
"state": "needs_input",
"question": {
"question_id": "public_url.…",
"ask": "Is one of these the public URL Layers can use to onboard Widget?",
"options": [{ "option_id": "url_1", "label": "https://…" }]
}
}
}Nothing was written: no binding, no .mcp.json entry, no skill. Put
workspace.question.ask and its option labels to the person you are working
for, then run one of the two lines. rerun carries the literal <their URL>
placeholder, so replace it with the address they give before running it;
rerun_no_public_url is a command as it stands.
Without the flag the output is formatted text. With it, progress lines move to stderr so stdout carries the object alone, and a failure prints one object on stderr with a non-zero exit status:
{
"error": {
"code": "login_required",
"message": "Not logged in to Layers. Run `layers login`"
}
}Before waiting for sign-in, both login --json and setup --json print the
relay line and a human_step event on stderr:
Send this to the person: https://growth.layers.com/link/BCDF-GHJK (code BCDF-GHJK)
{"event":"human_step","url":"https://growth.layers.com/link/BCDF-GHJK","code":"BCDF-GHJK"}
The event contains the public URL and code. The pending sign-in proof stays in
pending-login.json under the Layers config directory with mode 0600. A
restarted command resumes that link until its expiry; a completed or refused
sign-in removes the file. LAYERS_AGENT_LABEL sets the requesting agent's
display label, with Layers CLI as the default. Link creation, polling and
session refresh use https://api.layers.com, or LAYERS_API_URL when set.
layers mcp is a stdio proxy that prints no result of its own, so the flag
does nothing there.
Setup writes the four non-secret files above. The .mcp.json entry contains no
endpoint or credential policy; those remain machine-owned. Credentials use a
writable key derived from environment, issuer, OAuth client, user, binary, MCP
origin, and OAuth origin, so production, staging, local development, and the
internal cora binary never share one. The token identity and the repository
binding are rechecked before each forwarded request. Setup does not upload or
scan source code.
Claude Code discovers the repository .mcp.json entry but keeps a new project
server pending until the user approves it. Setup reports that state explicitly;
start claude in the repository and approve the exact layers mcp entry once.
Layers never edits Claude Code's private trust state.
| Command | What it does | --json object |
|---|---|---|
layers setup |
Sign in and wire this repository to the Layers Growth MCP | bindings, files, clients, and readiness |
layers call <tool> [json] |
Call a repository-bound Layers tool from the shell | the tool's structured envelope |
layers mcp |
Serve the Layers Growth MCP to a client over credential-isolated stdio | nothing; a stdio proxy prints no result |
layers setup flags: --org and --project to select the binding, --new to
create the first workspace for an account that has no project, --host (or
LAYERS_MCP_HOST) to point at another deployment, --credential-dir to choose
a machine-owned credential directory, --allow-file-credentials to consent to
the 0600 file fallback, and --require-keyring to withdraw that consent. On
Windows the credential store is pinned to the current user's profile-backed
Layers directory; custom roots and XDG_CONFIG_HOME are rejected until ancestor
ACL validation is certified.
Ids are the public org_… and prj_… selectors everywhere they are printed,
including layers org list --json. Both that form and the bare uuid are
accepted wherever an id is an input.
An account with no project has nothing to select, so layers setup creates one.
It reads the product name and one-line description this repository can answer
for itself, from a package manifest, the README title, and the README's first
paragraph, and shows them. No file, path, or source line leaves the machine.
It then sends that brief and says so:
Sending this brief to Layers.
Layers confirms the workspace with the account owner in the browser.
There is no terminal question, so the run behaves the same in a shell with no
tty, which is where a coding agent runs. A repository whose README describes
nothing gets a refusal naming the repair instead of an invented description.
--accept-brief is still accepted and does nothing.
| Command | What it does | --json object |
|---|---|---|
layers login |
Sign in by link or local browser | the account signed in |
layers login --headless |
Sign in by link without a browser or localhost listener | the account signed in |
layers logout |
Sign out of this repository's Layers Growth MCP session | which stores were cleared, and what remains |
layers whoami |
Show the current user, resolving the session first | the session state and the account |
layers auth status |
Session health, expiry, and the recent auth trail | the same report --json always printed |
layers auth log |
Timestamped refresh / adopt / quarantine / revoke history | the events and the log path |
layers auth restore |
Reverse a credential quarantine, no browser needed | what was restored, and whether it expired |
layers logout clears the credential this repository's Layers Growth MCP uses and
names it. When the same account is still signed in to the shared layers login
session on this machine, the report says so; layers logout --all clears that
one too. A malformed or foreign .mcp.json never blocks a logout.
Every session command takes --credential-dir, layers logout included. That
directory is the store: deleting it ends the session for every command, and
nothing falls back to another one. --all clears the plain store of the
directory it was given, so a scoped run's follow-up carries the flag too:
layers logout --all --credential-dir <dir>.
| Command | What it does | --json object |
|---|---|---|
layers update |
Update the CLI to the latest version | the versions, and whether an update was installed |
layers version |
Show version info | version, build date, commit, Go version, platform |
Once a day, after a command has finished, the CLI checks the latest published release and prints one line to stderr when your binary is behind:
layers 2.0.0 is behind 2.0.2. Run `layers update`.
The API can also report a minimum version in the X-Layers-CLI-Floor response
header. The CLI prints the same line for that, on any command, without waiting
for the daily window.
The line is silent when your binary is current, when the check fails, when
stdout is not a terminal, when the command answers in JSON, and when
LAYERS_NO_UPDATE_CHECK is set:
export LAYERS_NO_UPDATE_CHECK=1The check never blocks a command. It runs after the output, with a two-second
timeout, and the last attempt is stamped at
~/.config/layers/update-check.json (or $XDG_CONFIG_HOME/layers/).
Can't sign in? Open the printed sign-in link on any device and choose Allow. The CLI also opens a local browser and listens on ports 54321-54323; whichever sign-in finishes first completes the login. If those ports are unavailable, the link still works.
"the sign-in link expired"? The CLI waits until the link's server-issued
expiry, including for a local browser sign-in. Run layers login again for a
fresh link. Restarting before expiry keeps the same link.
Signing in over SSH or on a headless machine? Run layers login --headless,
send the printed link to the person you work for, and wait for them to choose
Allow. Nothing needs to be entered in the terminal.
BOOTSTRAP_NOT_AVAILABLE? The Layers edge is not offering workspace
creation at that moment. Nothing local repairs it: try again later, or send
support@layers.com the request id the message carries.
Agent can't see the Layers tools? Restart it, or run /mcp. MCP servers are
read at startup, so a .mcp.json written mid-session is not picked up until
then. layers call <tool> works immediately in the meantime.
layers is not on your PATH? An MCP client cannot spawn layers mcp until
it is. Install with npm install -g @layers/cli, or put the binary's directory
on PATH.
mcpServers.growth names the retired growth MCP? This repository was wired
to the retired growth service. Run layers setup; it removes that entry when it
is exactly the one an older layers setup wrote, and tells you what to fix when
somebody edited it.
WORKSPACE_NOT_BOUND? You are not in a repository layers setup ran in.
The binding lands at the repository root, so run layers setup from there.
Session problems? layers auth status reports the state, the expiry, and
what repairs it, and its Store: line names the credential directory it read
(store.kind and store.directory under --json; whoami carries the same
object). auth status, auth log and auth restore read the store whoami,
org and call read, so the two reports can be compared by path. layers auth restore reverses a quarantine without a browser. layers login repairs the
session every command reads, this repository's MCP session included, so
whoami, org and call agree after it.
layers mcp says the endpoint it was set up for is unreachable? Setup
records the endpoint it resolved in the machine's config, and .mcp.json never
names one, so a repository cannot decide where your session goes. If that record
is missing or stale, rerun layers setup (with --host if you use a
non-default deployment) in the repository to write it again.
Keychain errors on Linux? Install gnome-keyring or kwallet. Setup blocks
by default; --allow-file-credentials explicitly permits the mode-0600 machine
record under ~/.config/layers/. It is not encrypted and is never stored in the
repository.
- Layers: the main site
- Docs: install, catalog, and error recovery
- Dashboard: the web dashboard
- Changelog: release notes
MIT