Skip to content

Authentication

For most users, the one-command setup is the easiest way to get started:

Terminal window
# Claude Code users (authentication + MCP + commands + skills + credential guard)
mureo setup claude-code
# Cursor users (authentication + MCP only)
mureo setup cursor
# OpenAI Codex CLI users (MCP + credential guard + workflow skills + shared skills)
mureo setup codex
# Gemini CLI users (extension manifest + MCP)
mureo setup gemini
# CLI-only users (authentication only, terminal prompts)
mureo auth setup
# Browser configuration UI — localhost, no terminal input needed
# (supersedes the removed `mureo auth setup --web`)
mureo configure

--skip-auth and non-interactive invocation

Section titled “--skip-auth and non-interactive invocation”

Every mureo setup … subcommand accepts --skip-auth, which installs the MCP config, credential guard, and (where supported) command/skill files without running OAuth. Useful for the double-click installer flow where authentication is handled later via /onboard in Claude Code, $onboard in Codex, or mureo auth setup in a real terminal.

When mureo setup … is invoked from an AI agent’s subprocess (Claude Code’s Bash tool, Codex, etc.) that has no controlling TTY, --skip-auth is implied automatically so the command cannot hang on a typer.confirm prompt. A banner in stdout tells the operator to finish authentication in Terminal.app afterwards.

Each subcommand also exposes explicit --google-ads/--no-google-ads and --meta-ads/--no-meta-ads flags, so you can specify exactly which platforms to configure without any prompt. Passing them alongside --skip-auth (or under a non-TTY) emits a warning and is ignored.

mureo configure — browser configuration UI

Section titled “mureo configure — browser configuration UI”

mureo auth setup --web was removed; its browser credential flow is now part of the unified mureo configure UI.

Prefer mureo configure when you were pointed to mureo by an AI agent that cannot safely receive terminal input, or you simply want a GUI. It starts a short-lived HTTP server on a random localhost port, opens your browser at it, and — beyond Google Ads / Meta Ads / GA4 credential entry via HTML forms and standard OAuth redirects (every field deep-linked to the right console), plus an Amazon Ads card in the dashboard’s Plugin credentials section whose Authorize with Amazon flow obtains the LwA tokens (Amazon’s consent has no loopback callback, so it is a guided paste-code flow: mureo opens the consent page and you paste the redirected address back; see amazon-ads.md) — also lets you pick the Claude host, run basic setup (MCP server + credential-guard hook + skills), add the official MCP providers, switch each platform between mureo-native and the official MCP, and scaffold Demo/BYOD. Flags: --no-browser, --timeout-seconds N (idle shutdown, default 600). The same security hardening (CSRF rotation, OAuth state re-validation, DNS-rebinding guard, localhost-pinned redirect verification, generic error surface, POST size cap, CSP) applies — see SECURITY.md.

The auth step reuses what is already on disk: with a saved Google refresh token (Google Ads and Search Console share one) the Google authorization step is skipped and the UI says so, offering a Re-authorize Google button for when you do want to mint a new token.

When something fails in the UI, the on-screen message is deliberately generic (an error surface must not echo token material). The cause is in the configure log — ~/.mureo/logs/configure.log, printed on startup and written on every platform — including the ones that are otherwise invisible: a Meta token refresh that failed or could not be persisted, an account listing that was rejected, a credentials.json that would not parse. Raise the detail with MUREO_LOG_LEVEL=DEBUG mureo configure. No log line at any level carries a token, secret or credential value. See cli.md — Configure log.

mureo loads credentials from ~/.mureo/credentials.json, falling back to environment variables if the file is missing or incomplete.

Create ~/.mureo/credentials.json with the following structure:

{
"google_ads": {
"client_id": "YOUR_OAUTH_CLIENT_ID",
"client_secret": "YOUR_OAUTH_CLIENT_SECRET",
"refresh_token": "YOUR_REFRESH_TOKEN",
"login_customer_id": "1234567890",
"developer_token": "OPTIONAL_LEGACY_TOKEN"
},
"meta_ads": {
"access_token": "YOUR_ACCESS_TOKEN",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET"
}
}

developer_token is optional (legacy): Google stopped issuing developer tokens on 2026-09-09; sent if present, ignored by the API.

You can include only the platforms you use. For example, if you only use Google Ads, the meta_ads section can be omitted.

FieldRequiredDescription
developer_tokenNoLegacy. Google stopped issuing developer tokens on 2026-09-09; sent if present, ignored by the API.
client_idYesOAuth 2.0 client ID
client_secretYesOAuth 2.0 client secret
refresh_tokenYesOAuth 2.0 refresh token
login_customer_idNoManager account ID (MCC). If omitted, the target customer_id is used as fallback.
FieldRequiredDescription
access_tokenYesMeta Graph API access token (User or System User token)
app_idNoMeta App ID
app_secretNoMeta App Secret

If ~/.mureo/credentials.json is missing or lacks the required fields, mureo falls back to environment variables.

VariableRequiredDescription
GOOGLE_ADS_DEVELOPER_TOKENNoLegacy. Google stopped issuing developer tokens on 2026-09-09; sent if present, ignored by the API.
GOOGLE_ADS_CLIENT_IDYesOAuth 2.0 client ID
GOOGLE_ADS_CLIENT_SECRETYesOAuth 2.0 client secret
GOOGLE_ADS_REFRESH_TOKENYesOAuth 2.0 refresh token
GOOGLE_ADS_LOGIN_CUSTOMER_IDNoManager account (MCC) customer ID
VariableRequiredDescription
META_ADS_ACCESS_TOKENYesGraph API access token
META_ADS_APP_IDNoMeta App ID
META_ADS_APP_SECRETNoMeta App Secret
VariableRequiredDescription
AMAZON_ADS_CLIENT_IDYesLogin with Amazon (LwA) application client ID
AMAZON_ADS_REFRESH_TOKENConditionalLwA refresh token — with the client secret, mureo mints and refreshes access tokens for you
AMAZON_ADS_CLIENT_SECRETConditionalLwA application client secret
AMAZON_ADS_ACCESS_TOKENConditionalLwA access token (expires in ~60 min)
AMAZON_ADS_REGIONNona / eu / fe (default na)
AMAZON_ADS_ACCOUNT_MODENodynamic / fixed (default dynamic)
AMAZON_ADS_PROFILE_IDNoFixed account mode only
AMAZON_ADS_ACCOUNT_IDNoFixed account mode only
AMAZON_ADS_MANAGER_ACCOUNT_IDNoFixed account mode only

“Conditional” means the client ID plus either AMAZON_ADS_ACCESS_TOKEN or both of AMAZON_ADS_REFRESH_TOKEN and AMAZON_ADS_CLIENT_SECRET. See amazon-ads.md.

Resolution order: credentials.json takes priority. Environment variables are only checked if the corresponding section in credentials.json is missing or incomplete.

1. Google Ads API access (Google Cloud Console)

Section titled “1. Google Ads API access (Google Cloud Console)”
  1. Go to the Google Cloud Console and create a new project (or select an existing one).
  2. Enable the Google Ads API under APIs & Services > Library.
  3. On the Google Ads API Overview page, sign up for API access. Enabling the API grants Test access (test accounts only) right away; Basic access (production accounts) needs brand verification and an application on the same page, which Google may approve automatically. No Google Ads manager account is required.
  4. The access level belongs to this project — every OAuth client you create in it inherits the project’s access level.
  1. In the same project, navigate to APIs & Services > Credentials.
  2. Click Create Credentials > OAuth client ID.
  3. Select Desktop app as the application type.
  4. Copy the Client ID and Client Secret.

Use the google-auth-oauthlib library to obtain a refresh token:

from google_auth_oauthlib.flow import InstalledAppFlow
flow = InstalledAppFlow.from_client_config(
{
"installed": {
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
}
},
scopes=["https://www.googleapis.com/auth/adwords"],
)
flow.run_local_server(port=8080)
print("Refresh token:", flow.credentials.refresh_token)

Alternatively, use the Google OAuth Playground with the https://www.googleapis.com/auth/adwords scope.

Scope matters. The refresh token must carry the Google Ads scope https://www.googleapis.com/auth/adwords. Reusing a refresh token minted for a different scope makes Google Ads API calls fail at runtime with ACCESS_TOKEN_SCOPE_INSUFFICIENT. mureo configure / mureo auth setup request this scope (plus Search Console) automatically — prefer them over hand-minted tokens. Official reference: Google Ads API — OAuth 2.0 scopes.

Existing users (migrating from a developer token)

Section titled “Existing users (migrating from a developer token)”

Google migrated the access level of every Cloud project that made API calls with an approved developer token in the 90 days before 2026-09-09. To confirm your setup:

  1. Open the project’s Google Ads API > Overview page and check the access level shown there.
  2. Confirm the developers who work on the integration hold owner or editor IAM roles on the project.
  3. Leave the old developer_token in credentials.json or delete it — mureo no longer needs it.

mureo configure / mureo auth setup request the following scopes automatically during sign-in — you do not list them by hand. The full set is the source of truth in mureo/auth_setup.py (_META_OAUTH_SCOPES):

ScopeEnables
ads_managementCreate / edit campaigns, ad sets, ads, budgets, bids
ads_readRead ad data and insights
business_managementResolve ad accounts reached through a Business Portfolio (a permission warning may appear during sign-in — it is required and safe to accept)
pages_show_listList the Facebook Pages you can link
pages_manage_adsManage ads tied to a Page
pages_read_engagementRead a Page’s posts and photos (list posts for the Boost Post flow, meta_ads_page_posts_list; list existing Page photos to pick an Instant Form cover from, meta_ads_pages_photos_list)
leads_retrievalRetrieve leads from Lead Ads / Instant Forms

Notes:

  • public_profile is granted by default on every Facebook Login and does not need to be requested explicitly, so it is not in the list above.
  • “Page Public Metadata Access” / “Page Public Content Access” are not required — mureo only ever operates on Pages you administer (it resolves a Page Access Token via /me/accounts and Business-owned Pages), never arbitrary public Pages.
  • pages_manage_posts was requested through v0.14.0 and is not requested any more. It existed to upload a Page photo for an Instant Form cover; the cover is now picked from photos the Page already has (meta_ads_pages_photos_list), which reads with pages_read_engagement + pages_show_list. A token issued earlier still carries the granted permission — nothing in mureo calls it, and dropping it needs no action from you.
  • After upgrading mureo to a version that adds new scopes, re-run mureo auth setup (or re-authenticate in mureo configure) so the token is re-issued with the new permissions — an existing token does not gain scopes retroactively. Removing a scope needs no re-auth.

Option A: Graph API Explorer (for testing)

  1. Go to Meta Graph API Explorer.
  2. Select your app.
  3. Click Generate Access Token. For read/write ads, ads_management + ads_read is the minimum; add the pages_* / leads_retrieval scopes from the table above to exercise Page, Lead Ads, and Instant Form features.
  4. The resulting token is short-lived (1-2 hours).

Option B: Long-Lived Token (for production)

  1. Obtain a short-lived user token via the Graph API Explorer.
  2. Exchange it for a long-lived token (60 days):
Terminal window
curl -X POST "https://graph.facebook.com/v26.0/oauth/access_token" \
-d "grant_type=fb_exchange_token" \
-d "client_id=YOUR_APP_ID" \
-d "client_secret=YOUR_APP_SECRET" \
-d "fb_exchange_token=SHORT_LIVED_TOKEN"

Use POST, not GET. The Graph /oauth/access_token endpoint accepts these parameters via the request body, keeping client_secret and the token out of the URL (and out of any request/proxy logs). mureo’s own token exchange posts them as a form body for the same reason.

Option C: System User Token (recommended for automation — and required for Live apps)

A Business Manager system-user token is the most robust Meta credential, and for many operators it is the only one that works end to end:

  • Live-mode apps cannot complete OAuth from the localhost configure UI. Facebook rejects the http://localhost redirect on its own consent page, so the failure happens before mureo’s callback is ever reached — the browser login simply dead-ends. A system-user token needs no browser redirect.
  • Dev-mode apps cannot create ad creatives. Uploading the image asset may succeed, but publishing a new creative to Meta requires a Live app — a development-mode app is blocked with error subcode 1885183. A system-user token minted on the Live app clears this.

Generate one (4 steps):

  1. Business settings → System users → create a system user with the Admin role.
  2. Assign the ad account (Manage ads) and the Page (Manage content) to that system user.
  3. Generate a token for your Live app — pick the 60-day expiry if you want mureo to track and renew it — and select the scopes ads_management, ads_read, business_management, pages_manage_ads, pages_read_engagement (add the remaining pages_* / leads_retrieval scopes from the table above for Page / Lead Ads / Instant Form features).
  4. Copy the generated token.

Entering the token in the configure UI. In the Meta Ads authentication step of mureo configure, open “Paste a system-user token” (next to Login with Facebook), paste the token, click Validate token (this reports the granted vs missing scopes and lists the ad accounts the token can reach), pick the ad account, then Save.

Most of these tokens expire. Business settings offers a 60-day expiry for a new system-user token; a token issued without one does not expire at all. mureo asks Meta’s Graph debug_token endpoint which of the two it has and records the answer beside the token — a date as token_expires_at, or Meta’s “never” verdict as token_never_expires. With a date, the status card counts the days down and warns you once fewer than 14 remain. With “never”, it says the token does not expire and leaves it alone: mureo never exchanges a permanent token, because that would trade it for a 60-day one.

The App ID and App Secret are what make that inspection possible. Meta only describes a token to the app that issued it: debug_token is a GET authenticated with an app access token (app_id|app_secret), and a system-user token cannot inspect itself. So the card’s optional App ID and App Secret do two jobs:

  • read the expiry — without the pair mureo skips the inspection entirely and the card says the expiry is untracked, naming the two fields to fill in. That is not an error, and it never blocks the save: the token is stored and works either way. If Meta refuses the inspection (usually because the pair belongs to a different app than the one that issued the token), the card says that instead;
  • renew a 60-day token — a week before it expires, using Meta’s documented exchange for expiring system-user tokens (grant_type=fb_exchange_token with set_token_expires_in_60_days; see Install Apps, Generate, Refresh, and Revoke Tokens). A token issued without an expiry is never renewed, whatever is stored.

Leave the pair blank and nothing breaks — mureo simply cannot show or track the date. Values already stored are kept when you re-paste a token without re-entering them, and they are what the inspection uses on that later paste.

Prefer that card. Saving META_ADS_ACCESS_TOKEN through the Setup tab’s mureo Credentials (advanced) form also works — a hand-entered token is stored as entered and stays off the auto-refresh clock (that write clears token_obtained_at, token_expires_at, token_type and token_never_expires, because they all describe the token being replaced and this form makes no Graph call to learn new ones) — but it writes one field, so it neither validates the token, nor reads its expiry, nor lets you pick an ad account.

  1. Go to Meta for Developers.
  2. Navigate to your app > Settings > Basic.
  3. Copy the App ID and App Secret.

These are optional for basic use, but required for reading a pasted token’s expiry and required for automatic token refresh (see below). Meta only describes a token to the app that issued it, so the pair has to belong to that app.

mureo can automatically refresh Long-Lived Tokens before they expire, so you never have to manually exchange tokens again.

  1. When mureo auth setup saves a Meta Ads token, it records a token_obtained_at ISO 8601 timestamp in credentials.json. The configure UI’s paste card additionally records what Meta’s debug_token reported for that token — token_expires_at when it has an expiry, token_never_expires: true when it does not. That inspection needs the app ID and secret of the issuing app; without them nothing is recorded and the expiry stays untracked.
  2. Each time Meta Ads credentials are loaded, mureo checks whether the token is due.
  3. If token_never_expires is set, the token is never exchanged — whatever else is stored. Meta reported it as permanent, and swapping it for a 60-day token would be a downgrade.
  4. If token_expires_at is known, the token is due 7 days before that date. This is the only correct clock for a pasted system-user token: it may have been minted at any point in its 60-day life, so its age says little about how long it has left.
  5. If it is not known, mureo falls back to the token’s age: 53+ days old, a 7-day margin before the ~60-day life of a long-lived user token that mureo minted itself.
  6. Either way mureo exchanges it for a fresh token via the Meta Graph API. set_token_expires_in_60_days=true is added only when token_type says SYSTEM_USER — Meta documents that parameter for refreshing a system-user token and not for the long-lived user token exchange, and the token’s own kind is the only thing that distinguishes the two. The new token, timestamp and expiry are written back to credentials.json atomically.
  7. This runs on every path that opens a Meta client: the MCP tools and, since #726, the analytics adapters behind mureo_analytics_run and the report skills.
FieldRequiredWhy
app_idYesNeeded to inspect the token (debug_token) and for the token exchange API call
app_secretYesSame — the two form the app access token both calls authenticate with
token_obtained_atAutoWritten by mureo auth setup and the paste card; can be added manually (ISO 8601 format)
token_expires_atAutoWritten by the paste card from Meta’s debug_token, and by each refresh. When present it replaces the 53-day age rule with “7 days before this date”
token_typeAutoGraph’s debug_token verdict, written by the paste card only. SYSTEM_USER selects the system-user refresh; absent or anything else uses the documented user-token exchange
token_never_expiresAutoGraph’s other debug_token verdict (expires_at: 0), written by the paste card only. When true, mureo never exchanges the token and never warns about an expiry

If app_id or app_secret are missing, auto-refresh is silently skipped and the existing token is used as-is.

token_never_expires must be a JSON boolean — mureo logs and ignores any other value, including the string "false", and treats the token as one whose expiry it never established.

If your credentials live outside mureo’s own file — a host that builds MetaAdsCredentials itself through a mureo.runtime_context_factory plugin — that store has to carry token_never_expires through for the “never renewed” guarantee to apply. Without it the field defaults to False, and a permanent token stored alongside an app ID and secret can still be exchanged for a 60-day one. That is the pre-#740 behaviour, never worse than it, but it is not the fix either.

{
"meta_ads": {
"access_token": "YOUR_ACCESS_TOKEN",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"token_obtained_at": "2025-12-01T00:00:00Z",
"token_expires_at": "2026-01-30T00:00:00Z",
"token_type": "SYSTEM_USER"
}
}

Auto-refresh runs on credential load, so a mureo that has not been used for two months, or an exchange Meta keeps rejecting, leaves an aging token on disk. The dashboard’s mureo integrations list watches for exactly that: past 53 days the Meta row shows how old the token is and why that matters, next to a Re-authenticate button that opens the system-user token card in place — no need to re-run the setup wizard.

That age warning only fires for a token mureo could have exchanged: without app_id / app_secret there is no exchange to have missed, and a token saved without a token_obtained_at stamp has an age mureo cannot know.

The expiry warning is separate and carries no such condition. Whenever token_expires_at is on disk, the Meta row shows the days remaining and warns below 14 — precisely because the install that cannot auto-extend is the one that most needs the notice. /daily-check reports the same countdown as a Watch finding, and an already-expired token as Action needed, so it reaches you in the morning report rather than in a failed run.

  • Concurrent protection — an asyncio.Lock prevents multiple simultaneous refresh attempts.
  • Atomic file write — credentials are written to a temp file first, then renamed, to prevent corruption.
  • 0600 permissions — the credentials file is restricted to the owner only.
  • Graceful fallback — if the refresh fails for any reason (network error, expired app secret, etc.), mureo continues with the existing token and logs a warning. No tool calls are blocked.

mureo auth setup (also called as part of mureo setup claude-code) walks you through authentication interactively:

  1. Google Ads OAuth — Enter Client ID/Secret (developer token optional), open browser for OAuth, select account.
  2. Meta Ads OAuth — Enter App ID/Secret, open browser for OAuth, obtain Long-Lived Token, select account.
  3. MCP configuration — Choose global (~/.claude/settings.json) or project-level (.mcp.json).

Project-Level MCP Configuration (.mcp.json)

Section titled “Project-Level MCP Configuration (.mcp.json)”

If you choose project-level placement, mureo auth setup creates a .mcp.json file in your project root:

{
"mcpServers": {
"mureo": {
"command": "python",
"args": ["-m", "mureo.mcp"]
}
}
}

AI agents that support .mcp.json (e.g., Claude Code) will automatically discover and connect to the mureo MCP server when working in that project directory.

Use the mureo auth commands to verify your setup:

Terminal window
# Show authentication status for all platforms
mureo auth status
# Check Google Ads credentials (shows masked values)
mureo auth check-google
# Check Meta Ads credentials (shows masked values)
mureo auth check-meta

Example output for mureo auth status:

=== Authentication Status ===
Google Ads: Authenticated
Meta Ads: Authenticated

Example output for mureo auth check-google:

{
"developer_token": "***************abcd",
"client_id": "123456789.apps.googleusercontent.com",
"client_secret": "***************wxyz",
"refresh_token": "***************efgh",
"login_customer_id": "1234567890"
}

developer_token is null unless a legacy token is stored; a stored one is shown masked, as above.

Secrets are masked, showing only the last 4 characters. This lets you verify the right credentials are loaded without exposing them.