Docs · MCP

Connect your AI agent

Claude Code, Codex, Cursor, Claude and other MCP apps can run HappyKeywords for you: track apps, check ranks in every country, run the Audit, write your listing and upload it.

Runs on your Mac

A local server inside the app. Your data stays on your Mac.

104 tools

Apps, keywords, ranks, the Audit, Optimize and upload.

One click to set up

Settings → AI apps (MCP) → Add, or one command.

Two ways to use AI with HappyKeywords

Ask the AI Advisor inside the app, or connect the agent you already use. Both read the same numbers.

The AI Advisor: the panel opens from the app header, "Which keywords should I focus on?" is answered with keyword chips showing today's ranks, and a chip opens its numbers

Built in

AI Advisor

Ask about your app in a panel next to it. It reads your keywords, ranks, Audit, listing and Optimize draft, and answers in a few lines, with today's numbers on each keyword and a button to the step that does the work.

Setup
Uses your AI from Settings → AI, or another one just for that chat. Nothing else to install.
Changes your app
No. It only chats; you make the changes in the app.
Best for
Quick answers while you work.
Watch it in action ↑

Your own agent

Claude Code, Codex, Cursor, Claude

Your agent uses HappyKeywords' 104 tools from its own window, over MCP, and can do the work for you.

Setup
One click in Settings → AI apps (MCP), or one command.
Changes your app
Yes, when you ask: adds keywords, runs checks and the Audit, writes options, and uploads after you say yes.
Best for
Longer jobs, many apps, and workflows you repeat.
See it in action ↓
Claude Code

> How is AI Tennis Swing Coach doing, and what should I fix first?

  • ● status
  • ● audit_get(appId: 13)
  • ● listing_get(appId: 13)

AI Tennis Swing Coach scores 34/100, flat all week (35 → 34). The listing text is strong at 87, but rankings are weak at 8. …

#1 for “ai tennis coach” in the US (demand 60) and the UK (56), and top 5 in most other countries.

… Names that drop “Swing” in German, Russian, Italian, Spanish, Portuguese and Japanese (for example, the German name becomes “AI Tennis Coach”). Right now you're #1 for “ai tennis swing coach” in Germany, Austria, Switzerland and Russia (demand about 45 in each) … Keep “Swing” in the name unless you've decided you don't need those searches.

Ratings. You have 7, a 4.86 average. … An in-app review prompt after a good swing score would help more than any keyword change.

A real conversation with Claude Code, trimmed (… marks a cut). HappyKeywords answered from the app on this Mac.

Connect

HappyKeywords needs to be in your Applications folder and activated.

HappyKeywords Settings, AI apps (MCP): Claude Code, Codex, Cursor and Claude, each with an Add button
Settings → AI apps (MCP) in HappyKeywords: press Add next to your agent.

Claude Code

  1. 1

    One click: in HappyKeywords, open Settings → AI apps (MCP) and press Add next to Claude Code.

  2. 2

    Or run this once:

    Terminal
    claude mcp add-json --scope user happykeywords \
      '{"type":"stdio","command":"/Applications/HappyKeywords.app/Contents/MacOS/HappyKeywords","args":["--mcp"]}'
  3. 3

    Check: claude mcp list shows happykeywords. Then start a new Claude Code session.

Codex

  1. 1

    One click: Settings → AI apps (MCP) → Add next to Codex. The Codex CLI and the Codex app share it.

  2. 2

    Or run this once:

    Terminal
    codex mcp add happykeywords -- \
      "/Applications/HappyKeywords.app/Contents/MacOS/HappyKeywords" --mcp
  3. 3

    Check: codex mcp list shows happykeywords.

Cursor

  1. 1

    One click: Settings → AI apps (MCP) → Add next to Cursor.

  2. 2

    Or add this to ~/.cursor/mcp.json:

    ~/.cursor/mcp.json
    {
      "mcpServers": {
        "happykeywords": {
          "command": "/Applications/HappyKeywords.app/Contents/MacOS/HappyKeywords",
          "args": [
            "--mcp"
          ]
        }
      }
    }

Claude (desktop app)

  1. 1

    One click: Settings → AI apps (MCP) → Add next to Claude, then restart Claude.

  2. 2

    Or add this to Claude's config file:

    ~/Library/Application Support/Claude/claude_desktop_config.json
    {
      "mcpServers": {
        "happykeywords": {
          "command": "/Applications/HappyKeywords.app/Contents/MacOS/HappyKeywords",
          "args": [
            "--mcp"
          ]
        }
      }
    }

Any other MCP app

  1. 1

    Gemini CLI, OpenCode, Windsurf and others: add a local (stdio) server with this command. Settings → AI apps (MCP) shows the same config to copy.

    Command
    /Applications/HappyKeywords.app/Contents/MacOS/HappyKeywords --mcp

Try asking

Start a new session in your agent, then ask in plain words.

  • “Which of my apps lost ranks since the last check?”
  • “Where is my app outside the top 10 in Germany, and what would it take to get in?”
  • “Add Japan and Korea to my app and check every keyword there.”
  • “Run the Audit for my app and tell me what to fix first.”
  • “Write three name and subtitle options for my app, then localize the best one into German and French.”
  • “Find hot niches in the US App Store and show me product ideas for the best one.”

How long jobs run

  • Checks, the Audit, writing and uploads run inside the app. The tool that starts one answers at once.
  • The agent follows along with wait, which returns within 50 seconds, and calls it again until the job is done.
  • You can watch the same job in the HappyKeywords window, and stop it there.
  • If your AI runs out of usage halfway, the job pauses: the agent can switch to another AI (job_use_ai), continue later or stop it.
  • Uploads happen only after the agent shows you publish_preview and you say yes. publish_undo puts the draft back.
The HappyKeywords window during a rank check: Checking 34 of 407, about 10 min left, with Stop
A check running in the HappyKeywords window, with its progress and Stop, whoever started it.

Workflows

The calls an agent makes for common jobs.

Track your own apps

asc_apps→app_add_all→wait→status

Refresh ranks

check_run→wait→board_get

Where an app stands

audit_run→wait→audit_get

A better listing

optimize_write→wait→optimize_choose→optimize_localize→wait→optimize_get

Upload, then Undo if needed

publish_preview→publish_upload→wait→publish_undo

Ideas before you build

hot_niches_find→wait→hot_niches_get→product_ideas_find

Tools (104)

The same calls the HappyKeywords window makes. Read tools only look; write tools change your data in the app, and the upload tools write to App Store Connect.

Apps, keywords and ranks · 41
  • statusread

    Start here. Your apps (id, countries, keyword count, last check), running and recent jobs, whether the App Store can be reached, and how fast searches go.

  • app_listread

    Your apps and research apps, in sidebar order: id, kind (mine | research), App Store id, name, subtitle, developer, ratings, countries in order, folder.

  • sidebar_getread

    What the sidebar shows: apps (mine and research, with their folder and order) and folders.

  • app_addwrite

    Track an app from its App Store link, numeric id, or name (the best App Store match). kind: mine (default, one of your apps) or research (an app you study). It starts with the default countries. mainLocale: its main listing language (an App Store Connect code like "en-US"; default: Apple's primary language when your App Store Connect key sees the app); its home store joins the countries. Nothing is searched until check_run. isNew is false when it was already tracked. (Competitors inside an app: competitor_add. All your App Store Connect apps at once: app_add_all.)

  • app_settingswrite

    Change an app's settings. notes: product notes (what it does, who it's for, what's coming). checkInterval: how often it's re-checked (daily, weekly, biweekly, monthly, off, or default = Settings → Check every app). mainLocale: the main listing language (an App Store Connect code like "es-MX", or default = Apple's primary language); its home store joins the app's countries when missing. It never changes anything in App Store Connect. Only what you pass changes.

  • listing_getread

    What the public App Store shows for an app in each country it's checked in (name, subtitle, ratings, when seen), and every change of name or subtitle since tracking began, newest first. asc: what App Store Connect has (null when it was never read): every language of the live version and the one in progress (name, subtitle, keyword field, promo text, description, What's New, links), versions per platform with the suggested next one, when it was read and the last problem. A keyword field of null means not known, "" means empty. Over MCP, descriptions and What's New come as their length; pass locale (an App Store Connect code like "de-DE") for that language's texts in full.

  • app_movewrite

    Move an app in the sidebar: into a folder (folderId) or out of one (folderId null), between My apps and Research (kind), and to a position in the order.

  • folder_createwrite

    Create a sidebar folder in My apps (kind mine, the default) or Research. Move apps into it with app_move; they take the folder's kind.

  • folder_movewrite

    Move a sidebar folder to My apps (kind mine) or Research. Its apps go with it and become that kind.

  • folder_renamewrite

    Rename a sidebar folder.

  • folder_removewrite

    Remove a sidebar folder. Its apps stay, back in My apps or Research.

  • app_removewrite

    Stop tracking an app: its keywords, countries, competitors and rank history are deleted from this Mac. Its jobs stop.

  • app_marketswrite

    Set an app's countries, in display order. New countries fill in by themselves once the app has been checked.

  • board_getread

    An app's keyword × country board: each cell's phrase, rank (null = not in the top 200), demand and difficulty (0–100 estimates; demand is null with demandSignal false when there's no signal at all: shown as No data, which does not mean nobody searches it), verdict (own, push, target, hard, maybe, keep, swap, dead, or nodata), chance (0–1, shown as rankability: how realistic the top 10 is for the app), competitors' ranks, when it was checked (null = never). Over MCP it comes compact: per keyword, cells as country → [phrase ("" = the keyword itself), rank, demand, difficulty, verdict, chance], with lastChecked per country; pass storefront for that country only with every detail (previous rank, demand signal, competitors' ranks by id, the top 5 apps, rank history, where your listing has it).

  • term_getread

    Everything about one keyword in one country: the phrase, top 10 apps, App Store suggestions, rank history and the plain-English reasons behind the numbers.

  • term_setwrite

    Set the phrase people type in one country for a keyword (e.g. "kalorienzähler" in Germany). Empty = use the keyword itself. The cell is checked again by itself.

  • keyword_addwrite

    Track keywords (English phrases) for an app in all its countries. Up to 200 at once; ones already tracked are skipped. Once the app has been checked, they fill in by themselves.

  • keyword_hidewrite

    Hide a keyword from the board and from checks without losing its history (hidden true), or show it again (hidden false). board_get lists hidden keywords.

  • keyword_avoidwrite

    🚫 Not for this app (on, default): mark a phrase the app should never target, with an optional note why ("no offline mode yet"). Every AI step (Optimize options, other languages, promo and description, More keywords, Language check) leaves it out, the keyword field packer skips it, a listing that still uses it gets a red warning, and the tracked keyword with that label is hidden. off = true uses it again (and shows the keyword again); rerun Optimize to use it. Calling it again with a note changes the note. board_get lists them in avoid.

  • keyword_fitswrite

    "This fits" (on, default): say a phrase belongs to this app although its name, subtitle, description and your product notes don't use those words yet (a feature that's coming, say). The phrase stops reading "Fit unclear" in Missing from your listing and Where you stand, and it can be recommended again; it also comes off 🚫 Not for this app if it was there. off = true takes that back. The opposite mark is keyword_avoid.

  • keyword_renamewrite

    Rename a keyword (its English phrase). Countries without their own phrase are checked again with the new one; the old rank history stays.

  • keyword_countrywrite

    Remove a keyword from one country only (tracked false; it stays in the app's other countries and isn't checked there), or bring it back (tracked true).

  • keyword_removewrite

    Stop tracking a keyword in every country (its rank history goes too).

  • competitor_addwrite

    Track a competitor next to an app (App Store link, id or name). Their ranks come from the same searches, at no extra cost. Up to 10 per app.

  • competitor_removewrite

    Stop tracking a competitor of an app.

  • competitor_ideasread

    Apps that keep showing up in the top 10 for this app's keywords, from searches already made (no new searches).

  • app_suggestread

    For a newly added app: about 30 suggested keywords to start with (picked: true; more with picked: false), from its listing, App Store suggestions and words top apps use, plus suggested countries that fit the languages its listing offers. Add them with keyword_add and app_markets, then check_run.

  • keyword_ideasread

    Keyword ideas without AI: what people type (App Store suggestions), phrases several top apps use, and phrases from the app's own name and subtitle.

  • store_searchread

    Find apps on the App Store by name. byName: complete a partial name with App Store suggestions (finds small apps plain search buries).

  • markets_listread

    Every country HappyKeywords supports: storefront code, name and flag.

  • check_runwrite

    Search every keyword in each of an app's countries: rank, demand, difficulty. Reuses searches from the last hour unless force is true. Returns the job at once; follow it with wait. If a check of this app is running, that one is returned.

  • refresh_planread

    Refresh keywords: every app with its keyword × country cells and when it was last checked, to pick which to re-check (about 2 seconds per cell).

  • refresh_runwrite

    Re-check several apps in one go: each gets a check, run one after another. Searches from the last hour are reused. Returns the jobs; follow them with wait.

  • job_listread

    Running, waiting, paused and interrupted jobs, then recent finished ones (newest first). Pass appId for one app.

  • job_stopwrite

    Stop a job (running, waiting, paused or interrupted). What it finished stays saved.

  • job_retrywrite

    Run the failed items of a finished job again ("done · N to retry" or failed).

  • job_continuewrite

    Continue a job that was interrupted when HappyKeywords closed, or one paused by a limit, right now.

  • job_dismisswrite

    Hide a finished job's error message.

  • waitread

    Returns when a job changes state (of appId's jobs, when given), or after `seconds` (default and most 50). Returns at once when nothing is running. Call again while jobs run.

  • settings_getread

    Settings: checkEvery (how often apps are re-checked while HappyKeywords is open; autoCheck = not off), keywordIdeas (ideas panel), appearance (system, light, dark), boardMetrics (what a board cell shows), detailsOn (keyword details on click or hover), defaultMarkets (countries a new app starts with), mode (easy = fewer numbers and choices, details folded, AI asks once per run; expert = everything; new installs start in easy).

  • settings_savewrite

    Change settings (see settings_get). Only what you pass changes.

App Store Connect · 5
  • asc_statusread

    App Store Connect: whether a key is connected (accounts: key type, the Key ID's last 4 characters, apps seen, when last tested, the last problem in plain words), whether keys can be saved on this Mac, and whether a read is running. Nothing secret.

  • asc_testread

    Tests each connected App Store Connect key with Apple (lists its apps and opens one app's listing; changes nothing in App Store Connect). Returns the same as asc_status, with the result as appsSeen, testedAt and problem (the key's own problems only). A passing failure (offline, slow down, Apple not answering) is an error with a plain message and changes nothing.

  • asc_appsread

    Apps the connected App Store Connect key sees, A–Z: Apple ID, name, bundle id, Apple's primary language, icon, whether it's tracked here (appId, kind), when its listing was last read and the last problem. refresh: ask Apple for the list again first (one request).

  • app_add_allwrite

    Add apps your App Store Connect key sees as your apps, in one job (returns it at once; follow it with wait). apps: Apple IDs from asc_apps, each with its main listing language (mainLocale: an App Store Connect code, or null = Apple's primary language). countries: default (Settings → default countries) or suggested (each app's suggested countries); the main language's home store is added. keywords: suggested (the ones app_suggest ticks) or none. check: now (each app is checked as it's added, one after another) or later (with the next automatic check). An app already yours is skipped; a Watchlist app moves to your apps and keeps its keywords and checks; an app not on the App Store yet fails. Each listing is read from App Store Connect (read only).

  • listing_syncwrite

    Read App Store Connect again (read only; nothing changes there): one app (appId), or every one of your apps the connected key sees. Returns the job at once (the running one if a read is already going), or null when there's nothing to read (no key connected, or the key doesn't see that app). Follow it with wait, then listing_get.

AI and jobs · 17
  • ai_statusread

    AI tools on this Mac (Claude Code, Codex, Gemini through the Antigravity CLI, GitHub Copilot, Grok, OpenCode, Ollama): copies found with versions, state (not_installed, too_old, not_signed_in, not_working, unknown = not tested, ready), a plain message, the fix command, chosen model and reasoning level with the choices, usage left per limit with reset times, which AI is in use (null until the user chooses one), and AI runs at once. refresh: look for AI tools again first (a few seconds).

  • ai_testread

    Sends one tiny request to an AI tool and reports how long it took (in ai_status). Uses a little of the plan's usage. Never changes which AI is in use. Offline is an error that changes nothing.

  • job_use_aiwrite

    Use another AI now: a job paused at its AI's usage limit (job_list: pauseReason ai_limit, with aiPause.ai = the AI that's out of usage and aiPause.others = the AIs that can take over) goes on right away with one of aiPause.others, for this job only. The AI chosen in Settings stays the one in use for everything else, and a later retry of the job asks that one again. The other choices: job_continue tries the same AI again now, job_stop stops the job, and doing nothing lets it continue by itself at resumeAt.

  • keywords_morewrite

    More keywords: asks the AI in use for about 15 new search phrases for the app (in its first country's language, from its listing, keyword field and product notes), then measures each in that country (demand, difficulty, potential, your rank). Nothing is tracked: they appear in keyword_ideas as the "ai" group. About 1 AI request and 15 App Store searches. Returns the job at once (the running one if there is one); follow it with wait.

  • localize_planread

    What Localize would do for an app: the languages among its countries that aren't English (German once for Germany, Austria and Switzerland), with their countries and how many keywords still search the English keyword there, plus the AI requests and App Store searches it would take. Phrases you typed are never replaced, so they don't count. keywordIds: only these keywords. redo: also the phrases the AI chose earlier (redoable says how many keywords have one).

  • keyword_localizewrite

    Localize: for each language among the app's countries, asks the AI in use for 2–3 ways people there type each keyword, measures each (and the English keyword itself) in that language's first country, and makes the most searched one the keyword's phrase in every country of that language; names (apps, games, brands) stay as they are. Phrases you typed stay. The cells then check by themselves. keywordIds: only these keywords (default all). redo: also look again at the phrases the AI chose earlier. See localize_plan for the cost first. Returns the job at once; follow it with wait.

  • niche_planread

    What Explore a niche would use in these countries (store codes, e.g. us, de): AI requests (1 for the name and about 20 keywords, plus Localize's for each language that isn't English), App Store searches (Localize's and the first check of every keyword in every country) and about how many minutes.

  • niche_createwrite

    Explore a niche: makes a niche for an app idea (no App Store app needed; it shows under Niches in the sidebar), then the AI in use names it, gives it an emoji, a tagline and about 20 keywords, says them the way people search in each country's language, and the first check measures demand and difficulty in every country. There's no rank (no app yet). See niche_plan for the cost first. Returns the niche and the job at once; follow it with wait.

  • niche_findwrite

    Explore a niche again for a niche whose keywords weren't found (the AI's answer couldn't be used, or no AI was set up): the AI names it and finds about 20 keywords, Localize, then the first check. Returns the job at once; follow it with wait.

  • product_ideas_getread

    A niche's latest Product ideas: 2–4 product directions (who it's for, pitch, first features, risks), each with the niche's keywords that support it (demand, difficulty, potential in the niche's first country, today's numbers) and the apps it would compete with. stale: a check since then brought newer numbers (product_ideas_find again). null when there are none yet.

  • product_ideas_findwrite

    Product ideas for a niche: asks the AI in use for 2–4 narrower product directions built only on the niche's measured keywords and the apps in their top results. About 1 AI request, no App Store searches, under a minute. Needs at least 3 checked keywords. Returns the job at once; follow it with wait, then product_ideas_get.

  • product_ideas_trackwrite

    Track these keywords: makes a new niche from one product direction (index from product_ideas_get, 0 first), named after it, in the same countries, with just its keywords. Their local phrases and latest numbers are copied, so it needs no new searches. Returns the new niche.

  • hot_niches_getread

    The last Find hot niches: niches from Apple's top charts, best first, each with a plain idea, its category, its apps (with their Top grossing / Top free places and release dates) and tags: makesMoney (its apps in Top grossing: how many, best place), newMoney (an app under 18 months old already in Top grossing), rising (apps that climbed since an earlier day; "tomorrow" on the first day), fresh (new apps in a top 100), crowded (big old apps hold top places). Apple publishes no revenue or downloads: 'makes money' means in Top grossing. explored: the niche Explore made from it. null before the first Find.

  • hot_niches_planread

    What Find hot niches would use for a country and categories (App Store genre ids; Games is 6014): chart reads, AI requests and about how many minutes. No App Store searches.

  • hot_niches_findwrite

    Find hot niches: reads Apple's top charts (Top grossing and Top free per category, New apps) for one country, picks the apps worth a look by rules (young apps already making money, apps in both charts, fast climbers, new apps in a top 100), and the AI in use groups them into niches (without an AI, each app is its own row). Replaces that country's last list; the charts are read again once a day from then on, so Rising works from the second day. genres: App Store genre ids (default: every category but Games). A Find again on the same day reuses that day's grouping (no AI requests); fresh: true has the AI in use group them again. Returns the job at once; follow it with wait, then hot_niches_get.

  • hot_niche_exploredwrite

    Marks a hot niche (id from hot_niches_get) as explored into a niche (appId from niche_create), so the list shows Explored · open.

  • niche_updatewrite

    Renames a niche, or changes its emoji or tagline. Only for niches (an app's name comes from the App Store).

Audit · 4
  • competitor_dismisswrite

    ✕ Not a competitor: take an unrelated app out of the Audit's Main competitors (the next one moves up) and out of what the takeaways and next steps say; the score still counts every app above you. off = true brings it back. audit_get lists them in dismissed; you is your own app's row.

  • audit_getread

    Where an app stands (the Audit tab), worked out now from the same numbers as board_get, with no AI: score 0–100 = 0.5 Rankings + 0.3 Listing + 0.2 Competition (at most Rankings + 30), each part with bars, a note and how it was worked out; every chosen country (countries[], left = not checked yet) and, with no storefront, all countries weighted by market size; main competitors (apps above you in your searches: ahead, avgRank, reviews, rating, released, updated, tracked); keywords in and outside the top 10 (outside sorted by potential); nextSteps from the numbers (each with the step it leads to); the AI's latest takeaways (stale when a check since brought newer numbers; null before audit_run with takeaways); history (score per day). Not for niches.

  • audit_planread

    What audit_run would cost: cells a re-check searches (about 2 seconds each) and minutes, when the numbers were last checked and whether that's over 12 hours ago (old). Takeaways are 1 AI request.

  • audit_runwrite

    Rerun audit: recheck = check every keyword again first (a normal check, App Store searches; see audit_plan), then save the audit to its history, then takeaways = ask the AI in use for a headline and 3–6 takeaways (1 AI request; numbers stay ours). feedback: what the AI should look at. Returns the job at once; follow it with wait, then audit_get. The score itself never needs this: audit_get has it.

Optimize · 33
  • optimize_copy_writewrite

    Optimize step 2: the main language's promo text (170) and description (4000) with the AI in use (1 AI request): improved from the current ones, or fresh = written anew. keepPromises keeps true promises of the current texts ("free", "no account"); premium lets it mention the paid upgrade as your texts or notes describe it. Returns the job at once; follow with wait, then optimize_get (draft.copy).

  • optimize_copy_setwrite

    Type the promo text or description yourself: the main language's (no locale), or one translation (locale). keep = true keeps App Store Connect's as they are (main: counts as done; a translation: nothing uploaded for it). Over Apple's limits is refused. One undo step.

  • optimize_copy_fix_nameswrite

    Put this app's own name where a promo text or description still says an old one (or another language's name): the main language's (draft.copy.main.looks) and every translation's (each language's own name; its check looks). Each fix is kept with Put back (draft.copy.main.fixes with optimize_copy_put_back; a translation's check fixes with its Put back). One undo step. Returns {fixed: fields changed, view}.

  • optimize_copy_put_backwrite

    Put back a name fix in the main promo text or description (optimize_copy_fix_names), while the text still reads as the fix left it; it isn't made again. One undo step.

  • optimize_copy_translatewrite

    Translate the main promo text and description (yours, else App Store Connect's) into these languages (default: step 3's languages and the ones already in App Store Connect). 1 AI request per language, each saved as it finishes. Returns the job at once.

  • changes_getread

    The change log: every change to the app's listing seen so far, newest first (public page name/subtitle per country from checks; keyword field per language from App Store Connect's live version), each with ranks before → after for the tracked keywords in that store (last check before the change, latest check), the top-10 count before and now, and early = under 3 days ago (ranks take a few days to move).

  • optimize_localize_planread

    What Optimize step 3 would do: the languages (default: the languages of the app's countries plus the ones already in App Store Connect, the main language left out; or the ones given), which are new to App Store Connect, all languages grouped (yours, inAsc, others), and the cost: AI requests (1 per 5 languages for ways to search + 1 per language to write) and App Store searches (each way measured in that language's own store).

  • optimize_localizewrite

    Optimize step 3: localize the chosen main listing (optimize_choose first) into these languages (default as optimize_localize_plan). First, with App Store Connect connected, the main name is saved in App Store Connect (optimize_check_name_apple): a taken name is refused before any AI is used; an accepted one stays held for your app. Each language: ways to search each intent (AI), measured in its own store, then its name, subtitle, keyword field and 2–3 backup names (AI), packed and checked; its name is searched in its own store and a taken one swaps to the first free backup (draft.locales[].swapped). Languages kept as they are stay kept. Editing the main listing afterwards never clears a language (v2): it marks what became out of date (draft.stale), and optimize_localize with those locales, optimize_keep_current or editing them yourself are the three ways on. A field you typed yourself is kept by every update. Returns the job at once; follow with wait, then optimize_get (draft.locales).

  • optimize_locale_setwrite

    Edit one language's name, subtitle or keyword field in the draft, or use = a measured way to put in the field that carries its intent. One undo step.

  • optimize_locale_keepwrite

    Keep a language as it is in App Store Connect (keep = true: nothing will be uploaded for it), or use the draft again (false). One undo step.

  • optimize_language_checkwrite

    Language check (rules only: free, no AI, instant) on every written language and translation: fixes what's safe (emoji in the name, subtitle or keyword field; a word written twice in a row; keyword entries repeated, already in the name or subtitle, or another app's name; spaces after commas; a keyword field over 100 characters loses entries from the end) and keeps each field's before for optimize_language_put_back. A fix you put back isn't made again. What needs your judgment is a look (price words like "free" with a one-click change, links of the main description missing in a translation). Each language's draft.locales[].check (and copy.translations[].check) shows fixes and looks. Localize runs it by itself on each language it writes. Never blocks an upload. One undo step.

  • optimize_language_check_aiwrite

    Language check's AI pass (optional; uses AI requests: 1 per 5 written languages, shown as aiRequests in optimize_get draft.reviewPlan). Reads each written language against the main listing: wrong meaning, a dropped main topic, the wrong script, stray English, words Apple may reject (other apps' or drug brands, medical claims, price words, features the app doesn't have). Each problem becomes a look in draft.locales[].check.looks (id ai:…), with the AI's fix when it passes our rules (optimize_language_fix uses it). Nothing changes by itself. Returns {jobId}: wait, then optimize_get.

  • optimize_language_fixwrite

    Make a look's one-click change in one language (look id from check.looks, e.g. price:subtitle or fix:keywords). Kept with its before, so optimize_language_put_back undoes it. One undo step.

  • optimize_language_put_backwrite

    Put back what the language check changed in one field of one language (only while the field still reads as the fix left it). The same fix isn't made again by later checks. One undo step.

  • optimize_getread

    Optimize step 1 (name, subtitle and keyword field) for an app or niche: the main listing language and the store it's measured in; the current listing (App Store Connect, else the public page with the keyword field not known) with its Reach and Difficulty; phrases (tracked keywords there plus phrases measured for the draft: demand, difficulty, rank, chance); ranked = words of the current listing that hold a top-30 rank (the packer keeps them); draft = null before Optimize starts, else its options (texts, reach 0–100 = share of measured demand whose words the listing has, difficulty = demand-weighted difficulty of the phrases it covers, vsNow, ★ recommended, chosen, warnings, gained/lost phrases, why, intents), directions, undo/redo; path = the step path; plan = what optimize_write costs. Nothing is uploaded. Over MCP, other languages' descriptions and language-check texts come as their length; pass locale (an App Store Connect code) for that language only, in full.

  • optimize_writewrite

    Write options with the AI in use: first up to 12 new phrases measured in the main store (1 AI request + about 12 App Store searches), then 3 options (1 AI request), each packed and checked (Apple's limits, no other apps' names, words that rank kept). more = 3 more options from the phrases already measured (1 AI request). Saves directions first when given. Returns the job at once; follow with wait, then optimize_get.

  • optimize_check_nameread

    Is this name taken? Searches the public App Store (iPhone, iPad and Mac) in the language's home store (main language unless locale is given; names are per language) for a live app with exactly this name (case doesn't count; your own app never does), and saves the answer with the draft for a week. "free" means no published app has it: removed and unreleased apps hold names too, so only App Store Connect knows for sure (optimize_check_name_apple). Each option's nameCheck and, when taken, altNames (other names to try, best Reach first) are in optimize_get; so are each language's in draft.locales. About 1 s.

  • optimize_check_name_applewrite

    Save the main-language name in App Store Connect (default: the chosen listing's name): the only way to know Apple takes it, since there's no call to ask and removed or unreleased apps hold names the store search can't see. It's saved in the version in progress (editable app information; nothing is submitted, the live listing doesn't change). outcome: accepted (Apple took it and it's held for your app until you upload, pick another name, or optimize_name_put_back), refused (another app has it; nothing changed; code = Apple's error code) or skipped (note says why: not connected, no version in progress). optimize_get draft.nameHold shows the held name. optimize_localize does this itself before any AI request. Once a listing is chosen, only its name can be saved. Only your own apps with App Store Connect connected.

  • optimize_name_put_backwrite

    Put the old main-language name back in the version in progress where a name is held (optimize_get draft.nameHold). Does nothing if App Store Connect has another name there now or an upload took it over (Undo upload restores it then).

  • optimize_change_mainwrite

    Change main listing: step 1 is locked while languages written from the chosen listing exist (optimize_get draft.locked). This stops a running Localize, clears those languages (their promo text and description translations stay) and unchooses the listing, so step 1 can change and optimize_localize writes them again (draft.locked.plan says what that costs). A name held in App Store Connect goes back to the old one. One undo step: Undo brings the languages back.

  • optimize_settingswrite

    Change the draft's directions (for the AI), phrases to include (each one the listing should cover), or keepRanked (the packer keeps keyword-field words that rank now; default true). One undo step.

  • optimize_option_addwrite

    Add an option to edit by hand: a copy of the current listing (or of the texts given). Works with no AI. One undo step.

  • optimize_option_setwrite

    Edit an option's title, name, subtitle or keyword field (Apple's limits are 30, 30 and 100 characters; over-limit text is kept but flagged and can't be chosen). Editing a chosen option unchooses it. One undo step.

  • optimize_keep_currentwrite

    Optimize v2: mark work written from an older main listing (draft.stale) as fine as it is — the texts don't change, they just stop saying "Needs update". Default: every stale language and every field. locales narrows it to those languages; fields narrows it to those fields (name, subtitle, keywords, promo, description), which is how "Looks good" clears one mark after you've read it. The other two choices are optimize_localize / optimize_copy_translate with those locales (update them) or editing them yourself. One undo step.

  • optimize_options_clearwrite

    Remove the options you aren't using: everything but the chosen one, the ★ pick and the ones you name in keep. One undo step. Returns the view.

  • optimize_option_removewrite

    Remove an option from the draft. One undo step.

  • optimize_choosewrite

    Use this listing: choose an option as the main listing (optionId omitted = choose none). Refused when it's over Apple's limits. One undo step.

  • optimize_packwrite

    Pack an option's keyword field with no AI: 100 characters, no spaces after commas, each word once across name/subtitle/keywords, single words (phrases in Japanese/Chinese/Thai), no other apps' names, words that rank kept (keepRanked), free room filled with the words of the best measured phrases. Returns the draft and what changed. One undo step.

  • optimize_undowrite

    Undo the draft's last change (optimize_get says what: undoLabel).

  • optimize_redowrite

    Redo what optimize_undo undid.

  • optimize_start_overwrite

    Start a new, empty draft from the latest audit. The old draft is set aside, not deleted: it stays saved, and while the new draft is still empty optimize_get's draft.setAside says what it holds (options, languages, translations) and optimize_start_over_undo brings it back. A name held in App Store Connect goes back to the old one first.

  • optimize_restore_previouswrite

    Restore previous draft: go back to the draft Start over set aside (optimize_get's draft.setAside says what it holds). Nothing is deleted — the draft you're leaving becomes the one you can come back to. One snapshot, not a history: starting over again replaces it. Refuses when there's nothing set aside. Returns the view.

  • optimize_exportread

    The chosen listing (else the ★ option) as format json (with our numbers) or fastlane (deliver's metadata files: <locale>/name.txt, subtitle.txt, keywords.txt, promotional_text.txt, description.txt, returned as {path: text}). locales: only these languages (App Store Connect codes). Over MCP, an export too long for one answer (many languages) returns its languages instead: ask again with locales, a few at a time. Nothing is written to App Store Connect.

Upload · 4
  • publish_previewread

    Review & upload (reads only): reads App Store Connect fresh, then says what an upload of the Optimize draft would write: target (the in-progress version, or a new version it would create when none is in progress; never the live one), sentence (the plain description shown before Upload), rows per language (isNew, cells from → to, loss = keyword-field entries that would go with the rank they hold, guarded = a dropped entry ranks in the top 30 so the keyword field is held back unless removeAnyway, needsDescription, over = fields over Apple's limits, never sent), unchanged languages, blocked (why nothing can be uploaded), planKey (pass it to publish_upload) and the last upload.

  • publish_uploadwrite

    Upload: writes the reviewed plan to App Store Connect (only after the developer saw publish_preview and said yes; confirm must be true). planKey from that preview: if App Store Connect or the draft changed since, nothing is written. choices per language: skip = fields not to send, removeAnyway = send a keyword field the guard held back, force = send a field kept because the version in progress has other text (kept), noBorrow = a new language doesn't borrow the main listing's name, subtitle and keywords (rows[].borrowed; borrowing is on by default, and without it a new language without its own name sends nothing). Emoji are taken out of names, subtitles and keyword fields before sending (rows[].emoji). A name Apple refuses leaves that language's name and subtitle as they were; its promo text and description still go, and its keyword field only when it drops nothing ranking in the top 30 with the old name (failed[].fields says what didn't change). Never submits for review. Returns the job at once.

  • publish_undowrite

    Undo upload: puts the version back exactly as it was before that upload (fields back, languages the upload created removed), while it's still in progress. A field changed in App Store Connect since is left alone unless listed in overwrite. confirm must be true. Returns the job at once.

  • publish_lastread

    The app's last upload: state (done, partial, failed, undone…), version, languages, failed ones with the reason, conflicts Undo left alone, and whether Undo is possible.

Troubleshooting

“HappyKeywords isn't activated on this Mac yet.”

Open HappyKeywords and paste the license key from your email, then ask again.

The agent doesn't see the tools

Start a new agent session. Check its list command: claude mcp list or codex mcp list.

It worked, then stopped after moving or updating the app

Open Settings → AI apps (MCP) in HappyKeywords and press Update next to the agent.

“Move HappyKeywords to your Applications folder…”

The app is running from the disk image. Drag it to Applications, open it from there, then press Add again.

A long job seems stuck

Ask the agent to call status. Jobs keep running in the app, and wait returns within 50 seconds by design.

Still stuck? Email [email protected].