# ASOMaster MCP server

ASOMaster is a desktop App Store Optimization app for the Apple App Store and Google Play (macOS, Windows beta), made by Cressu OÜ. It ships an MCP server, `asomaster-mcp`, that lets an AI agent read the owner's keyword boards and do the same work as the app: add apps, check ranks in every market, find and localize keywords, draft, localize and (with the owner's explicit approval) publish store listings. Changes show up live in the app.

Human-readable version: https://asomaster.app/docs/mcp/

## How it works

- Transport: stdio (newline-delimited JSON-RPC 2.0). Protocol versions 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25.
- The bridge forwards each call to the ASOMaster app on the same computer over a local channel (macOS: owner-only Unix socket; Windows: named pipe with a per-user token). ASOMaster ships for macOS and Windows. Nothing goes through a cloud service.
- If the app is closed, the bridge starts it and retries for up to 30 seconds.
- Requires an activated license that includes MCP. Otherwise every tool returns `license_required`: stop and ask the owner to activate (https://asomaster.app/license/).

## Install

In the app: **Settings › MCP clients › Install** writes the entry for Claude Code, Codex, OpenCode, Cursor, Claude Desktop or Windsurf. **Update** fixes entries that point at a moved or reinstalled app; **Remove** deletes only the ASOMaster entry.

Bridge path:

| System | Path |
|---|---|
| macOS | `/Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp` |
| Windows | `%LOCALAPPDATA%\ASOMaster\asomaster-mcp.exe` |

### Claude Code

```sh
claude mcp add --scope user asomaster -- /Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp
```

### Codex (`~/.codex/config.toml`)

```toml
[mcp_servers.asomaster]
command = "/Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp"
args = []
```

### Cursor (`~/.cursor/mcp.json`)

One-click: https://cursor.com/en/install-mcp?name=asomaster&config=eyJjb21tYW5kIjoiL0FwcGxpY2F0aW9ucy9BU09NYXN0ZXIuYXBwL0NvbnRlbnRzL01hY09TL2Fzb21hc3Rlci1tY3AiLCJhcmdzIjpbXX0%3D

```json
{ "mcpServers": { "asomaster": { "command": "/Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp", "args": [] } } }
```

### Claude Desktop (`claude_desktop_config.json`)

macOS: `~/Library/Application Support/Claude/`. Windows: `%APPDATA%\Claude\`.

```json
{ "mcpServers": { "asomaster": { "command": "/Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp", "args": [] } } }
```

### Windsurf (`~/.codeium/windsurf/mcp_config.json`)

```json
{ "mcpServers": { "asomaster": { "command": "/Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp", "args": [] } } }
```

### OpenCode (`~/.config/opencode/opencode.json`)

```json
{ "mcp": { "asomaster": { "type": "local", "command": ["/Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp"], "enabled": true } } }
```

### Any other stdio client

```json
{ "mcpServers": { "asomaster": { "type": "stdio", "command": "/Applications/ASOMaster.app/Contents/MacOS/asomaster-mcp", "args": [], "env": {} } } }
```

## Rules for agents

1. Inspect first: `get_overview` gives project ids, markets, running jobs and the license state.
2. Store and AI work (adding apps, rank checks, popularity, suggestions, localization, Listing Studio runs, the assistant, imports, publishing) is queued as a job and the tool returns at once.
3. Wait with `wait_for_jobs` (up to 50 s per call; pass `projectId` for one app) until `idle` is true. Don't poll `get_overview` in a loop. `stop_job` cancels.
4. Read the results again before the next step.
5. `estVol` / `estDiff` are ASOMaster estimates (0–100). `pop` is real popularity from Apple Ads, Google Ads or an imported file. Never present one as the other; say which you use. Missing values mean "not measured", not zero.
6. `publish_listing` changes the live store page. Call it only after the owner has seen the final text and told you to publish in this conversation; pass `ownerApproved: true` and quote their approval. The app then asks the owner in a native dialog; anything but "Publish" cancels. Nothing is submitted for review.
7. Ask before destructive tools. `remove_project` needs `confirmName` (the exact project name).
8. Pass credentials as file paths (`privateKeyPath`, `serviceAccountPath`). Secrets go to the OS keychain and are never returned. Don't echo secrets.
9. If a job reports `pausedUntil`, the app is backing off from a store rate limit on purpose. Keep waiting.

## Workflows

- **Track your own apps:** `list_console_apps` → `add_console_apps` (or `add_app`) → `wait_for_jobs` → `get_overview` → `run_rank_check` → `wait_for_jobs` → `get_project_board`.
- **Track a rival:** `search_store` → `add_app` (kind `competitor`) → `wait_for_jobs` → `get_project_board`.
- **Research a niche:** `brainstorm_project` with a brief → `wait_for_jobs` → `run_rank_check` → `wait_for_jobs` → `get_project_board` (sort `opportunity`); `suggest_keywords` for more.
- **Refresh ranks:** `run_rank_check` (`force` to ignore today's cache) → `wait_for_jobs` → `list_rank_alerts`.
- **Reposition:** `update_project` (new brief) → `suggest_keywords` → `run_rank_check` → `wait_for_jobs` → promote with `add_keywords` (section `listing`), hide with `edit_keyword`.
- **Listing Studio:** `optimizer_start` → `wait_for_jobs` → `get_optimizer` → (`optimizer_save_option` / `optimizer_more_options`) → `optimizer_choose` → `optimizer_localize` → `wait_for_jobs` → review with `optimizer_edit_locale` / `optimizer_pick_phrase` → show the owner the exact text → only on their go-ahead: `get_next_version` → `publish_listing`. `optimizer_localize` with `remaining: true` finishes only failed or missing languages.

## Example requests

- Which keywords did I lose ground on this week, and where?
- Where am I outside the top 10 in Germany? Sort by opportunity.
- Add Japan and Korea to my app and check them.
- Track the top three rivals for "white noise" in the US and show where they beat me.
- Draft new listing options, compare the best with my current listing, localize it. Don't publish.

## Errors

Errors come back as `{error: {code, message, action?}, hint}`.

| Code | What to do |
|---|---|
| `license_required`, `feature_not_licensed` | Stop and ask the owner to activate or renew in the app. |
| `ai_not_configured` | Check `get_ai_status`; the owner sets up a provider in Settings › AI. |
| `console_not_connected` | `connect_console` with a key file path, or ask the owner to connect in Settings › Store consoles. |
| `app_unavailable` | Ask the owner to open ASOMaster. |

## Troubleshooting

- Tools gone after moving or reinstalling the app: Settings › MCP clients › Update, then restart the client.
- Environment variables (all optional): `ASOMASTER_DATA_DIR` (must match the app's), `ASOMASTER_IPC_ENDPOINT`, `ASOMASTER_APP_PATH`, `ASOMASTER_NO_LAUNCH=1`, `ASOMASTER_MCP_CALL_TIMEOUT_MS` (default 90000).
- Support: asomaster@cressu.com

## What the server tells agents

These are the instructions the bridge sends in its `initialize` reply:

```text
ASOMaster is the owner's desktop App Store Optimization workspace for the Apple App Store and Google Play. These tools drive the running app (it is started automatically when closed). Everything you change appears live in the owner's app.

WORKFLOW: inspect -> act -> wait -> read.
1. Inspect: get_overview first (project ids, markets, running jobs, license). Then get_project_board for a project (summarised per market), get_term_detail for one keyword in one market, get_optimizer for listing work.
2. Act: tools that do store or AI work (add_app, run_rank_check, fetch_popularity, suggest_keywords, localize_keyword, brainstorm_project, optimizer_start / _more_options / _localize, ask_assistant, import_file, publish_listing) only QUEUE a background job and return immediately.
3. Wait: call wait_for_jobs (<= 50 s per call) and repeat until idle is true. Do not poll get_overview in a tight loop. stop_job cancels.
4. Read the results again (board / optimizer / assistant thread) before deciding the next step.

DATA YOU MUST KEEP APART:
- estVol / estDiff are ASOMaster ESTIMATES (0-100) modelled from store search results. They exist for every checked keyword.
- pop is REAL popularity (0-100) from a connected source (Apple Ads, Google Ads Keyword Planner or an imported file). It only exists after connect_popularity + fetch_popularity. Never present an estimate as real popularity or vice versa; say which one you are using.
- rank = position in store search for that market; absent = not ranked in the checked results.
- Markets are lower-case country codes ("us", "de"); listing locales look like "en-US". Apple and Google projects are separate even for the same app.

TYPICAL TASKS:
- Track an app: search_store -> add_app -> wait_for_jobs -> get_overview -> run_rank_check -> wait_for_jobs -> get_project_board.
- Find keywords: suggest_keywords or add_keywords (section idea) -> run_rank_check -> compare estVol / estDiff / pop / rank / opp, then promote winners with add_keywords section listing or hide losers with edit_keyword.
- Improve the listing: optimizer_start -> wait -> get_optimizer -> (optimizer_save_option to tweak) -> optimizer_choose -> optimizer_localize -> wait -> review drafts (optimizer_edit_locale / optimizer_pick_phrase) -> show the owner the exact text.

SAFETY:
- NEVER call publish_listing unless the owner has seen the final listing text and explicitly told you to publish it in this conversation. Publishing changes the live store page that real users see. Quote their approval in the approval field.
- Ask before destructive tools (remove_project, remove_keywords, remove_folder, optimizer_reset, disconnect_console, disconnect_popularity, clear_assistant, acknowledge_alerts with clear).
- For credentials prefer file paths (privateKeyPath, serviceAccountPath); secrets are stored in the OS keychain and are never returned. Do not echo secrets back to the user.
- The stores rate-limit; large checks take minutes. If a job reports pausedUntil, the app is backing off on purpose — keep waiting.

ERRORS come back as {error:{code,message,action?},hint}. license_required / feature_not_licensed: the owner must activate or upgrade in the app - stop and tell them. ai_not_configured: see get_ai_status. console_not_connected: connect_console. app_unavailable: ask the owner to open ASOMaster.
```

## Tools (57)

Generated from the bridge's `tools/list`. Every tool has a closed JSON Schema (`additionalProperties: false`) and MCP annotations.

### Read the workspace

Read-only views of projects, boards, single keywords, markets, alerts and the store search. Start every session with get_overview.

#### `get_overview` (read-only)

Start here. Lists every tracked project (id, store, kind, markets, keyword and top-10 counts, searches checked vs never checked, audit step, last check), folders, running and recently finished jobs, key settings, license state and unread rank alerts. Use the ids it returns with every other tool.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `folderId` | `string` |  | Only projects in this folder. |
| `kind` | `"mine" \| "competitor" \| "brainstorm"` |  | Only this kind of project. |
| `store` | `"apple" \| "google"` |  | Store: "apple" (App Store) or "google" (Google Play). |

#### `get_project_board` (read-only)

Keyword x market grid for one project, summarised per market: for each keyword its id, label, row summary and, per shown market, rank / previous rank, ESTIMATED volume & difficulty (estVol/estDiff), REAL popularity (pop, only when a provider is connected), verdict and opportunity. Also the top competitors. With more than 4 tracked markets only the main market is shown unless you pass markets. Hidden keywords are excluded unless section is hidden/all.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `limit` | `integer 1–1000` |  | Max rows per section (default 150). |
| `markets` | `array of string` |  | Countries to show (default: all if <= 4 tracked, else the main market). |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `section` | `"active" \| "listing" \| "idea" \| "hidden" \| "all"` |  | Which rows: active = listing + idea (default). |
| `sort` | `"board" \| "opportunity" \| "rank" \| "estVolume" \| "popularity"` |  | Row order (default: board order). |

#### `get_term_detail` (read-only)

Everything about one keyword in one market: the current cell (rank, estimates, real popularity, verdict), rank/estimate history by day, and the apps currently ranking for it (position, rating count, installs, whether the phrase matches their title/subtitle).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `historyPoints` | `integer 0–400` |  | Most recent history points to return (default 30). |
| `keywordId` | `string` | yes | Keyword id (from get_project_board). |
| `market` | `string` | yes | Market = ISO 3166-1 alpha-2 country code, e.g. "us", "de". |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `topApps` | `integer 0–50` |  | How many ranking apps to return (default 10). |

#### `list_markets` (read-only)

Markets (countries) available for a store with search language, listing locales and a 0-10 potential score. Use the country codes as market ids.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `featuredOnly` | `boolean` |  | Only the curated featured markets. |
| `store` | `"apple" \| "google"` | yes | Store: "apple" (App Store) or "google" (Google Play). |

#### `list_rank_alerts` (read-only)

Rank-change alerts raised by checks (keyword, market, from -> to rank), newest first.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `limit` | `integer 1–500` |  | Max alerts (default 50). |
| `projectId` | `string` |  | Only alerts for this project. |
| `unreadOnly` | `boolean` |  | Only unread alerts (default true). |

#### `search_store` (read-only, uses the network)

Live store search in one market (max 10 apps). Accepts a search term or a pasted store URL. Apps already tracked carry trackedAs {projectId, kind}. Use it to find app ids before add_app.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `country` | `string` | yes | Market = ISO 3166-1 alpha-2 country code, e.g. "us", "de". |
| `store` | `"apple" \| "google"` | yes | Store: "apple" (App Store) or "google" (Google Play). |
| `term` | `string` | yes | Search term, app name or store URL. |

### Apps, rivals, niches and folders

Add your own apps (from search, a link or your connected console), rivals and niche ideas; organise them in folders.

#### `list_console_apps` (read-only, uses the network)

Connected developer console accounts for a store (App Store Connect / Google Play Console) and the apps they can see, with whether each app is already tracked.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `store` | `"apple" \| "google"` | yes | Store: "apple" (App Store) or "google" (Google Play). |

#### `add_app` (uses the network)

Start tracking an app as a project: your own app (kind mine) or a competitor. Accepts an App Store / Play id, bundle / package name or store URL (find it with search_store). Imports the live listing and seeds keywords in the background; the new project appears in get_overview when the job finishes.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `app` | `string` | yes | App id, package name or store URL. |
| `brief` | `string` |  | Product facts, upcoming features and positioning, saved before the first AI brainstorm (keep upcoming features apart from the current listing). |
| `folderId` | `string` |  | Folder to put it in. |
| `kind` | `"mine" \| "competitor"` | yes | mine = an app the owner publishes; competitor = someone else's. |
| `markets` | `array of string` |  | Markets to track (default: the owner's default markets). |
| `store` | `"apple" \| "google"` | yes | Store: "apple" (App Store) or "google" (Google Play). |

#### `add_console_apps` (uses the network)

Track every app visible in the connected developer console account(s) for a store as 'mine' projects (skips ones already tracked).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `store` | `"apple" \| "google"` | yes | Store: "apple" (App Store) or "google" (Google Play). |

#### `brainstorm_project` (uses the network)

Create a brainstorm project for an app that does not exist yet: the AI proposes keywords from a short brief and checks them in the chosen markets. Requires a ready AI provider.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `brief` | `string` | yes | What the app does and for whom. |
| `markets` | `array of string` |  | Markets to evaluate (default: owner defaults). |
| `store` | `"apple" \| "google"` | yes | Store: "apple" (App Store) or "google" (Google Play). |

#### `update_project`

Rename a project, change its brief (context used by the AI) or its automatic rank-check schedule.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `autoCheck` | `"inherit" \| "off" \| "daily" \| "weekly" \| "biweekly" \| "monthly"` |  | Automatic check schedule (inherit = global setting). |
| `brief` | `string` |  | Short description of the app used by the AI. |
| `name` | `string` |  | Display name. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `set_project_markets`

Replace the list of markets (countries) tracked for a project. New markets are empty until run_rank_check runs for them.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `markets` | `array of string` | yes | Full list of markets to track. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `move_project`

Move a project into a folder (null = top level), optionally placing it before another project.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `beforeId` | `string` |  | Place before this project id. |
| `folderId` | `string or null` | yes | Target folder id, or null for no folder. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `resync_project` (uses the network)

Re-import the app's live listing / metadata from the store (and console when connected).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `remove_project` (destructive)

Permanently delete a project with its keywords, rank history and optimizer drafts. Only when the owner asked for this exact project: pass its name (from get_overview) as confirmName; a mismatch deletes nothing.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `confirmName` | `string` | yes | The project's exact name, as get_overview lists it. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `save_folder`

Create a folder (omit folderId) or rename an existing one.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `folderId` | `string` |  | Folder to rename; omit to create a new folder. |
| `name` | `string` | yes | Folder name. |

#### `remove_folder` (destructive)

Delete a folder. Its projects are kept and move to the top level.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `folderId` | `string` | yes | Folder id. |

### Keywords and local phrases

Add, suggest, localize and tidy keywords. Each keyword has its own phrase in every tracked market.

#### `add_keywords`

Add one or more keywords (search phrases) to a project. section 'listing' = phrases the listing targets now; 'idea' = candidates to evaluate (default). localize=true asks the AI to translate each phrase into every tracked market's search language (otherwise the same phrase is used everywhere). New keywords have no ranks until run_rank_check.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `keywords` | `array of string` | yes | Phrases to add. |
| `localize` | `boolean` |  | Translate per market with AI (default false). |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `section` | `"listing" \| "idea"` |  | Board section (default idea). |

#### `suggest_keywords` (uses the network)

Ask the AI for new keyword ideas for a project based on its listing, brief and current board; they land in the 'idea' section.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `count` | `integer 1–100` |  | How many ideas (default chosen by the app). |
| `hint` | `string` |  | Focus for the ideas, e.g. "budgeting for students". |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `localize_keyword` (uses the network)

Have the AI translate a keyword into the search language of each market (or the given markets), replacing non-overridden per-market phrases.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `keywordId` | `string` | yes | Keyword id (from get_project_board). |
| `markets` | `array of string` |  | Only these markets (default: all tracked). |

#### `set_market_phrase`

Override the exact search phrase used for a keyword in one market (e.g. a better local wording). An empty phrase removes the override.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `keywordId` | `string` | yes | Keyword id (from get_project_board). |
| `market` | `string` | yes | Market = ISO 3166-1 alpha-2 country code, e.g. "us", "de". |
| `phrase` | `string` | yes | Phrase to search in that market; empty = remove override. |

#### `edit_keyword`

Rename a keyword and/or hide it (hidden keywords stay tracked but leave the active board) or unhide it.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `hidden` | `boolean` |  | true = hide, false = show again. |
| `keywordId` | `string` | yes | Keyword id (from get_project_board). |
| `label` | `string` |  | New label. |

#### `remove_keywords` (destructive)

Permanently delete keywords and their rank history. Prefer edit_keyword hidden:true unless the owner wants them gone.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `keywordIds` | `array of string` | yes | Keywords to delete. |

### Rank checks, popularity and alerts

Queue store searches, fetch real popularity from a connected source, and read or clear rank alerts.

#### `run_rank_check` (uses the network)

Search the store for every keyword in the project's markets and record ranks, estimated volume/difficulty and competitors. Runs as a background job (the store is rate-limited, so large boards take minutes). Results already checked today are reused unless force is true.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `force` | `boolean` |  | Re-check even if already checked today. |
| `markets` | `array of string` |  | Only these markets (default: all tracked). |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `fetch_popularity` (uses the network)

Pull REAL search popularity for the project's keywords from the connected source (Apple Ads popularity or Google Ads Keyword Planner). Separate from rank checks and from ASOMaster's estimates; requires a connected source (get_popularity_status).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `markets` | `array of string` |  | Only these markets (default: all tracked). |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `get_popularity_status` (read-only)

Which REAL popularity sources (Apple Ads, Google Ads Keyword Planner, imported files) are connected, for which store, and when they last fetched. Credentials are never returned.

No parameters.

#### `acknowledge_alerts` (destructive)

Mark rank alerts as read (given ids, or all when ids is omitted). With clear:true, delete all alerts instead.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `clear` | `boolean` |  | Delete all alerts (ignores ids). |
| `ids` | `array of string` |  | Alerts to mark read (default: all). |

### Listing Studio

Draft listing options, choose one, localize it, review each language and publish. Publishing needs the owner's explicit approval.

#### `get_optimizer` (read-only)

Listing optimizer state for a project: step, current live listing, generated listing options (with reach/difficulty, covered phrases and validation problems), the chosen option, core phrases and per-locale drafts with translation candidates. Descriptions are cut unless full is true.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `full` | `boolean` |  | Return full descriptions (default false). |
| `locale` | `string` |  | Only show this locale's draft in full; others are listed by status. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_start` (uses the network)

Start the listing optimizer for a project: scouts the board's keywords in the main locale and has the AI write several complete listing options (title, subtitle/short description, keyword field, description) scored for reach and difficulty. Replaces previous options. Background job; then read get_optimizer.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `ai` | `"claude_code" \| "codex" \| "opencode" \| "ollama"` |  | AI provider used by the app for generation (see get_ai_status). |
| `directions` | `string` |  | Optional creative direction for the AI, e.g. "emphasise offline use". |
| `locale` | `string` | yes | Main listing locale, e.g. "en-US". |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_more_options` (uses the network)

Generate additional listing options next to the existing ones.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `count` | `integer 1–10` | yes | How many more options. |
| `directions` | `string` |  | Optional creative direction for the AI, e.g. "emphasise offline use". |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_save_option`

Add your own listing option (omit optionId) or replace the listing of an existing option. The app re-scores it and reports validation problems (length limits, repeated words) in get_optimizer.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `label` | `string` |  | Name for a new option. |
| `listing` | `object` | yes |  |
| `listing.description` | `string` |  | Full description (4000 chars). |
| `listing.keywords` | `string` |  | Apple keyword field, comma separated (100 chars). |
| `listing.locale` | `string` | yes | Listing locale, e.g. "en-US". |
| `listing.promotionalText` | `string` |  | Apple promotional text (170 chars). |
| `listing.shortDescription` | `string` |  | Google short description (80 chars). |
| `listing.subtitle` | `string` |  | Apple subtitle (30 chars). |
| `listing.title` | `string` |  | App name / title (Apple 30 chars, Google 30). |
| `optionId` | `string` |  | Option to replace; omit to add a new one. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_remove_option` (destructive)

Delete one listing option (generated or custom) from the optimizer.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `optionId` | `string` | yes | Option id. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_choose`

Pick the listing option to ship and the core phrases it must carry into every locale. Next step: optimizer_localize.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `corePhrases` | `array of string` | yes | Main-locale phrases to keep in every locale. |
| `optionId` | `string` | yes | Chosen option id. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_localize` (uses the network)

Write per-locale drafts of the chosen listing: the core phrases are translated, candidate local phrasings are measured in each locale's test market, and the best are used. A locale that fails does not stop the others; get_optimizer lists failedLocales and lastError. Background job.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `directions` | `string` |  | Optional creative direction for the AI, e.g. "emphasise offline use". |
| `locales` | `array of string` |  | Only these locales (default: see scope). |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `remaining` | `boolean` |  | Only locales without a draft yet or that failed last time (finish an interrupted run). |
| `scope` | `"markets" \| "all"` |  | Without locales: the tracked markets' locales (markets, default) or every listing locale of the store in fast mode (all: batched AI candidates, parallel, measured in each locale's test market only). |

#### `optimizer_edit_locale`

Change fields of one locale's draft listing (empty string clears a field).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `fields` | `object` | yes | Fields to change; an empty string clears the field. |
| `fields.description` | `string` |  | Full description (4000 chars). |
| `fields.keywords` | `string` |  | Apple keyword field, comma separated (100 chars). |
| `fields.promotionalText` | `string` |  | Apple promotional text (170 chars). |
| `fields.shortDescription` | `string` |  | Google short description (80 chars). |
| `fields.subtitle` | `string` |  | Apple subtitle (30 chars). |
| `fields.title` | `string` |  | App name / title (Apple 30 chars, Google 30). |
| `locale` | `string` | yes | Locale of the draft. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_pick_phrase`

In one locale draft, use a different measured candidate phrase for a core phrase (from that locale's candidates in get_optimizer).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `keyword` | `string` | yes | Core phrase (main locale) being translated. |
| `locale` | `string` | yes | Locale of the draft. |
| `phrase` | `string` | yes | Candidate phrase to use. |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `optimizer_reset` (destructive)

Discard all options, the choice and every locale draft for the project.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `get_next_version` (read-only, uses the network)

The editable app version on the developer console (if any) and the version string the app would suggest for publishing.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `publish_listing` (destructive, uses the network)

DESTRUCTIVE / LIVE: pushes the chosen listing and every ready locale draft to App Store Connect or Google Play Console, changing what real users see. Only call after the owner has reviewed the exact listing text and explicitly told you to publish in this conversation; never on your own initiative and never as part of a larger plan they approved in general terms. Requires a connected console. The app then shows the owner a confirmation dialog listing the store, app, version, locales and fields; nothing is published unless they click Publish there (no answer within ~2 minutes = denied, error code not_approved). Background job once approved; follow with wait_for_jobs and report the outcome.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `approval` | `string` | yes | Quote the owner's explicit go-ahead. |
| `ownerApproved` | `true` | yes | Must be true: the owner explicitly approved publishing this listing. |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `versionString` | `string` |  | App version to attach the listing to (see get_next_version); default: the app's suggestion. |

### Assistant

Talk to the in-app assistant. It proposes changes; they apply only when approved.

#### `ask_assistant` (uses the network)

Send a message to the in-app ASO assistant for a project (keywords or optimizer scope). It PROPOSES keyword / listing changes (status proposed) but applies nothing by itself; review them with the owner, then call apply_assistant_changes or dismiss_assistant_changes. Background job; read the reply with get_assistant_thread after wait_for_jobs.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `ai` | `"claude_code" \| "codex" \| "opencode" \| "ollama"` |  | AI provider used by the app for generation (see get_ai_status). |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `scope` | `"keywords" \| "optimizer"` | yes | Conversation scope. |
| `text` | `string` | yes | Message. |

#### `get_assistant_thread` (read-only)

Recent messages of the in-app ASO assistant for a project (keywords or optimizer scope), including keywords it mentioned and the changes it proposed with their status (proposed / applied / dismissed / failed).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `lastMessages` | `integer 1–100` |  | How many recent messages (default 10). |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `scope` | `"keywords" \| "optimizer"` | yes | Conversation scope. |

#### `apply_assistant_changes`

Apply changes the assistant proposed in one of its messages (ids from get_assistant_thread, e.g. "c0"; omit changeIds for every still-proposed change). Already applied or dismissed changes are skipped. Only apply what the owner agreed to.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `changeIds` | `array of string` |  | Changes to apply (default: all proposed). |
| `messageId` | `string` | yes | Assistant message id (get_assistant_thread). |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `dismiss_assistant_changes`

Mark changes the assistant proposed in one of its messages as dismissed (omit changeIds for every still-proposed change). Nothing in the project changes.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `changeIds` | `array of string` |  | Changes to dismiss (default: all proposed). |
| `messageId` | `string` | yes | Assistant message id (get_assistant_thread). |
| `projectId` | `string` | yes | Project id (from get_overview). |

#### `clear_assistant` (destructive)

Delete the assistant conversation history of a project.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | yes | Project id (from get_overview). |

### Store consoles and popularity sources

Connect App Store Connect, Google Play Console, Apple Ads or Google Ads. Pass key files by path; secrets go to the OS keychain and are never returned.

#### `connect_console` (uses the network)

Connect an App Store Connect API key (apple: keyId, issuerId, private key .p8) or a Google Play service account (google: service-account JSON, optional package names). Pass FILE PATHS (privateKeyPath / serviceAccountPath; files must be .p8/.pem/.json/.txt in the owner's Downloads, Desktop or Documents folder) so secrets never enter the conversation; the bridge reads them locally and the app stores them in the OS keychain. Credentials are never returned. Needed for console apps, publishing and own-app metadata.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `issuerId` | `string` |  | apple: issuer id. |
| `keyId` | `string` |  | apple: API key id. |
| `label` | `string` |  | Account label shown in the app. |
| `packageNames` | `array of string` |  | google: package names this account manages. |
| `privateKeyPath` | `string` |  | apple: path to the AuthKey_XXXX.p8 file. |
| `privateKeyPem` | `string` |  | apple: PEM contents (prefer privateKeyPath). |
| `serviceAccountJson` | `string` |  | google: service-account JSON contents (prefer serviceAccountPath). |
| `serviceAccountPath` | `string` |  | google: path to the service-account JSON file. |
| `store` | `"apple" \| "google"` | yes | Store: "apple" (App Store) or "google" (Google Play). |

#### `test_console` (uses the network)

Verify a connected console account still works (updates its status in list_console_apps).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `accountId` | `string` | yes | Console account id (list_console_apps). |

#### `disconnect_console` (destructive)

Remove a console account and delete its stored key from the keychain. Ask the owner first.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `accountId` | `string` | yes | Console account id. |

#### `connect_popularity` (uses the network)

Connect a source of REAL keyword popularity: apple_ads (Apple Ads API: clientId, teamId, keyId, private key, optional orgId), google_ads_planner (Google Ads Keyword Planner: developerToken, customerId, refreshToken, oauthClientId, oauthClientSecret, optional loginCustomerId) or imported (a popularity export file at filePath). Prefer file paths for keys. Secrets go to the OS keychain and are never returned. Then call fetch_popularity.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `clientId` | `string` |  | apple_ads: client id. |
| `customerId` | `string` |  | google_ads_planner: customer id. |
| `developerToken` | `string` |  | google_ads_planner: developer token. |
| `filePath` | `string` |  | imported: path to the export file (CSV / JSON). |
| `keyId` | `string` |  | apple_ads: key id. |
| `loginCustomerId` | `string` |  | google_ads_planner: manager account id. |
| `oauthClientId` | `string` |  | google_ads_planner: OAuth client id. |
| `oauthClientSecret` | `string` |  | google_ads_planner: OAuth client secret. |
| `orgId` | `string` |  | apple_ads: org id when several are visible. |
| `privateKeyPath` | `string` |  | apple_ads: path to the private key PEM. |
| `privateKeyPem` | `string` |  | apple_ads: PEM contents (prefer privateKeyPath). |
| `refreshToken` | `string` |  | google_ads_planner: OAuth refresh token. |
| `source` | `"apple_ads" \| "google_ads_planner" \| "imported"` | yes | Popularity source. |
| `store` | `"apple" \| "google"` |  | Store: "apple" (App Store) or "google" (Google Play). |
| `teamId` | `string` |  | apple_ads: team id. |

#### `disconnect_popularity` (destructive)

Disconnect a popularity source and delete its stored credentials. Ask the owner first.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `source` | `"apple_ads" \| "google_ads_planner" \| "imported"` | yes | Popularity source. |

### Settings, AI, import and export

Change app settings, pick and test the AI provider, import keyword files and export data.

#### `update_settings`

Change app settings; only the keys you pass change. Use set_ai_provider for the AI provider/model.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `settings` | `object` | yes |  |
| `settings.aiConcurrency` | `integer 1–16` |  | Parallel AI requests. |
| `settings.aiEffort` | `"low" \| "medium" \| "high"` |  | AI reasoning effort. |
| `settings.aiFallback` | `boolean` |  | Fall back to another ready provider on failure. |
| `settings.alerts` | `object` |  |  |
| `settings.autoUpdate` | `boolean` |  | Install updates automatically. |
| `settings.blurMode` | `boolean` |  | Blur app names for screenshots. |
| `settings.cellDisplay` | `"rank" \| "estimate" \| "popularity"` |  | What board cells show. |
| `settings.checkSchedule` | `"off" \| "daily" \| "weekly" \| "biweekly" \| "monthly"` |  | Global automatic check schedule. |
| `settings.checkSpeed` | `"normal" \| "fast"` |  | Store request pacing (fast risks throttling). |
| `settings.defaultMarkets` | `object` |  |  |
| `settings.metricDriver` | `"estimate" \| "popularity"` |  | Which metric drives colours, verdicts and opportunity: ASOMaster estimate or real popularity. |
| `settings.parallelApps` | `integer 1–4` |  | Projects checked at the same time. |
| `settings.theme` | `"system" \| "light" \| "dark"` |  | UI theme. |
| `settings.updateChannel` | `"stable" \| "beta"` |  | App update channel. |

#### `get_ai_status` (read-only)

Status of the local AI providers the app uses for brainstorming, suggestions, the optimizer and the assistant: installed / signed in / ready, current model and the next setup step when not ready.

No parameters.

#### `set_ai_provider`

Select the local AI provider (and optionally model) the app uses for generation. Check readiness with get_ai_status first.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `model` | `string` |  | Model id from get_ai_status (default: provider default). |
| `provider` | `"claude_code" \| "codex" \| "opencode" \| "ollama"` | yes | AI provider used by the app for generation (see get_ai_status). |

#### `test_ai_provider` (read-only, uses the network)

Send a tiny test prompt through a provider and record latency / errors (see get_ai_status).

| Parameter | Type | Required | Description |
|---|---|---|---|
| `provider` | `"claude_code" \| "codex" \| "opencode" \| "ollama"` | yes | AI provider used by the app for generation (see get_ai_status). |

#### `import_file` (uses the network)

Import an Astro export or a CSV of apps / keywords from a local path (must be a .csv/.tsv/.txt/.json file in the owner's Downloads, Desktop or Documents folder). format astro without path reads Astro's own database on this Mac: its popularity becomes real popularity and each Astro app's phrases join the tracked app with the same App Store id. previewOnly:true just reports what would be imported (rows, apps, keywords, countries). With projectId the phrases are added as keywords of that project; otherwise apps become projects (optionally in folderId). Background job.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `folderId` | `string` |  | Folder for imported projects. |
| `format` | `"astro" \| "csv"` | yes | File format. |
| `path` | `string` |  | Absolute path of the file (omit with format astro to read Astro's local database). |
| `previewOnly` | `boolean` |  | Only preview (default false). |
| `projectId` | `string` |  | Add the imported phrases to this project. |
| `store` | `"apple" \| "google"` |  | Store: "apple" (App Store) or "google" (Google Play). |

#### `export_data`

Export a project's board, rank history or optimizer drafts as CSV or JSON. By default returns the content (cut at maxChars); with saveToFile:true writes the file into the app's exports folder instead.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `format` | `"csv" \| "json"` | yes | File format. |
| `maxChars` | `integer 100–2000000` |  | Max characters of content to return (default 50000). |
| `projectId` | `string` | yes | Project id (from get_overview). |
| `saveToFile` | `boolean` |  | Write to <dataDir>/exports instead of returning content. |
| `what` | `"board" \| "history" \| "optimizer"` | yes | What to export. |

### Jobs

Store and AI work runs as background jobs. Wait for them, or stop one.

#### `wait_for_jobs` (read-only)

Block until background work changes state: returns as soon as a job finishes (done / failed / stopped), or when nothing is queued or running, or after timeoutSeconds (max 50). With jobId, waits for that job only; with projectId, for that project's jobs. Call it repeatedly after any tool that queued work until idle is true, then read the results. Reports progress of jobs still running.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `jobId` | `string` |  | Wait for this job only. |
| `projectId` | `string` |  | Only this project's jobs (a finished fetch can start brainstorming, then a check). |
| `timeoutSeconds` | `integer 1–50` |  | Max seconds to wait (default 45, max 50). |

#### `stop_job`

Stop a queued or running background job (rank check, AI generation, import, ...). Work already done is kept.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `jobId` | `string` | yes | Job id (get_overview / wait_for_jobs). |

