# Wow Keywords MCP

Give Claude Code, Codex, OpenCode, Cursor or any MCP client the whole app: track apps, research keywords in every market, run the ASO audit and publish it to App Store Connect.

## How it works

- A local stdio server bundled inside Wow Keywords. No account, API key or hosted server.
- The agent launches the bridge; the first tool call connects to the running app (and opens it on Mac).
- Data stays on the computer. App Store Connect keys never reach the agent.
- Needs an activated copy of Wow Keywords on Mac or Windows. `ELECTRON_RUN_AS_NODE=1` runs the bridge on the app itself, so Node.js is not needed.

## Connect

Fastest for every agent below: click **MCP** in the Wow Keywords sidebar, then **Add**. That page also shows commands with your exact install path.
Paths below are the default installs. Windows: replace `YOU` with your user name in JSON configs.

### Claude Code

**Run** this once:

Mac:

```bash
claude mcp add --scope user wow-keywords \
  -e ELECTRON_RUN_AS_NODE=1 \
  -- "/Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords" \
     "/Applications/Wow Keywords.app/Contents/Resources/mcp.cjs"
```

Windows:

```powershell
claude mcp add --scope user wow-keywords `
  -e ELECTRON_RUN_AS_NODE=1 `
  '--' "$env:LOCALAPPDATA\Programs\Wow Keywords\Wow Keywords.exe" `
       "$env:LOCALAPPDATA\Programs\Wow Keywords\resources\mcp.cjs"
```

**Check:** `claude mcp list` shows `wow-keywords`. Start a new Claude Code session.

### Codex

**Run** this once:

Mac:

```bash
codex mcp add wow-keywords \
  --env ELECTRON_RUN_AS_NODE=1 \
  -- "/Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords" \
     "/Applications/Wow Keywords.app/Contents/Resources/mcp.cjs"
```

Windows:

```powershell
codex mcp add wow-keywords `
  --env ELECTRON_RUN_AS_NODE=1 `
  '--' "$env:LOCALAPPDATA\Programs\Wow Keywords\Wow Keywords.exe" `
       "$env:LOCALAPPDATA\Programs\Wow Keywords\resources\mcp.cjs"
```

**Check:** `codex mcp list` shows `wow-keywords`. The Codex app, CLI and IDE extension share it. Start a new session.

### OpenCode

**Add** this to your OpenCode config, keeping anything already there:

Mac — `~/.config/opencode/opencode.json`:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "wow-keywords": {
      "type": "local",
      "command": [
        "/Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords",
        "/Applications/Wow Keywords.app/Contents/Resources/mcp.cjs"
      ],
      "environment": {
        "ELECTRON_RUN_AS_NODE": "1"
      },
      "enabled": true
    }
  }
}
```

Windows — `%USERPROFILE%\.config\opencode\opencode.json`:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "wow-keywords": {
      "type": "local",
      "command": [
        "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\Wow Keywords.exe",
        "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\resources\\mcp.cjs"
      ],
      "environment": {
        "ELECTRON_RUN_AS_NODE": "1"
      },
      "enabled": true
    }
  }
}
```

**Check:** `opencode mcp list` shows `wow-keywords` as connected.

### Cursor

**Install** with one link, then confirm in Cursor:

Mac: [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=wow-keywords&config=eyJjb21tYW5kIjoiL0FwcGxpY2F0aW9ucy9Xb3cgS2V5d29yZHMuYXBwL0NvbnRlbnRzL01hY09TL1dvdyBLZXl3b3JkcyIsImFyZ3MiOlsiL0FwcGxpY2F0aW9ucy9Xb3cgS2V5d29yZHMuYXBwL0NvbnRlbnRzL1Jlc291cmNlcy9tY3AuY2pzIl0sImVudiI6eyJFTEVDVFJPTl9SVU5fQVNfTk9ERSI6IjEifX0=) · Windows: [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=wow-keywords&config=eyJjb21tYW5kIjoiQzpcXFVzZXJzXFxZT1VcXEFwcERhdGFcXExvY2FsXFxQcm9ncmFtc1xcV293IEtleXdvcmRzXFxXb3cgS2V5d29yZHMuZXhlIiwiYXJncyI6WyJDOlxcVXNlcnNcXFlPVVxcQXBwRGF0YVxcTG9jYWxcXFByb2dyYW1zXFxXb3cgS2V5d29yZHNcXHJlc291cmNlc1xcbWNwLmNqcyJdLCJlbnYiOnsiRUxFQ1RST05fUlVOX0FTX05PREUiOiIxIn19)

**Paste** into the Cursor MCP file:

Mac — `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "/Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords",
      "args": [
        "/Applications/Wow Keywords.app/Contents/Resources/mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

Windows — `%USERPROFILE%\.cursor\mcp.json`:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\Wow Keywords.exe",
      "args": [
        "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\resources\\mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

**Check:** Cursor Settings → **MCP** shows `wow-keywords` with a green dot.

### Claude Desktop

**Open** Claude Desktop → **Settings → Developer → Edit Config** and add:

Mac — `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "/Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords",
      "args": [
        "/Applications/Wow Keywords.app/Contents/Resources/mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

Windows — `%APPDATA%\Claude\claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\Wow Keywords.exe",
      "args": [
        "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\resources\\mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

**Quit and reopen** Claude Desktop. The tools menu in a new chat lists `wow-keywords`.

### Windsurf

**Add** to the Windsurf MCP config:

Mac — `~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "/Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords",
      "args": [
        "/Applications/Wow Keywords.app/Contents/Resources/mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

Windows — `%USERPROFILE%\.codeium\windsurf\mcp_config.json`:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\Wow Keywords.exe",
      "args": [
        "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\resources\\mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

**Check:** press **Refresh** in Windsurf’s MCP panel; `wow-keywords` lists its tools.

### Other apps

Any app that runs a local (stdio) MCP server works. Most take this JSON:

Mac:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "/Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords",
      "args": [
        "/Applications/Wow Keywords.app/Contents/Resources/mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

Windows:

```json
{
  "mcpServers": {
    "wow-keywords": {
      "command": "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\Wow Keywords.exe",
      "args": [
        "C:\\Users\\YOU\\AppData\\Local\\Programs\\Wow Keywords\\resources\\mcp.cjs"
      ],
      "env": {
        "ELECTRON_RUN_AS_NODE": "1"
      }
    }
  }
}
```

Apps with a form instead ask for three fields:

Mac:

```
Command      /Applications/Wow Keywords.app/Contents/MacOS/Wow Keywords
Arguments    /Applications/Wow Keywords.app/Contents/Resources/mcp.cjs
Environment  ELECTRON_RUN_AS_NODE=1
```

Windows:

```
Command      C:\Users\YOU\AppData\Local\Programs\Wow Keywords\Wow Keywords.exe
Arguments    C:\Users\YOU\AppData\Local\Programs\Wow Keywords\resources\mcp.cjs
Environment  ELECTRON_RUN_AS_NODE=1
```

## Try asking

- “Which of my apps lost the most ranks since the last check?”
- “Where is my app outside the top 10 in Germany, and what would it take to get in?”
- “Brainstorm keywords for a keto calorie tracker and check them in the US.”
- “Add Japan and Korea to my app and check every keyword there.”
- “Run the ASO audit for my app and show me the three options side by side.”

## Rules for agents

Wow Keywords tracks App Store keywords in every market, in each market’s own language, for three kinds of entries (status apps[].kind): the owner’s apps (mine), research apps (competitor: competitors and any app being studied), and brainstorms (session_create: a topic, no app; volume and difficulty only, no rank). For product pivots, pass brief when adding the app or save app_context before research; the current store listing alone does not describe upcoming features. The sidebar groups them in folders: My apps, Research, Brainstorms and the owner’s own (status settings.folders; app_move, folder_create). Start with status, or overview for every app at once. board_get returns the grid compactly; term_get has one phrase’s top apps. Long work (app_add, app_fetch, keywords_brainstorm, check_run, audit_english, audit_localize, audit_publish) runs in the app and the tool returns at once: call wait (returns within 50s; call again while work runs), then status. Own apps and research apps can be audited and exported; only connected owner apps can publish. The ASO audit: audit_english → audit_choose → audit_localize → audit_get (export) or audit_publish (next_version first; only when the owner asked). AI runs share one pool across apps (Settings aiParallel, 1–4, default 3), so queueing many apps is fine; one job per app and kind. Missing measurements are null, never zero. Volume is a 0–100 proxy derived from relevant search-result apps, not Apple search counts or search popularity. Make research and draft decisions within the owner’s request; ask only for missing product facts or choices that materially change the requested direction. Publishing requires the owner’s authorization, but do not ask again when it is already given.

- Tools marked **background** return at once. Call `wait` (returns within 50 s), repeat while jobs run, then `status`.
- `audit_publish` writes to App Store Connect. Call it only when the owner asked. It never submits for review.
- Ids: apps are App Store ids or `bs_…` (brainstorms); keyword ids come from `board_get`; storefronts are lowercase codes from `markets_list`.

## Workflows

| Goal | Calls |
|---|---|
| Track your own apps | `asc_apps` → `app_addAll` → `wait` → `overview` |
| Track a competitor | `store_search` → `app_add` → `wait` |
| Research a niche without an app | `session_create` → `wait` → `board_get` |
| Refresh ranks | `check_run` → `wait` → `board_get` |
| Reposition an app | `app_context` → `keywords_brainstorm` → `wait` → `board_get` → `audit_english` → `wait` → `audit_get` |
| ASO audit | `audit_english` → `wait` → `audit_get` → `audit_choose` → `audit_localize` → `wait` → `audit_get` |
| Publish the audit | `next_version` → `audit_publish` → `wait` |

## Troubleshooting

| Problem | Fix |
|---|---|
| “Wow Keywords is not running and could not be opened.” | Open Wow Keywords, then ask again. |
| “Activate Wow Keywords first.” | Enter your license key in the app. |
| The agent doesn’t see the tools | Start a new agent session. Check its list command (`claude mcp list`, `codex mcp list`, `opencode mcp list`). |
| It worked, then stopped after moving or reinstalling the app | Click **MCP** in the Wow Keywords sidebar and press **Update** next to the agent. |
| A long job seems stuck | Ask the agent to call `status`. Jobs keep running in the app; `wait` returns within 50 seconds by design. |

## Tools (54)

### Read

#### `status` (read)

Tracked apps (ids, markets), connections and recent jobs. Start here.

#### `overview` (read)

One line per tracked entry: kind, markets, keyword count, phrases checked vs never checked, last check, audit stage (none, options, chosen, localized, published) and running jobs. Use it to plan work across all apps before reading a board.

| Param | Type | Notes |
|---|---|---|
| `kind` | mine \| competitor \| brainstorm | Only this kind. |

#### `board_get` (read)

The app’s keyword grid, compact: listings, then one row per keyword (id, label, source, hidden, mean volume) with one cell per market (phrase, rank, earlier rank, volume proxy 0-100 (relevant apps in search results, not search counts), difficulty 0-100, verdict; rank null = not in the first 200; checked false = never searched). Top 5 competitors per market and the researcher chat. Pass storefront for one market only. term_get has a cell’s top apps and history.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `storefront` | string | Storefront code, lowercase, e.g. "es". markets_list has them all. |

#### `term_get` (read)

Everything about one keyword in one market: the phrase, rank history, volume and difficulty breakdown, and the top apps for that search (name, subtitle, ratings, whether each counts toward volume).

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `keywordId` * | string | Keyword id from board_get. |
| `storefront` * | string | Storefront code, lowercase, e.g. "es". markets_list has them all. |

#### `markets_list` (read)

Every storefront code with its country, language and App Store Connect locales. main = one of the 52 stores the window offers; prefer those. size (main stores only) = estimated market: population, smartphone users and iPhone users in millions, iOS share %, GDP per person at PPP in thousand $, score 0–10 (√iPhone users × purchasing power^1.5 × access, log scale, USA = 10) and share % of all main stores, access 0–1 = share of the market that can actually buy (restriction says why; Russia and India are cut); rounded estimates, not measurements.

#### `ai_status` (read)

Which AI CLIs can run here (Claude Code, Codex, OpenCode, Ollama): installed, signed in, and their models with reasoning levels. Pass one as ai on AI tools, or save it with settings_save.

#### `wait` (read)

Returns status immediately when no matching job is running. Otherwise waits until a job changes state (or one of appId’s jobs, when given) or seconds pass (default and max 50). Call again while jobs run; a finished fetch can start brainstorming, then a check, so inspect all jobs for the app.

| Param | Type | Notes |
|---|---|---|
| `appId` | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `seconds` | integer | 5–50. |

### Apps

#### `store_search` (read)

Top 8 apps in one App Store (default us) for a name or niche, or the one app an App Store link names, from the store the link names: id, name, developer, ratings, whether it is tracked and whether the App Store Connect key sees it (mine). Track one with app_add.

| Param | Type | Notes |
|---|---|---|
| `term` * | string | Name, niche or App Store link. |
| `storefront` | string | Storefront code, lowercase, e.g. "es". markets_list has them all. |

#### `asc_apps` (read)

Every app the connected App Store Connect accounts can see: id, name, bundle id, account (key id), whether it is on the App Store and already tracked. null = no key connected. Track one with app_add and its id.

#### `app_add` (write, background)

Track an app from its App Store URL or numeric id; an unreleased app in the owner’s App Store Connect account is added by its id (asc_apps). A URL naming a country (apps.apple.com/ru/…) makes that store the app’s main one: its name, first country, and the audit’s default main listing. Reads every locale’s listing next (App Store Connect when a key is connected). kind: mine (the owner’s app) or competitor; by default mine when the App Store Connect key sees the app, else competitor. store_search finds ids by name. Adding automatically reads listings, brainstorms and checks: wait for those jobs instead of starting a duplicate brainstorm. markets scopes this app’s initial work; brief gives the AI product direction before it starts. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `url` * | string | App Store URL or id. |
| `kind` | mine \| competitor | Optional. |
| `folder` | string | Optional sidebar folder: "mine", "research" or a folder id (status settings.folders). Default: My apps when the key sees it, else Research. |
| `markets` | string[] | Initial storefronts for this app only. Omit to use settings.defaultMarkets; choose a focused set before the first automatic check. |
| `brief` | string | Product facts, upcoming changes and preferred positioning, saved before the first brainstorm. Distinguish upcoming features from the current listing. Read back in status or board_get.app.brief. |

#### `app_context` (write)

Save or replace the app’s product brief: product facts, upcoming changes and preferred positioning. All later brainstorms, researcher conversations and ASO audits use it. This does not start jobs or erase measurements. Empty brief clears it. For directions that apply to one audit only, use audit_english request. Read back in status or board_get.app.brief.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `brief` * | string | Product brief, up to 4000 characters; empty clears. |

#### `app_addAll` (write, background)

Track every on-store app the App Store Connect key sees that is not tracked yet, as the owner’s apps. Returns as soon as the work starts. Call wait, then status, to follow it.

#### `app_kind` (write)

Move an app between My apps (mine) and Research (competitor). Both kinds can be audited and exported; only connected owner apps can publish to App Store Connect.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `kind` * | mine \| competitor | Kind. |

#### `app_move` (write)

Move an entry to a sidebar folder and place it in that folder’s order. to: "mine" (My apps; makes it the owner’s), "research" (Research; competitors and apps being studied), "brainstorms" (brainstorms only) or an owner-made folder id, where it keeps its kind. before: the entry it lands above; omit for last.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `to` * | string | Folder. |
| `before` | string | Optional entry id it goes above. |

#### `folder_create` (write)

Make a sidebar folder. Returns its id. Move entries in with app_move.

| Param | Type | Notes |
|---|---|---|
| `name` * | string | Folder name. |

#### `folder_rename` (write)

Rename an owner-made sidebar folder.

| Param | Type | Notes |
|---|---|---|
| `folderId` * | string | Folder id. |
| `name` * | string | New name. |

#### `folder_remove` (write, destructive)

Delete an owner-made sidebar folder. Its entries go back to My apps, Research or Brainstorms; nothing is deleted.

| Param | Type | Notes |
|---|---|---|
| `folderId` * | string | Folder id. |

#### `import_astro` (write, background)

Import Astro (the Mac ASO tracker) as evidence: per app, every phrase it tracked with Astro’s popularity, difficulty, rank and best rank (astro_get). Nothing is added to the board; the researcher and brainstorms use it to suggest keywords. Apps not tracked here are added (with their Astro stores) and brainstormed with it. Reads Astro’s own database on this Mac, or the CSV from Astro’s Export all keywords (file). preview: counts only, nothing changes. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `file` | string | Optional absolute path to an Astro keyword CSV export. |
| `preview` | boolean | Only count what would come across. |
| `folder` | string | Optional folder for apps that are new here: "mine", "research" or a folder id. |

#### `astro_get` (read)

What Astro (another ASO tracker) followed for this app, saved by import_astro: per store and phrase, Astro’s popularity (Apple search popularity 5–100; 5 = the floor), difficulty, current rank and best rank in the last 90 days (null = not ranked). Evidence for which keywords to add (phrases the app already ranks for, with popularity above 5), not tracked keywords. The researcher and brainstorms read it too.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |

#### `app_markets` (write)

Set the tracked countries, in display order; all show in the grid. Newly added countries get every keyword translated (AI) and checked; dropped ones keep their phrases for later.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `markets` * | string[] | Storefront codes. |

#### `app_autoCheck` (write)

Set how often this app is re-checked on its own. default = follow settings autoCheck (the default for every app).

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `autoCheck` * | default \| off \| daily \| weekly \| biweekly \| monthly | Interval. |

#### `app_fetch` (write, background)

Re-read the app’s name, subtitle and keyword field per locale. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |

#### `app_remove` (write, destructive)

Stop tracking an app and delete its keywords.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |

### Brainstorms

#### `session_create` (write, background)

Start a keyword brainstorm: no app, just a topic ("a calorie tracker for keto dieters"). The AI proposes 15–25 keywords, checked for volume and difficulty in the US first, then in the Settings countries (settings.defaultMarkets), or only in the markets given. Change countries later with app_markets. Brainstorms have no rank, listing or audit. Returns its id (bs_…). Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `brief` * | string | What to brainstorm. |
| `markets` | string[] | Optional storefront codes; default: US, then the Settings countries. |

#### `session_update` (write)

Rename a brainstorm or change its topic.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `name` | string | New name. |
| `brief` | string | New topic. |

### Keywords

#### `keywords_brainstorm` (write, background)

AI rebuilds the app’s keywords: the phrases its listing targets (name, subtitle and keyword field combined) plus up to 10 ideas, each with one phrase in every market, then checks new phrases. Keywords the user added or edited stay. On a brainstorm it adds up to 25 more keywords and keeps everything. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |

#### `keyword_add` (write, background)

Track a keyword (English phrase). scope "all" (default): the AI fills in the phrase each tracked country types; "english": the label as is, in every tracked country (no translation). Pass terms (storefront → phrase) to set phrases yourself; missing countries are still translated with scope all. section "listing" files it under App Store Connect, else Ideas. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `label` * | string | English phrase, e.g. "habit tracker". |
| `scope` | all \| english | Which countries. |
| `terms` | object | Optional storefront → local phrase. |
| `section` | idea \| listing | Section. |

#### `keyword_localize` (write, background)

Translate a keyword into every tracked country it has no phrase in yet, then check. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `keywordId` * | string | Keyword id from board_get. |

#### `keyword_update` (write)

Rename a keyword.

| Param | Type | Notes |
|---|---|---|
| `keywordId` * | string | Keyword id from board_get. |
| `label` * | string | New English phrase. |

#### `keyword_hide` (write)

Hide a keyword from the board and from checks without deleting it, or show it again.

| Param | Type | Notes |
|---|---|---|
| `keywordId` * | string | Keyword id from board_get. |
| `hidden` * | boolean | true hides, false shows. |

#### `keyword_remove` (write, destructive)

Stop tracking a keyword in every market.

| Param | Type | Notes |
|---|---|---|
| `keywordId` * | string | Keyword id from board_get. |

#### `term_set` (write)

Set the phrase one market types for a keyword. Empty text removes the keyword from that market.

| Param | Type | Notes |
|---|---|---|
| `keywordId` * | string | Keyword id from board_get. |
| `storefront` * | string | Storefront code, lowercase, e.g. "es". markets_list has them all. |
| `text` * | string | Phrase as people type it in that market. |

#### `check_run` (write, background)

Search every term in each market: rank, volume and difficulty. Reuses today’s cached results unless force is true; pass force true when the owner asks to refresh ranks now. Apps are also re-checked on their own (settings autoCheck). Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `storefronts` | string[] | Only these markets. |
| `force` | boolean | Re-check ranks already checked today (default false). |

### ASO audit

#### `audit_get` (read)

The app’s ASO audit: main-locale current fields, options, chosen fields, core keywords and research; per locale current vs proposed fields and the local phrases tested with their numbers. export: true returns the export file format instead (per-locale fields, changes, tests).

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `export` | boolean | Return the export format. |

#### `audit_english` (write, background)

ASO audit step 1 (own apps and competitors; a competitor’s keyword field is not public, so it is inferred): optimizes the main listing (locale, default en-US or the last audit's): measures extra phrases in that locale's store when the AI needs more data, then proposes 3 name / subtitle / keyword field options in its language (one recommended), keeping the brand. Every other locale is localized from it in step 2. Result in audit_get english. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `locale` | string | Optional main listing: an App Store Connect locale, e.g. "es-ES". Default en-US. |
| `request` | string | Optional note for the auditor, e.g. "keep the brand in the name". |
| `ai` | object | Optional AI for this run; omitted = Settings. ai_status lists the providers that can run and their models. |

#### `audit_more` (write, background)

Write more main-listing options (default 3), different from the ones the audit has; existing options stay. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `count` | integer | 1–5. |
| `request` | string | Optional directions, e.g. "lead with the main feature". |
| `ai` | object | Optional AI for this run; omitted = Settings. ai_status lists the providers that can run and their models. |

#### `audit_measure` (write, background)

Measure phrases for the audit in the main listing's store: one App Store search each (cached per day); results join audit_get english.research. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `phrases` * | string[] | Phrases in the main listing's language, lowercase. |

#### `audit_option_add` (write)

Add your own main-listing option to the audit (shown as a tab). Words already in name or subtitle are dropped from the keyword field; limits 30/30/100.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `title` * | string | 2–3 words. |
| `why` | string | One line: what it wins. |
| `explain` | string | Optional: two short paragraphs on the idea. |
| `fields` * | object | name, subtitle, keywords. |

#### `audit_option_update` (write)

Change an existing main-listing option in place (it keeps its tab and number). Pass the full new fields; title/why/explain are optional.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `optionId` * | string | The option id from audit_get. |
| `title` | string | Optional new title. |
| `why` | string | Optional. |
| `explain` | string | Optional. |
| `fields` * | object | name, subtitle, keywords. |

#### `audit_option_remove` (write, destructive)

Remove one main-listing option from the audit.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `optionId` * | string | Option id from audit_get. |

#### `audit_choose` (write)

Settle the main listing's fields: an option’s fields, the current ones, or your own. Limits: name 30, subtitle 30, keywords 100. core = the 2–4 main-language phrases every locale tests local words for.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `fields` * | object | name, subtitle, keywords. |
| `core` | string[] | Optional core phrases. |

#### `audit_localize` (write, background)

ASO audit step 2: for every App Store locale (or the ones given) the AI lists local candidates for each core keyword, each is searched once in that locale’s store, and the locale is written from the winners. Result in audit_get locales. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `locales` | string[] | App Store Connect locales, e.g. "de-DE". Default: all 49 besides the main listing. |
| `request` | string | Optional directions; default = the main listing audit’s request. |
| `ai` | object | Optional AI for this run; omitted = Settings. ai_status lists the providers that can run and their models. |

#### `audit_locale_set` (write)

Edit one locale’s proposed fields by hand.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `locale` * | string | App Store Connect locale. |
| `fields` * | object | name, subtitle, keywords. |

#### `audit_locale_pick` (write, background)

Use another tested phrase for a core keyword in one locale; the AI rewrites that locale. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `locale` * | string | App Store Connect locale. |
| `keyword` * | string | Core keyword. |
| `text` * | string | One of its tested candidates. |

#### `audit_clear` (write, destructive)

Delete the app’s ASO audit.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |

#### `next_version` (read)

For an own app: the editable iOS version in App Store Connect, if any, and the next version number to create. audit_publish writes into the editable one, or needs versionString = next. null = no key connected.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |

#### `audit_publish` (write, background, destructive)

Write the audit into App Store Connect: the editable iOS version (created as versionString when there is none), every locale’s keyword field, name and subtitle; missing locales are added. Never submits for review. Only when the owner asked. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `versionString` | string | New version number when no editable version exists, e.g. "2.4". |

### Researcher chat

#### `agent_ask` (write, background)

Message the in-app keyword researcher for an app, as the owner would in the chat. It reads the board and may add, hide, delete or edit keywords; with scope "audit" it also reads the ASO audit and may add or remove listing options and measure US phrases. Its answer lands in board_get chat. Returns as soon as the work starts. Call wait, then status, to follow it.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `text` * | string | The message. |
| `scope` | keywords \| audit | The page the owner is on. |
| `ai` | object | Optional AI for this run; omitted = Settings. ai_status lists the providers that can run and their models. |

#### `agent_clear` (write, destructive)

Clear the researcher chat for an app.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |

### App Store Connect and settings

#### `asc_connect` (write)

Save an App Store Connect API key (encrypted on this computer) as one more developer account (status settings.ascAccounts), then re-read every app’s listings. The same key again updates it; replace = an account id this key takes the place of. Pass keyFile (path to the AuthKey_<id>.p8; keyId then comes from its name) or privateKey.

| Param | Type | Notes |
|---|---|---|
| `issuerId` * | string | Issuer id (UUID). |
| `keyId` | string | Key id; optional with an AuthKey_<id>.p8 keyFile. |
| `keyFile` | string | Absolute path to the .p8 file. |
| `privateKey` | string | .p8 file contents, when there is no file. |
| `replace` | string | Optional account id (key id) to replace. |

#### `asc_test` (write)

Check the saved App Store Connect keys with Apple (one account with accountId, else all). Returns how many apps they see, or the reason it fails (a revoked key or wrong Issuer ID → asc_connect with a new key; 403 → the key needs App Manager access).

| Param | Type | Notes |
|---|---|---|
| `accountId` | string | Optional account id (key id). |

#### `asc_disconnect` (write, destructive)

Forget one App Store Connect account (accountId = its key id), or every account without accountId.

| Param | Type | Notes |
|---|---|---|
| `accountId` | string | Optional account id (key id). |

#### `settings_save` (write)

Pick the AI CLI used for brainstorming, its model and reasoning level (null = the CLI default), how often every app is re-checked on its own (default daily), check speed, how many AI runs go at once, whether detail popups open on click or hover, and the countries new apps start with.

| Param | Type | Notes |
|---|---|---|
| `aiProvider` | claude \| codex \| opencode \| ollama | Provider. |
| `aiModel` | object | Model and reasoning level for one provider. Models are listed by Settings → AI agent. |
| `aiCli` | object | Which installed copy of a CLI to run, when status lists more than one (ai_status copies). null = the newest (default). |
| `autoCheck` | off \| daily \| weekly \| biweekly \| monthly | Auto-check interval. |
| `checkSpeed` | normal \| fast | Check speed: normal (2 phrases at a time, default) or fast (3 at a time, closer together; Apple may pause it more often). |
| `aiParallel` | integer | AI runs at once across every app (brainstorms, translations, audits, chat), 1–4, default 3. App Store checks have their own queue and are not affected. |
| `details` | click \| hover | Detail popups open on click (default) or on hover. |
| `boardMetrics` | string[] | Metrics shown on the board, at least one. Default: all three. Order is always rank, difficulty, volume. |
| `defaultMarkets` | string[] | Storefront codes for new apps. |

#### `job_stop` (write)

Stop a running job for an app (status lists them). Stopping localize also drops the keywords still waiting to be translated; phrases already written stay.

| Param | Type | Notes |
|---|---|---|
| `appId` * | string | App id from status: an App Store id, or "bs_…" for a brainstorm. |
| `kind` * | fetch \| brainstorm \| localize \| check \| agent \| audit \| auditLocalize \| publish | Job kind. |

`*` = required.
