AppTail – ASO & Keyword Research
Server Details
App Store keyword rankings, competitors, markets, reviews and analytics for iOS apps, from your AI.
- Status
- Healthy
- Uptime
- 95.4% over 22 days
- Last Tested
- Transport
- Streamable HTTP · MCP 2025-11-25
- URL
TDQS
Scored across 25 tools
Descriptions work hard to draw boundaries, e.g. get_keywords explicitly replaces deprecated tools and get_competitors is contrasted with get_landscape. Still, several read/report tools overlap conceptually (get_performance vs get_signals vs explain_period; get_market vs get_landscape vs get_competitors), so an agent could occasionally hesitate between them.
Every tool follows a clean verb_noun snake_case pattern (add_app, get_keywords, remove_competitors, save_market, tag_keywords). No mixing of conventions or vague verbs across the set.
25 tools sits at the heavy end for a single server, and the surface spans many subdomains (apps, keywords, markets, competitors, reviews, performance, charts, signals). Each tool is justified by domain breadth, but the set is large enough to be borderline.
Strong lifecycle coverage: apps, competitors, keywords and markets each have add/remove/read, reviews have read/reply, plus discovery, signals, charts and performance. Minor gaps like no remove_app/update_app keep it just short of full coverage.
Available Tools
25 toolsadd_appAdd AppAInspect
Add an app to the account as one of the user's own. Takes an Apptail app_id, an App Store URL, or a name — resolved and imported the way search_apps does. Tracking starts immediately (rankings, reviews, competitors); measured downloads and proceeds still require connecting App Store Connect in the console, which cannot be done from here.
| Name | Required | Description | Default |
|---|---|---|---|
| app | Yes | The app to claim: an Apptail app_id, a full App Store URL, or the app's name. A URL is the reliable form — a name resolves against the whole store, and two apps can share one. | |
| country | No | Primary storefront for this app, ISO 3166-1 alpha-2 (e.g. US, DE). Defaults to US. This is the storefront its keywords and rankings are tracked in first; more can be added in the console. | |
| dry_run | No | Resolve the app and report what would happen without claiming it. Worth doing when the user gave a name rather than a URL, so they can confirm it is the right app before it counts against their plan. |
Output Schema
| Name | Required | Description |
|---|---|---|
| app | No | The app as it now appears in the account. Null under dry_run only if nothing resolved. |
| note | No | What happens next, when there is something the user should know. |
| status | Yes | `added` when it is now in the account, `already` when it was there before this call, `would_add` under dry_run. |
| country | Yes | The storefront it was added for. |
| apps_used | Yes | Own apps in the account after this call. |
| connected | Yes | Whether App Store Connect reports for this app. False for almost every freshly added app — say so, because get_performance will return nothing for it until it is connected in the console. |
| apps_limit | Yes | How many the plan allows. -1 means unlimited. |
| console_url | No | Where to open this app in the AppTail console. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnlyHint=false, openWorldHint=true, destructiveHint=false. The description goes further, disclosing the immediate side effect (rankings, reviews, competitors begin tracking at once), the billing/plan implication, and an out-of-band dependency (App Store Connect must be connected in the console, not here). That is exactly the behavioral context annotations cannot carry.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, front-loaded with the core action, then input forms, then side effects and the ASC limitation. No sentence is filler; each adds a distinct fact an agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Safety profile comes from annotations and return shape from the output schema, so the description's job is side effects and prerequisites — both covered. Nothing an agent needs to call this mutating, importing tool correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is already 100%, so the schema sets the floor at 3. The description adds cross-tool semantics by stating all three accepted forms are resolved and imported the same way search_apps resolves them, which tells the agent the input is a resolution target rather than a raw key.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Add an app to the account as one of the user's own') and pinpoints the accepted input forms (app_id, App Store URL, or name). It also distinguishes itself from the sibling search_apps by noting resolution happens 'the way search_apps does', so an agent can tell importing apart from merely searching.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear context: tracking starts immediately, and measured downloads/proceeds require connecting App Store Connect in the console, which 'cannot be done from here' — a real prerequisite. It stops short of an explicit when-not-to-use rule or naming an alternative tool for the pre-check (dry_run covers that only inside the schema).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
add_competitorsAdd CompetitorsAInspect
Track one or more rival apps against one of YOUR apps. Each competitor can be given as an Apptail app_id, an App Store URL, or just a name — names and URLs are resolved and imported the way search_apps does, so you do NOT need to look up an app_id first. Returns one outcome per item: what was added, what was already tracked, and what a plan limit refused.
| Name | Required | Description | Default |
|---|---|---|---|
| app_id | Yes | Apptail app id for one of YOUR apps — from get_account. The rivals are tracked FOR this app: competitor sets belong to an app, not to the account, so removing one later affects only this app. | |
| country | No | Storefront to resolve names in, ISO 3166-1 alpha-2 (e.g. US, DE). Omit and it is guessed from the script the name is written in. Ignored for items given as an app_id. | |
| dry_run | No | Resolve every item and report what would happen, without tracking anything. Use it when the user named apps ambiguously and you want to confirm you found the right ones before changing their account. | |
| competitors | Yes | The rivals, up to 20. Each item is an Apptail app_id, a full App Store URL, or an app name in any language. Send all of them in one call — the plan limit is checked per item, so a batch that runs out of room still adds what fits and says which items did not. |
Output Schema
| Name | Required | Description |
|---|---|---|
| app_id | Yes | The app these rivals are tracked against. |
| dry_run | Yes | True when nothing was written. Every outcome then reads `would_add`. |
| outcomes | Yes | One row per item, in the order they were sent. Read `status` per row: a batch is rarely all one thing, and reporting it as "done" when two of five were refused is the failure this shape exists to prevent. |
| remaining | No | Competitor slots left for this app on this plan after the call. Null when the plan is unlimited. Zero means the next add will be refused — say so rather than letting the user find out. |
| added_count | Yes | How many are now tracked that were not before. |
| requested_count | Yes | How many items were sent. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations cover safety (readOnly=false, destructive=false, openWorld=true), and the description adds meaningful behavior beyond them: per-item outcome reporting, plan-limit refusal semantics, and that a batch still adds what fits. Good added context on partial success and limit handling.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three sentences, front-loaded with the core action, then resolution behavior, then return semantics. Every sentence earns its place with no redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return values needn't be spelled out, yet the description still summarizes the per-item outcome structure. Combined with the workflow guidance on avoiding a prior app_id lookup, an agent has everything needed to invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents app_id, country, dry_run, and competitors. The description mostly reinforces that resolution accepts app_id/URL/name, which the schema already states; baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('track rival apps against one of YOUR apps') and distinguishes itself from get_competitors/remove_competitors by being the add side of competitor tracking. It also clarifies the relationship to search_apps for name/URL resolution.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly tells the agent it does NOT need to look up an app_id first because names/URLs resolve like search_apps, which is the key workflow decision. It does not name explicit when-not conditions versus siblings, but the context is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
add_keywordsAdd KeywordsAInspect
Start tracking search terms for one of your apps, in one storefront. Send terms, or keyword_ids for suggestions returned by discover_keywords — a suggestion MUST be added by id, because re-resolving its term can land on a different keyword row. Counts against the plan's per-app keyword limit; dry_run reports what would happen without writing anything. Terms already in the corpus come back under already_tracked rather than as an error — that is the state the caller asked for.
| Name | Required | Description | Default |
|---|---|---|---|
| app_id | Yes | Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id). Keywords are tracked per app. | |
| country | Yes | Storefront to track these terms in, ISO 3166-1 alpha-2 (e.g. US, GB, DE). Required and not defaulted: a term tracked in the wrong storefront is a wasted keyword slot. Ranks are per storefront — the same term ranks differently in each. | |
| dry_run | No | Report what would be added, and how many slots are left, without tracking anything. Nothing is written and nothing counts against the plan. | |
| keywords | No | Search terms to start tracking, as a user would type them into the App Store (e.g. ["fitness tracker", "workout app"]). Send the whole batch in one call rather than one term per call. Terms we have never seen are created. Every added term whose results are more than a day old is crawled straight away, and positions usually land within a few minutes: a get_keywords read made immediately after this call shows those terms as unmeasured, which means not crawled yet and not that the app is missing from the results. Either this or `keyword_ids` is required. | |
| keyword_ids | No | Apptail keyword ids to start tracking — the `keyword_id` on a discover_keywords row. Use this rather than the term whenever you have an id: a term is re-resolved and can land on a different keyword row than the one you were shown. |
Output Schema
| Name | Required | Description |
|---|---|---|
| added | Yes | Terms now being tracked. Under `dry_run` this is what would be added and nothing was written. |
| limit | Yes | Keywords this plan allows per app. -1 means unlimited. |
| app_id | Yes | The app these terms are tracked for. |
| country | Yes | The storefront they were tracked in. |
| dry_run | Yes | True when nothing was written. |
| skipped | Yes | Everything not added: the union of `already_tracked` and the terms that could not be resolved in that storefront or that ran out of plan room. Do not report this count as a failure without reading `already_tracked` first. |
| remaining | No | Keyword slots left for this app on this plan after the call. Null when the plan is unlimited. Zero means the next add is refused. |
| added_count | Yes | Size of `added`. |
| limit_reached | No | Set when the account's plan limit stopped the run before the whole list was added. A ceiling, not a fault — say what was capped rather than retrying. |
| skipped_count | Yes | Size of `skipped`, both reasons together. Subtract `already_tracked` before calling anything a problem. |
| already_tracked | Yes | The subset of `skipped` that the app was already tracking. **This is the end state that was asked for, not a fault** — a term already in the corpus needs no retry and no apology. What is in `skipped` but not here is the half worth mentioning: a term that does not exist in that storefront, or one the plan had no room for (see `limit_reached`). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only say it is a non-destructive write in an open world; the description goes much further by disclosing plan-limit consumption, the no-write guarantee of dry_run, the idempotent 'already_tracked' outcome, and the immediate-crawl/unmeasured read-after-write behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose is front-loaded in the first clause, and the remaining sentences each carry a distinct operational fact (id requirement, quota, dry_run, already_tracked). No filler despite the density.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 5-parameter mutation with an output schema already handling return shape, the description covers everything an agent needs: quota implications, batching guidance, id ambiguity, dry-run semantics, and the non-error duplicate case.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and its field descriptions are already detailed, so baseline is 3. The description still adds cross-cutting semantics the schema cannot: that a re-resolved term can land on a different keyword row, justifying the id-vs-term choice, though it adds little on app_id/country/dry_run.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource+scope: 'Start tracking search terms for one of your apps, in one storefront.' It is immediately distinguishable from siblings like discover_keywords (suggestion source) and remove_keywords (inverse operation).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit routing rules: send terms OR keyword_ids from discover_keywords, with the hard condition that a suggestion MUST be added by id. Also flags the plan limit, the dry_run escape hatch, and that already-tracked terms are not an error.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
discover_keywordsDiscover KeywordsAInspect
Terms an app could be tracking but is not, merged from every source and each labelled with where it came from: what tracked competitors rank for, what the store's similar apps rank for, what those competitors put in their own titles and subtitles, what the store's own mining surfaced, and — reading the listing cold — what the app's own title, subtitle and description suggest. The last is the only source that works on an app added minutes ago; listing is the only one that can surface a phrase nobody has ranked for yet, and it offers a phrase only when two or more rivals publish it and Apple reports somebody searching it. Pass contains to also match terms already in AppTail's database. Already-tracked and already-dismissed terms are never returned. This is a list of candidates, not a measure of coverage: for how much of a niche's vocabulary the account is missing, and which holes matter most, call get_landscape and read term_gap. Discovery can persist shared suggested keyword records, cache generated suggestions, and queue background popularity collection. It does not add those terms to account tracking.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max suggestions, most-searched first. Default 60, capped at 200. The answer always says how many there were. | |
| app_id | Yes | Apptail app id to suggest for. get_account for the user's own apps. Suggestions are derived from this app's listing, its rivals and its neighbours, so they are only meaningful for the app they were asked about. | |
| source | No | Which sources to read. Omit for all five that need no argument — `competitors`, `similar`, `listing`, `store`, `ai`. `corpus` is a text match against terms AppTail already holds and does nothing without `contains`, which is why "all" does not silently include it. An unknown name is refused rather than ignored. | |
| country | No | Storefront to suggest for (e.g. US, DE). Defaults to the app's primary storefront — which is only one of them: an app localized into several languages is searched for in several storefronts, so call once per entry in get_app's `listing_storefronts`. Discovery is per storefront and never a blend: a German term is not a translation of an English one — `spritverbrauch` beats `kraftstoffverbrauch` by a margin no dictionary would tell you. | |
| contains | No | Only terms containing this text. With `source: ["corpus"]` this is a search of AppTail's keyword database. Note it searches terms AppTail holds, not the App Store: a phrase nobody has ever tracked returns nothing, and to start tracking a brand-new term you pass it straight to add_keywords. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Suggestions returned. |
| total | Yes | Suggestions found before the cap. |
| app_id | Yes | The app these suggestions are for. |
| country | Yes | The storefront they apply to. |
| sources | Yes | Sources actually read. A source not listed here was not consulted. |
| app_name | No | Its name. |
| by_source | Yes | How many suggestions each source produced, keyed by source name; a source with none is absent. **Read this before reporting an empty answer.** Five of the six need something on file — rivals, the store's similar-apps list, a mining job that runs fortnightly — so zero beside `competitors` is a fact about the account, not about the niche, and the fix is add_competitors rather than a different question. |
| truncated | Yes | True when `count` is below `total`. Say the list is partial rather than presenting it as everything worth tracking. |
| console_url | No | The app's keyword screen in the console, where the same list can be reviewed and added by hand. |
| suggestions | Yes | The terms, most-searched first. Add them with add_keywords using their `keyword_id`, never their text. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnlyHint=false, openWorldHint=true, destructiveHint=false. The description goes further, disclosing the actual side effects — persisting shared suggested keyword records, caching suggestions, queueing background popularity collection — and clarifies the boundary that it does not modify account tracking.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Dense and front-loaded with the core definition and the get_landscape alternative before the caveats. It runs long for a single description and repeats a few points already in the schema, but nearly every sentence carries decision-relevant information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation. The description still covers all the complexity an agent needs: the five default sources, why corpus is excluded from 'all', the never-returned already-tracked/dismissed terms, and the per-storefront constraint.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: `listing` is the only source that can surface an unranked phrase and only when two-plus rivals publish it, `corpus` requires `contains`, and storefront selection is never a blend. Some of this duplicates the schema text, hence not a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource — unearthing candidate terms an app could track but is not — and enumerates the provenance of each candidate. It is clearly distinguishable from get_keywords (tracked terms) and get_landscape (coverage gaps), which it names explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Routes the agent to siblings by condition: get_landscape/`term_gap` for coverage measurement, add_keywords to actually track a new term, and it explicitly states discovery does NOT add terms to tracking. It also tells the agent to call once per storefront using get_app's `listing_storefronts`.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
edit_market_corpusEdit Market CorpusADestructiveInspect
Add terms to one storefront's corpus, or take terms out of it. Add for a term the corpus SHOULD hold and does not — a rival's brand, a phrase Apple has never measured the popularity of, the term a thin storefront is really about; a pinned term is kept through every rebuild and refreshed on the same 72-hour cadence as the rest. Remove for a term that does not belong: ANY term can go, and one the heuristic found is also kept out of future rebuilds rather than returning within 72 hours. This is NOT save_market: seeds are what the corpus is expanded FROM and changing one re-runs the whole heuristic over that storefront, where these two writes act on single terms in the result.
| Name | Required | Description | Default |
|---|---|---|---|
| add | No | Terms to pin, as somebody would type them into the App Store. Send the batch in one call. Terms already in the corpus come back under `already_present` rather than as an error. | |
| remove | No | Keyword ids to take out of the corpus — the `keyword_id` on ANY get_market corpus row. A term the heuristic found is also recorded as excluded so rebuilds do not re-derive it; `add` on the same term later clears that again. | |
| country | Yes | Which storefront's corpus, ISO 3166-1 alpha-2 (e.g. US, DE). Required and not defaulted: a corpus belongs to one storefront, and there is no market-wide term list to add to. | |
| market_id | Yes | Apptail market id — from list_markets or save_market. |
Output Schema
| Name | Required | Description |
|---|---|---|
| cap | Yes | The most terms this storefront may hold, from the account's plan. A market's storefronts each get their own — the cap is per storefront, not shared across the market. |
| terms | Yes | How many terms this storefront's corpus holds now. |
| pinned | Yes | Terms added to the corpus by this call, with `origin: "manual"`. A term Apptail has never seen is created and queued for crawling, so its ranks appear within minutes rather than immediately. |
| caveats | Yes | What the caller needs to hear before reporting success — that ranks are not in yet, or that the cap cut the list. |
| country | Yes | The storefront that was edited. A corpus belongs to one storefront and is never shared between them. |
| refused | Yes | Terms not added because the storefront is at `cap`. Unpin something or drop a storefront; the account's plan sets the number. |
| removed | Yes | Keyword ids taken out of the corpus by this call. |
| excluded | Yes | The subset of `removed` that will ALSO be kept out of every future rebuild. A term the heuristic found is re-derived from the seeds every 72 hours, so removing it has to be recorded outside the corpus to last; a hand-pinned term needs no such record because nothing re-adds it. Pinning a term with `add` clears it from this list again. |
| market_id | Yes | The market this corpus belongs to. |
| console_url | No | This market's corpus in the console. |
| not_removed | Yes | Ids that were sent to `remove` and are still in the corpus, each with the reason. |
| already_present | Yes | Terms that were already in this corpus, and where they came from. Not an error — it is the state the caller asked for, and a `competitor` term is NOT re-recorded as `manual`, because the origin says who put the term here. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already flag destructive=true, but the description adds substantial context beyond them: pinned terms survive every rebuild and refresh on a 72-hour cadence, removed heuristic terms are recorded as excluded so they don't re-derive, and a later `add` clears that exclusion. These are non-obvious side effects an agent cannot infer from the structured fields.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core add/remove action, then the behavioral details, then the sibling contrast. Dense and mostly waste-free, though the long single paragraph carries a few stacked clauses that could be broken out.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return values need not be described. The description covers the add/remove semantics, cadence, rebuild persistence, exclusion behavior, and the differentiating contrast with `save_market`, leaving nothing an agent needs in order to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description goes further by framing the intent of `add` (terms the corpus should hold) versus `remove` (terms that don't belong, and the persistent exclusion effect of removing a heuristic term), adding conceptual meaning on top of the already-detailed schema descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific paired verb (add / remove) operating on a single storefront's corpus, and explicitly names what it is not (`save_market`). An agent can distinguish it from the heuristic-rerunning seed tool without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives explicit when-to-use conditions: add for a term the corpus SHOULD hold but doesn't (rival brand, unmeasured phrase, thin storefront's real subject); remove for a term that does not belong, noting ANY term can go. It also contrasts with `save_market`, explaining that seeds re-run the heuristic while these writes act on single terms in the result.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
explain_periodExplain PeriodARead-onlyInspect
Return an account or owned-app summary for a requested period: available measured App Store Connect performance, storefront changes, tracked keyword movements, recorded signals, review volume and the last sent digest. Returns comparison data, coverage and caveats identifying unavailable sections. Reads account data without changing tracking or publishing content.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| app_id | No | Narrow to one of the account's own apps — get_account lists them. Omit for the whole portfolio, which excludes apps the owner has hidden. | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `last_month`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. | |
| compare | No | What to read the period against. `previous` (the default) is the period immediately before — for a calendar month, the calendar month before. `year_ago` is the same dates a year earlier, for comparison with the corresponding period in the previous year. | |
| country | No | Storefront for the keyword half only; the money is reported across all of them with a per-storefront split. Defaults to the app's primary storefront. Ranks only exist per storefront. |
Output Schema
| Name | Required | Description |
|---|---|---|
| digest | No | **What the customer was last mailed** — the weekly digest, sent Wednesdays, that leads with the measured week and at most three moves. Null when none has gone out yet. Anything here is breakfast reading, not news: say "as your digest said" rather than presenting it as a discovery, and lead with what happened since `sent_at`. |
| window | Yes | The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store Connect reported. Quote these dates, not the requested ones. |
| app_ids | Yes | The apps this covers. |
| caveats | Yes | Everything the reader has to hear before this reads as a complete picture: apps with no connection, a window shortened to the last day Apple reported, a corpus larger than the movers shown. **Pass these on.** A composed answer that drops them is the confident-and-wrong failure this tool exists to avoid. |
| reviews | Yes | Review arrivals — a flow, counted over the period. Not the backlog of unanswered reviews, which has no window and is on the console's reviews screen. |
| signals | Yes | What the product itself detected. A stored record, not a reconstruction — quote these sentences rather than re-deriving them from the numbers. |
| headline | Yes | The two or three sentences a person would open with, each carrying its own figures. Lead with these; everything below is the evidence for them. |
| keywords | No | The keyword half. Null when the account tracks no apps. |
| performance | No | The measured half. **Null when the account has no apps at all**, and present with null figures when it has apps but no App Store Connect connection — those are different answers, and the caveats say which. |
| comparison_window | No | What everything here is read against. Null when `compare` was `none`. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, destructiveHint=false, openWorldHint=false, so the safety profile is covered. The description still adds real behavioral context beyond that: output includes comparison data, coverage and caveats identifying unavailable sections, and it explicitly states reads do not change tracking or publishing content.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with what is returned, then a second sentence on return shape and read-only behavior. The enumerated section list is long but each item earns its place by defining the aggregate; no filler or throat-clearing.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description needn't detail return values, yet it usefully flags caveats and unavailable-section coverage. Safety is covered by annotations and all six parameters are fully documented in the schema, leaving only the missing when-to-use/routing guidance as a gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3; the schema already explains default windows, from/to precedence, app_id scoping, and the compare enum. The description adds little parameter-level meaning beyond restating that it covers a requested period and noting per-storefront money reporting (which the schema attributes to country).
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Specific verb (Return) on a specific resource (account or owned-app summary) scoped to a requested period, with the aggregated sections enumerated: performance, storefront changes, keyword movements, signals, review volume, last digest. The aggregate role clearly separates it from single-purpose siblings like get_performance, get_signals, and get_reviews without needing to name them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No explicit when-to-use statement, no exclusions, and no routing to alternatives such as get_performance or get_reviews when only one section is wanted. Usage is only implied by the aggregate framing plus the schema's detailed parameter guidance, which the agent must infer triggers this tool rather than a narrower sibling.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_accountGet AccountARead-onlyInspect
The account: its apps with Apptail ids, its plan and the limits that can refuse a write, App Store Connect health per app, how fresh the first-party data is, and the storefronts it focuses on. Call this first whenever the request is about "my app" or "my keywords" — every other tool needs an app_id, and the focus countries and hidden apps here are what keep your answer agreeing with what the customer sees in the console.
| Name | Required | Description | Default |
|---|---|---|---|
| include_hidden | No | Also return apps the owner has set aside. Default true, because they are still tracked and still answerable — each one is marked `hidden: true`, and they must be left out of portfolio totals. Set false for just the working portfolio. | |
| include_competitors | No | Also return the rival apps tracked under this account's apps, each marked `is_competitor`. Default false: including them makes any portfolio total wrong, and get_competitors is the tool that answers questions about them. |
Output Schema
| Name | Required | Description |
|---|---|---|
| apps | Yes | The account's apps. `app_id` here is what every other tool takes; the numeric id in an App Store URL is `apple_app_id` and will not work. |
| plan | Yes | The plan and its ceilings, read from the services that enforce them — so a refusal cannot disagree with this. Tell the user what was capped rather than working around a limit. |
| count | Yes | Number of apps returned. |
| connection | Yes | App Store Connect health for the account. A `failed` or `stale` connection looks exactly like a quiet fortnight in the numbers, which is why it is stated separately rather than left to be inferred. `none` means Apple has never been connected — impressions, page views, downloads and proceeds do not exist for this account and never did, so say that rather than reporting a decline; everything AppTail crawls itself (ranks, charts, reviews, ratings, releases, prices, markets) works exactly as it does for anybody else. `pending` is the same absence with a connection already on its way: repeat the wait, not the setup instructions. |
| data_through | No | Most recent day App Store Connect reported anywhere in the account. Per-app dates are on each app, and they differ — one healthy app keeps this recent while another has been silent for weeks. |
| focus_countries | Yes | The storefronts this account actually watches, in its own order. Every console screen narrows to these. Narrow your answers to them too unless the user asks otherwise — an answer across all 122 storefronts buries the one they wanted. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint/openWorldHint=false, so the safety profile is covered. The description adds real context beyond that: it discloses that plan limits can refuse a write, that first-party data has a freshness dimension, and that hidden apps must be excluded from portfolio totals to keep answers consistent with the customer's console.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the returned payload before the routing instruction. The first sentence is a dense enumeration but every item earns its place; no filler or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-shape explanation is unnecessary, yet the description still covers payload scope, the must-call-first routing role, and the consistency caveats. Nothing an agent needs to call this correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and both booleans are documented with defaults and rationale, so the schema carries the burden. The description mentions hidden apps and focus storefronts but adds no syntax, default, or format detail beyond what the schema already provides; baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (get the account) and enumerates the payload: apps with Apptail ids, plan/limits, ASC health, data freshness, focus storefronts. It explicitly distinguishes itself from every sibling by noting that all other tools require an app_id obtained here.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives an explicit routing rule — 'Call this first whenever the request is about "my app" or "my keywords"' — plus the reason (every other tool needs an app_id). It also flags that focus countries and hidden apps must be honored for answers to match the console, which is actionable invocation guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_appGet AppARead-onlyInspect
Everything AppTail holds about one app, at the depth you ask for. The base answer is the store listing in one storefront — the localised title and subtitle a shopper in country actually reads, the price and its currency, version, category, release dates, the publisher's ids. include adds sections: prices (what it costs in every storefront), ratings (every storefront's rating and vote count, pooled honestly, plus a daily series and per-storefront movement with history_days), charts (the charts it currently ranks in), popularity (organic reach), developer (the publisher and its portfolio), versions (release cadence) and screenshots. Works for ANY app in the store, not only the user's own. It does NOT return impressions, downloads or revenue — get_performance does, and only for the user's own connected apps.
| Name | Required | Description | Default |
|---|---|---|---|
| app_id | Yes | Apptail app id. get_account for the user's own apps, search_apps for any other app in the store. Not the numeric id in an App Store URL (that is apple_app_id). | |
| country | No | Storefront to read the listing in (e.g. US, GB, DE). Defaults to the app's primary storefront. Title, subtitle, price, rating and screenshots all differ per storefront, so this changes the answer rather than filtering it — the `name` you get back is the title as published *there*, which for a localised app is not the title in any other market. `ratings.overall`, `ratings.by_country`, `ratings.history` and the `prices` section ignore it — they are every storefront — and `charts` is returned for all of them regardless. | |
| include | No | Extra sections to read. Omit for the listing alone, which is the cheap answer. `ratings`, `charts` and `popularity` need a Starter or Pro plan and are refused by name without one — the rest are free. Ask for what the question needs and nothing else: each section is a query. | |
| history_days | No | With `include: ["ratings"]`, also return the rating and the vote count day by day, this many days back from today, **and how each storefront moved over that window** — `ratings_gained` and `rating_change` on every row of `ratings.by_country`. Capped at 365. Ask for it whenever the question is whether a rating is *moving* or *where* it is moving: an average slides for weeks before enough people write about it, and a cross-section cannot tell a market that has always been low from one that fell this month. Ignored without the `ratings` include. |
Output Schema
| Name | Required | Description |
|---|---|---|
| icon | No | Absolute URL of the app icon. |
| name | No | The app's title **as published in `country`** — the string a shopper in that storefront reads, which is also the string Apple indexes. It is not a translation of one canonical name: an app localised for Mexico has a different title there, and `get_app(country: "mx")` returning the US one is what makes an agent report a localised app as "not localized". Falls back to the app's canonical name when this storefront has not been crawled yet, and is null only while a freshly imported app is still being processed. |
| tier | No | Apptail activity tier for `country`: 1 = currently charting, 2 = shipped more than two updates in the last 180 days. |
| price | No | Download price in `country`, in that storefront's own currency and in major units — 9.99, never 999. 0 means free, which is what most apps are; `currency` names the money. Null when this storefront has no listing on file. |
| store | No | Which store the app belongs to. |
| app_id | Yes | Apptail app ID. This is the `app_id` every other tool expects. |
| charts | No | Charts the app currently ranks in, across all countries and categories. Present only with `include: ["charts"]`; empty means it charts nowhere. |
| prices | No | What the app costs to download, per storefront. Present only with `include: ["prices"]`. This is the download price alone: `has_inapps` says whether there is more to pay after that, and get_app does not return in-app purchase tiers. |
| country | No | Storefront the metrics below were read from. Defaults to `primary_country`. |
| is_mine | No | True when this is one of the asking account's own apps. Null when the call carried no account — get_top_charts answers without a token — which is "not known here", not "no". |
| ratings | No | Present only when `ratings` is in `include` — a section you did not ask for is absent from the answer entirely, which is not the same as it being empty. |
| version | No | Latest published version string. |
| currency | No | ISO code of the currency `price` is in, derived from `country`. A price without it is a number, not an amount — several storefronts print a "$" and mean different money. |
| includes | Yes | The sections actually read. A section absent from this list is absent from the answer because nobody asked for it — which is not the same as it being empty. |
| subtitle | No | The subtitle under the title in `country` — thirty characters Apple indexes as heavily as the title, and the field most competitors leave generic. Null when the storefront listing has none, which is itself worth saying: an empty subtitle is unspent keyword weight. |
| versions | No | Release history. Present only with `include: ["versions"]`. Cadence is the one competitor signal readable straight off the store: four releases in six weeks then eight months of nothing is a team that moved on. |
| bundle_id | No | Store bundle identifier, e.g. "com.burbn.instagram". |
| developer | No | The publisher and its portfolio — with the cap on `top_apps` declared. Present only with `include: ["developer"]`; null there when AppTail holds no profile for the publisher. |
| has_inapps | No | Whether the app offers in-app purchases. |
| popularity | No | Organic reach in `country`. Present only with `include: ["popularity"]`, and **null there when no storefront could be resolved** — a key that is present and null means "asked for, nothing to read", which an absent key does not. |
| seller_url | No | Publisher's marketing website. |
| console_url | No | Where to open this app in the AppTail console — the owner's own dashboard when `is_mine`, the research view otherwise. Null where this environment serves no console. End an answer about an app with it. |
| screenshots | No | Present only with `include: ["screenshots"]`, and null there when no storefront could be resolved. |
| support_url | No | Support page URL. |
| apple_app_id | No | Apple's own numeric app ID, as it appears in App Store URLs. Not interchangeable with `app_id`. |
| developer_id | No | Apptail developer ID. Pass this as `developer_id` to get_developer_info. Null if we hold no profile for the publisher. |
| rating_count | No | Number of ratings in `country`. |
| release_date | No | Date the app first appeared in the store (YYYY-MM-DD). |
| rating_average | No | Mean star rating (1-5) in `country`. Null if the app has no ratings there yet. |
| primary_country | No | Lowercase ISO country code of the app's main storefront, e.g. "us". |
| primary_category | No | Store category ID. Pass this as `category` to get_top_charts. |
| privacy_policy_url | No | Privacy policy URL. |
| store_developer_id | No | The store's own publisher ID, as it appears in store URLs. Not interchangeable with `developer_id`. |
| listing_storefronts | No | One storefront per language the listing is localized in, `primary_country` first, e.g. ["ru", "us"]. An app published in two languages is searched for in two storefronts; keyword discovery and tracking are per storefront, so cover each of these rather than only `primary_country`. |
| is_tracked_competitor | No | True when the account already tracks this app as a competitor of one of theirs. False on a high-ranking rival is a suggestion worth making: add_competitors starts watching it. Null when the call carried no account. |
| primary_category_name | No | Human-readable name of `primary_category`, e.g. "Health Fitness". |
| current_version_release_date | No | Date the current version shipped (YYYY-MM-DD). Useful as an update-cadence signal. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Offers substantial disclosure beyond the readOnly/openWorld/destructive annotations: plan gating ('ratings, charts and popularity need a Starter or Pro plan and are refused by name without one'), cost behavior ('each section is a query'), a hard cap (history_days capped at 365), and an explicit non-goal (no impressions, downloads or revenue). This is exactly the extra context annotations cannot carry.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the core answer, then layers includes, then exclusions and the alternative tool. Dense but every sentence carries routing, cost or scope information. Length is justified by a 4-parameter tool with a 7-value enum, though a few phrases ('pooled honestly') are decorative rather than informative.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given an output schema exists, return fields need not be explained, and the description fully covers what an agent needs: scope, storefront semantics, section gating, plan requirements, cost, caps, and the excluded performance metrics. Nothing needed to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so baseline is 3, but the description adds narrative meaning: `country` 'changes the answer rather than filtering it', and sections are grouped by what question they answer (e.g. `popularity` = organic reach, `versions` = release cadence). Some content overlaps the already-rich schema, preventing a 5, but it does more than echo structured fields.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Everything AppTail holds about one app') with an explicit scope boundary: the base answer is 'the store listing in one storefront', and `include` adds named sections. It also explicitly distinguishes itself from get_performance and clarifies it works for 'ANY app in the store, not only the user's own', which separates it from the user-scoped siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives explicit when-to-use routing: get_account for the user's own apps, search_apps for any other app, get_performance for impressions/downloads/revenue. It also states when to use each include section and, crucially, when to omit them ('omit for the listing alone, which is the cheap answer'), plus which sections need a paid plan.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_competitorsGet CompetitorsAInspect
List tracked competitor apps for one of your apps, with how many more the plan allows — and suggested: up to ten rivals the account does not track yet, ranked by how many of the app's own search terms they sit in the top ten for. Works on an app with no keywords: its listing is read for terms and the store is crawled for them. Read suggested_basis.terms_pending before calling a short list complete. Suggestions can persist shared keyword records and queue background crawls. Suggested apps are not added to your competitor watchlist.
| Name | Required | Description | Default |
|---|---|---|---|
| app_id | Yes | Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id). |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Number of competitors returned. |
| limit | Yes | How many competitors this plan allows per app. -1 means unlimited. |
| app_id | Yes | The app these rivals are tracked against. |
| remaining | No | Slots left before add_competitors is refused. Null when the plan is unlimited. |
| suggested | Yes | Rivals worth tracking that are not on the list, strongest evidence first: apps in the top ten of the terms this app competes on (tracked, already ranked for, or read off its listing when it has neither), never the account's own apps, never one already tracked, never one hidden from the Landscape. Offer them with their `sample_terms`; add with add_competitors only when asked. Empty when the app has nothing to derive from — say that rather than "no competitors exist". |
| competitors | Yes | Apps tracked as competitors of the given app. Empty if none are tracked yet. |
| suggested_basis | No | What the suggestions were derived from. Null when the app is not one of the account's own — a suggestion needs your terms to read. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With readOnlyHint=false, the description proactively discloses the non-obvious write side effects: suggestions can persist shared keyword records and queue background crawls. It also explains the crawl/listing fallback for apps without keywords — real behavioral context beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core purpose is front-loaded in the first clause, followed by the suggested-rivals explanation and the persistence/watchlist caveats. Dense but each sentence carries information; the opening sentence is long and stacks a secondary clause, costing a little clarity.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, yet the description still points at the specific field (suggested_basis.terms_pending) needed to interpret completeness, and covers side effects and watchlist behavior. Nothing essential to correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the single app_id parameter is fully documented in the schema, including the get_account source and the apple_app_id distinction. The description adds no parameter-level detail, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (list tracked competitor apps) plus the secondary output (suggested untracked rivals) and the ranking basis. An agent can distinguish this from add_competitors/remove_competitors without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear operational context: it works on apps with no keywords, tells you to read suggested_basis.terms_pending before treating a short list as complete, and clarifies that suggestions are not added to the watchlist. It does not explicitly name alternative siblings or state when-not to call it, so not a full 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_keywordsGet KeywordsARead-onlyInspect
The account's tracked keywords for an app — or across the whole portfolio — over any window: where each term ranked at the start and end, how far it moved, how many days it held and how many days it was measured, and optionally its day-by-day history, the same terms measured for named competitors, Search Visibility (a 0–100 score per storefront with the terms that moved it), and the terms two of your own apps are both on. Sorted worst movement first by default, so "what dropped" is at the top of a 300-term corpus — which also makes the returned rows a selection rather than a sample: movers and coverage are counted over the whole corpus and are what a summary comes from. Filter by contains to ask about one term by name. This is the tool for every question about tracked terms; it replaces get_tracked_keywords, get_keyword_positions and compare_keyword_positions.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| sort | No | `movement` (the default) puts terms that left the results first, then real drops by size — the top of the list is what to ask about. `rank` is best position first, `volume` is most searched (Apple popularity), `competition` is most crowded. **Every sort selects, so a capped list is never a sample**: a movement-sorted read of 15 terms returns 15 movers whatever the corpus did. Count from `movers` and `coverage`, never from the rows. | |
| limit | No | Max terms returned, after sorting. Default 100, capped at 300. The answer always says how many the corpus holds. | |
| scope | No | `app` (the default) reads one app's corpus and needs `app_id`. `portfolio` reads every app the account owns and reports each term under whichever of them ranks best for it — that is the scope `include: ["shared"]` needs, and hidden apps are excluded from it. | |
| app_id | No | Apptail app id for one of YOUR apps — get_account lists them. Required for `scope: "app"`. Not the numeric id in an App Store URL (that is apple_app_id). | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `30d`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. | |
| tag_id | No | Only keywords carrying this tag. Tag ids come back on the keywords in this tool's own output. Omit for the whole corpus. | |
| country | No | Storefront to read ranks in (e.g. US, GB). Defaults to the app's primary storefront. Ranks only exist per storefront — the same term ranks differently in each — so one call answers for one market. | |
| include | No | Extra blocks. `history` adds a day-by-day series per term, capped at 25 terms — pair it with `keyword_ids` for specific terms. `visibility` adds **Search Visibility** — the 0–100 answer to "is our ASO working here", its like-for-like change and the terms that moved it, plus a line per storefront — and each row's `points_change` and `brand`; also the daily top-N counts, which are over every term the app appeared for, not over this corpus. `shared` adds the terms two of your own apps are both on, and needs `scope: "portfolio"`. | |
| contains | No | Only terms whose text carries this substring, matched case-insensitively and applied before the sort and the cap. This is how to ask about a term by name — "how is `mpg` doing" — without looking up its id first, and the only way to see a term that sits mid-table by movement and so never reaches the top of a sorted list. | |
| keyword_ids | No | Only these terms, by Apptail keyword id — from this tool's own output or discover_keywords. Ids the account does not track are still answered when the scope is one app, because "how is my app doing for this term" is a real question; the answer says so. Omit for the whole corpus. | |
| compare_with | No | Apptail app ids to measure on these same terms — from get_competitors or search_apps. Rivals are measured on YOUR corpus, not theirs. At most 5 are used and the answer names which; send the ones that matter first. |
Output Schema
| Name | Required | Description |
|---|---|---|
| sort | Yes | The order the list came back in. |
| count | Yes | Terms returned. |
| scope | Yes | Which scope answered: `app` or `portfolio`. |
| total | Yes | Terms the corpus holds in this storefront. Larger than `count` when the list was capped. |
| movers | Yes | The shape of the window in four numbers, **counted over every term in the corpus, not over the rows returned**. This is the only honest basis for a sentence about the corpus, and it is the one to lead with: 8 fell of 103 is a different week from 80 of 103, and a list of 15 rows cannot tell you which you are in. |
| shared | Yes | Terms two of the account's own apps are both on, worst first, when `include: ["shared"]` was asked for at portfolio scope. Empty otherwise. Read `competing` before calling anything a conflict. |
| window | Yes | The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store Connect reported. Quote these dates, not the requested ones. |
| app_ids | Yes | The apps these ranks were read for. |
| caveats | Yes | Sentences the reader has to hear before the list means what it looks like it means — a capped list and what it was selected for, terms nobody crawled, ranks that are weeks old, a history returned for only some rows, an include that did not apply. They carry the corpus-wide numbers in them so they survive being quoted alone. Pass these on; do not summarise them away. |
| country | Yes | Storefront the ranks were measured in. |
| coverage | Yes | How much of this corpus was actually looked at in this window — the answer to "is this a drop or a gap in your crawling", which is otherwise unanswerable from ranks alone and has been guessed at wrongly. AppTail does not crawl every term every day, so sparse coverage on a few terms is normal; a corpus-wide `unmeasured` is not. |
| keywords | Yes | The terms, in the order `sort` asked for. **A selection, not a sample**: this is the top of a sorted list, so when `truncated` is true these rows are whatever the sort favoured — with the default sort, the worst movers and nothing else. Read them for which terms to ask about next; read `movers` and `coverage` for anything about the corpus. Empty when none are tracked in this storefront — track some with add_keywords. |
| truncated | Yes | True when `count` is below `total`. Say the list is partial rather than reporting it as the whole corpus. |
| visibility | No | When `include: ["visibility"]` was asked for: Search Visibility (`score`, `storefronts`) and the daily band counts. **The band counts are not over this corpus**: they count every term the app appeared for that day in this storefront, tracked or not, so they move with whatever anybody crawled and can disagree with the rows (a corpus whose rows show 27 terms in the top 10 can read `top10: 20`). Read them as reach across the store, never as the corpus's state. Each is the last day with a row — a state, not a sum — and null when nothing has been rolled up. The whole block is null when visibility was not asked for. |
| compared_app_ids | Yes | The competitor ids actually measured. Shorter than what was sent when the cap or an unknown id dropped one. |
| compare_truncated | Yes | True when competitor ids were dropped for exceeding the cap of 5. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover safety (readOnlyHint=true, destructiveHint=false, openWorldHint=true), so the description focuses on behavior instead: results are sorted worst-movement-first, rows are a *selection* not a sample, and `movers`/`coverage` are counted over the whole corpus. That selection-vs-sample disclosure materially changes how an agent should interpret output. It stops short of covering pagination/rate-limit behavior, so it is strong but not exhaustive.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose and the sibling-replacement statement are front-loaded, and nearly every sentence carries load-bearing detail (selection semantics, sort behavior, cap of 300). It is, however, a dense wall of prose with long em-dash-chained clauses that could be broken into scannable blocks, so it is efficient but not perfectly structured.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 13-parameter tool with an output schema present, the description covers the decision-relevant surface: scope/app_id coupling, include-block requirements, date-window resolution, and result-selection semantics. Return-value shape is left to the output schema, which is the correct division of labor.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description nevertheless adds interpretation beyond the schema, e.g. that `contains` is the only way to surface a mid-table term that sorting would never expose, and that `keyword_ids` still answers untracked terms under a single-app scope. These are meaning-level additions rather than restatements.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a precise verb+resource+scope ('the account's tracked keywords for an app — or across the whole portfolio — over any window') and enumerates exactly what each returned row contains (start/end rank, movement, days held, measured days). It explicitly names the three siblings it supersedes — get_tracked_keywords, get_keyword_positions, compare_keyword_positions — so an agent can distinguish it without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It states outright that this is the tool for every question about tracked terms and lists the alternatives it replaces. It also gives conditional guidance inline: use `contains` to ask about one term by name, `scope: portfolio` for cross-app terms, `include: history` for day-by-day data. When-to-use and what-not-to-use are both explicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_keyword_serpGet Keyword SerpAInspect
The App Store search results for one term in one storefront, on one day: who ranked, in what order, with their names, subtitles and ratings. This is how you find out WHO took the places an app lost — a rank that fell is a fact, and the results page on the day it fell is the reason. Works for ANY term in the store, tracked or not, known to us or not: pass keyword_id for a term you already have an id for, or keyword + country for words a user typed, which resolves the term and crawls the storefront live when what we hold is over a day old. Omit the date for the most recent crawl. A lookup can persist a new shared keyword record and queue a background crawl that updates stored search observations. It does not track the keyword for your account.
| Name | Required | Description | Default |
|---|---|---|---|
| date | No | The day to read, `YYYY-MM-DD`. Omit for the most recent crawl, which is the only mode that ever crawls live — naming a day is a question about the past and is answered from what was recorded. A day nobody crawled comes back empty with `crawled: false` rather than as an error; pick one from `crawled_days`, which the answer always returns. | |
| limit | No | How deep to read. Default 25, capped at 50. Below about 25 nobody is competing with you for the term. | |
| country | No | Storefront for `keyword`, e.g. "us", "jp". Required with `keyword` and ignored with `keyword_id`, because a keyword already names its storefront. There is no worldwide search results page — every App Store search happens in one country. | |
| keyword | No | The search term itself, for words a user typed rather than an id — "sleep tracker", "заметки". Needs `country`. A term nobody has ever looked up is created and the storefront is read live, so this answers for any search in the store and not only for terms Apptail already holds. Prefer `keyword_id` when you have one: it is the exact row, where words go through the store's own normalisation first. | |
| keyword_id | No | Apptail keyword id — from get_keywords or discover_keywords, or off a `rank.move` signal. The storefront is fixed by the keyword itself; a term is tracked per country. Give this OR `keyword` + `country`, not both. |
Output Schema
| Name | Required | Description |
|---|---|---|
| apps | Yes | The results in rank order, best first. `is_mine` marks the account's own apps and `is_tracked_competitor` marks rivals it already watches — a high-ranking app that is neither is worth suggesting they add. |
| date | No | The day actually read, which is the day asked for or the most recent crawl. Null when nothing has been crawled for this term. |
| count | Yes | Apps returned. |
| country | No | Storefront these results come from. Fixed by the keyword. |
| crawled | Yes | Whether that day had a crawl at all. False with an empty list means nobody measured, **not** that nobody ranked. |
| keyword | Yes | The search term itself. |
| refreshed | Yes | True when this call read the App Store rather than the database — a term looked up for the first time, or one whose last crawl was over a day old. False means these are stored results; `date` says which day they are from. Do not describe results as live unless this is true. |
| keyword_id | Yes | The term that was read. |
| popularity | No | Apple search popularity for the term, roughly 5-100. Higher means more searched. Read it before calling a position valuable. |
| console_url | No | Where this search is shown in the console, with the icons and screenshots this payload leaves out. |
| my_position | No | The best rank held here by one of the account's own apps, or null when none of them are in these results. Null is not -1: it means no app of theirs appears in the depth that was read, which is not the same as being measured and absent. |
| crawled_days | Yes | Days in the last 30 that were crawled, newest first. Name one in `date` to see how the results looked then — comparing two days is what shows who took a place. |
| results_count | No | How many apps the store returns for this term in total. The results below are the top of that. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Discloses behavior well beyond annotations: a lookup can persist a new shared keyword record and queue a background crawl that updates stored observations, and can crawl the storefront live when cached data is over a day old. This reconciles the apparent read-only framing with readOnlyHint=false, and adds the empty-result semantics (`crawled: false`, not an error).
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with what the tool returns, then input modes, then side effects. Some sentences are rhetorical rather than operational ('a rank that fell is a fact, and the results page on the day it fell is the reason'), but nearly every clause carries usable information for an agent.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return shape need not be explained. The description covers purpose, input mode selection, live-vs-recorded behavior, empty-day semantics, and the mutation side effect — complete for a 5-param open-world lookup.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds cross-parameter constraints the schema does not: keyword_id vs keyword+country are mutually exclusive, and country is ignored with keyword_id because the term already names its storefront.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb+resource+scope: 'The App Store search results for one term in one storefront, on one day: who ranked, in what order.' It is immediately distinguishable from siblings like get_keywords (tracked term metadata) and get_top_charts (chart rankings).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly routes between input modes ('pass `keyword_id` for a term you already have an id for, or `keyword` + `country` for words a user typed'), tells when to omit the date, and names the alternative sources of a keyword_id (get_keywords, discover_keywords). It also states a negative: 'It does not track the keyword for your account.'
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_landscapeGet LandscapeARead-onlyInspect
Who the App Store shows beside one of your apps for the terms it competes on, in one storefront: every app in the niche with how many of your terms it ranks for and owns the top 10 for, its ratings, its review flow and its size, plus which apps arrived or fell out this window — and term_gap, how much of the niche's own vocabulary you do not track yet, with the biggest holes ready for add_keywords. This is the niche, not your watchlist: most rows are apps nobody added, and get_competitors answers the other question.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| app_id | Yes | Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id). | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `30d`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. | |
| country | No | Storefront to read the niche in (e.g. US, GB). A niche exists per storefront and is never blended across them. Defaults to the first of the account's focus_countries this app is actually tracked in, then the app's primary storefront. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Members returned. |
| terms | Yes | How many terms defined this niche: the account's tracked terms for this app in this storefront, plus the terms the app already holds a top-10 position for. Every `terms` and `top10_terms` figure below is out of this. |
| total | Yes | Members the niche actually holds. Larger than `count` means the list was cut. |
| app_id | Yes | The app whose niche this is. |
| window | Yes | The window used, and what the change figures are measured against. |
| caveats | Yes | Sentences the reader has to hear before the numbers mean what they look like they mean — a cut list, hidden rows, an unmeasurable gap, modelled sizes. Pass these on; do not summarise them away. |
| country | Yes | The storefront actually read, which may not be the one asked for. |
| members | Yes | The niche, most of the account's terms owned first. `entered` and `left` mark this window's movers. |
| app_name | No | Its name. |
| term_gap | Yes | How much of the niche's vocabulary the account is not watching. |
| truncated | Yes | True when `count` is less than `total`. Say the list is partial rather than presenting it as the whole niche. |
| visibility | No | The basis of each member's `visibility`. Null when it could not be read just now — then every member's `visibility` is null too, which is unknown, not zero. |
| console_url | No | This landscape in the console, in this storefront. |
| hidden_count | Yes | Apps the account owner has hidden from this landscape in the console. They are excluded from `members`, from the movers and from `term_gap` alike. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, openWorldHint=true and destructiveHint=false, so the safety profile is covered. The description adds real behavioral context on top: the niche is storefront-scoped and not blended, rows include apps 'nobody added', and the window determines which apps 'arrived or fell out'. It omits any row limit or rate-limit note, which is the only gap.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the core purpose before the term_gap detail and the sibling routing. It is dense and em-dash-heavy, making it slightly hard to scan, but every clause carries distinct information and nothing is padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be re-explained, and the description covers scope, sibling routing and follow-up actions. The only thing an agent might still wonder — how many rows come back or whether results are capped — is left unstated, keeping this just short of full completeness.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so every parameter (to, from, app_id, period, country) is already documented in the schema with formats, defaults and override rules. The description only implicitly touches country ('one storefront') and period ('this window'), adding no syntax or meaning beyond what the schema provides. Baseline 3 is correct when the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific resource (the niche landscape around one app in one storefront) and enumerates exactly what comes back: per-app ranking counts, top-10 ownership, ratings, review flow, size, arrivals/departures, plus term_gap. It explicitly separates itself from get_competitors by stating 'This is the niche, not your watchlist,' so an agent can distinguish the two without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It names the alternative (get_competitors) and the condition that selects it ('answers the other question'), and routes the follow-up work to add_keywords for the biggest term_gap holes. It also states the scope constraint that governs use — one storefront, never blended — which tells the agent this is a per-market read.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_marketGet MarketAInspect
Read iOS App Store niche data by saved market_id or by query and countries. Returns storefront competition, search visibility, app membership and optional corpus or movement data. Phrase research can create shared keyword records and persist crawled search observations; it does not save an account market. Downloads and revenue are labelled third-party rolling 30-day estimates. Reports per-storefront values, coverage and caveats.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| query | No | A phrase to resolve into a market right now, e.g. "AI calorie tracking". Requires `countries`. Can persist shared keywords and search observations. No market is saved to your account — call save_market with the same phrase to keep it. | |
| store | No | Read only this one storefront of a saved market. Omit for every storefront compared, which is where `overlap` and the contest range come from. | |
| app_id | No | Scopes `include: ["app_terms"]` to one app — an Apptail app id from `members`, `search_apps` or `get_landscape`. This is the audit trail for membership: it returns every (term, storefront) place that app holds in this market, how many days it held each, and which clause of the rule put it in `apps` or in `fringe`. Reach for it when the user disagrees with a market's membership, or asks why a particular app is or is not in the niche. | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `30d`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. | |
| include | No | Extra sections, each costing a query: `members` (the leaderboard), `corpus` (every term with why it is in), `movement` (who arrived and who left), `app_terms` (which terms ONE app holds here and whether that makes it a competitor — needs `app_id`). Ask for what the question needs. | |
| countries | No | Storefronts to ask the question in, ISO 3166-1 alpha-2 (e.g. ["US","GB","DE"]). Required with `query`. Each storefront gets its own corpus seeded from the same phrase; that is deliberate, and a phrase nobody searches in a storefront comes back with a thin corpus, which is itself the finding. | |
| market_id | No | Apptail market id for a market this account has saved — from list_markets. A saved market is a pure read: no crawling, sub-second. Send this OR `query`, not both. | |
| budget_seconds | No | How long a live `query` may spend crawling, shared across ALL storefronts rather than each. Default 5, max 30. It is rarely the binding limit — at most 12 terms are crawled per call whatever the clock says. | |
| new_within_days | No | Keep only apps FIRST RELEASED inside this many days — "what has launched into this niche lately". Applied to every app ranking for the corpus before the list is cut, so these are the new apps in the market and not the new apps among its leaders; a young app is rarely a leader yet, so filtering the leaderboard itself would answer nothing. Affects `members` only. `share` stays a share of the WHOLE market, which is the point of it. Not the same question as `movement`: that is who started ranking here, which is mostly apps that are not new at all. |
Output Schema
| Name | Required | Description |
|---|---|---|
| apps | Yes | UNION: apps COMPETING in at least one storefront, counted ONCE however many they rank in. Competing means a top-10 place on at least one of this market's terms AND a top-50 place on at least two of them — one term is a coincidence and a place nobody scrolls to is not competition. Never the sum of the per-storefront counts: one app in three storefronts is one app. A FLOOR rather than a count when `apps_capped` is true. Everything else the store returned is in `fringe`. |
| left | No | Apps that fell out of every storefront they held. include: ["movement"]. |
| name | No | What the market is called. Null for an unsaved reading. |
| slug | No | Its address in the console. |
| terms | Yes | SUM of the per-storefront corpora. The corpora are disjoint by construction — a term belongs to one storefront — so nothing is counted twice, and this is exactly what the market costs to crawl. |
| corpus | No | Every term, with its storefront and why it is in the corpus. Present only with include: ["corpus"]. |
| fringe | Yes | Apps that rank somewhere in this market's search results and are NOT competing in it: one term only, or no top-10 place anywhere. Typically several times `apps` — measured on a nine-storefront market, 578 competing against 2,001 fringe. They are real placements and worth naming as context (a store that seats an app beside yours), never as rivals. Counted once and never double-counted with `apps`: an app competing in one storefront and fringe in another is competing. |
| spread | No | open | contested | locked | mixed. `mixed` is the finding rather than a fudge: a niche locked in the US and open in Germany is the answer to "where should I launch this first". Storefronts in the same band report that band. |
| status | Yes | draft | building | ready | failed. A market is `building` while ANY of its storefronts is — never report it ready because most of them landed. `ready` beats `failed`, so a market can read `ready` and still hold a storefront whose build died: check each storefront's `failed_at` before quoting its figures. |
| window | Yes | The window read, and what `newcomers`, `departures` and the change figures are measured against. |
| caveats | Yes | Sentences the reader has to hear before the numbers mean what they look like they mean. Pass them on; do not summarise them away. |
| entered | No | Apps that arrived somewhere in this market this window. include: ["movement"]. |
| members | No | The leaderboard, ordered by rating count — the one size figure we measure for every app. Present only with include: ["members"]. |
| overlap | No | Of the apps holding a top-10 place in ANY storefront, the share holding one in EVERY storefront, 0-100. High means one leaderboard travels and a newcomer meets it wherever they launch; low means each storefront is its own contest, which is where an unowned storefront hides. NULL for a single-storefront market — it is not a question one place can answer. |
| built_at | No | The OLDEST of the storefronts' build times: a market is only as fresh as its worst. |
| app_terms | No | Every (term, storefront) place that app holds here, best place first — the evidence `app_verdict` was taken over. Present only with include: ["app_terms"]. |
| market_id | No | Apptail market id, or NULL when this reading was resolved from a phrase and not saved. Pass it to save_market to keep it, or to get_market to read it again for free. |
| newcomers | No | Distinct apps that arrived in at least one storefront this window. Arriving in `de` while already ranking in `us` counts as arriving in this market. **NULL means no storefront had a previous window to compare against** — nothing of this corpus was crawled then, which is the normal state of a market built inside the window. Never read null as 0, and never report a new market's whole membership as newcomers. |
| truncated | Yes | True when any list above was cut. Say so rather than presenting a partial list as the whole thing. |
| departures | No | Distinct apps that fell out of every storefront they held. Null on the same terms as `newcomers`. |
| app_verdict | No | Whether the app named by `app_id` is competing in this market, and which clause of the rule decided. Present only with include: ["app_terms"]. |
| apps_capped | Yes | True when the census stopped at its depth before it ran out of apps. `apps`, `newcomers`, `departures`, `downloads_30d` and `revenue_30d` are then floors, because all of them are summed over the counted set. Say "at least" rather than reporting the number as a total. |
| console_url | No | This market in the console. |
| contest_low | No | The least contested storefront's share of top-10 slots held by its top three, 0-100. |
| revenue_30d | No | ESTIMATE, same basis as downloads_30d. |
| stale_terms | Yes | Corpus terms across the market past their 72-hour refresh. |
| storefronts | Yes | One entry per storefront, and the only level at which contest, demand and depth exist. Compare these rather than averaging them. |
| contest_high | No | The most contested storefront's. A RANGE, deliberately: there is no market-level contest to average. |
| corpus_total | No | Terms the market holds. Larger than the list means it was cut. |
| size_covered | Yes | How many of the `apps` above carry an estimate at all, INCLUDING the ones the vendor would only bound as "under 1,000" / "under 5,000". Those contribute nothing to the sums, so a market with many small apps reports a size that is a floor. A total over 40 of 291 apps is a different statement from one over 280 — say which when quoting the size. |
| downloads_30d | No | ESTIMATE. A third-party model, worldwide and 30-day, summed over the UNION with each app counted once. Adding the per-storefront totals would count the same worldwide figure once per storefront and be wrong by roughly the storefront count. |
| members_total | No | Members the market actually holds. Larger than the list means it was cut. |
| pending_terms | Yes | Terms in the corpus that could not be crawled inside the budget. Their ranks are missing from this reading, so it is a floor. Saving the market and asking again returns a deeper answer. |
| app_terms_total | No | Places the app actually holds. Larger than the list means it was cut. |
| storefront_count | Yes | How many places this question is asked in. |
| pending_storefronts | Yes | Storefronts that ran out of budget before they were read at all. Name them; a partial answer that reads as complete is the failure this field exists to prevent. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only declare readOnlyHint=false and openWorldHint=true; the description goes further by disclosing the concrete side effects of phrase research (creating shared keyword records, persisting crawled search observations) and, critically, what it does NOT persist. It also flags data provenance ("third-party rolling 30-day estimates") and that coverage/caveats are reported, which an agent needs to interpret results honestly.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four sentences, front-loaded with purpose and invocation mode, then returns, then side effects, then data caveats — a logical order with no filler. Some sentences are dense, but each carries distinct information that is not repeated elsewhere in structured fields.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present and 100% schema coverage across 11 parameters, the description need not enumerate returns or per-parameter syntax; it supplies the high-level shape plus the non-obvious side-effect and provenance context. What is missing is only peripheral (auth/permission expectations, rate-limit behavior), which the annotations and schema largely imply.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents all 11 parameters in exceptional detail (including the market_id-vs-query exclusivity and period semantics). The description restates the two modes and mentions optional corpus/movement data but adds no syntax or format detail beyond what the schema provides, so baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Read iOS App Store niche data") and names both invocation modes (saved market_id vs. query + countries). It explicitly distinguishes itself from the write sibling by noting it "does not save an account market," so an agent can tell it apart from save_market without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The two calling modes give implied usage context, and the caveat about not saving a market hints at save_market as the alternative. However, no sibling is named as an explicit alternative and there is no when-not-to-use guidance (e.g., vs. get_landscape or search_apps), so routing is left largely to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_performanceGet PerformanceARead-onlyInspect
Read measured App Store Connect impressions, store views, downloads, sales and proceeds for apps connected to the authenticated account. Supports an owned app or portfolio, documented time windows, period comparisons and breakdowns by storefront or traffic source. Returns the actual reporting window, connection coverage and caveats. Does not provide private analytics for apps outside the account.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| grain | No | Bucket size for the series. Omit and it is chosen to fit the window — a day for a month, a week for a year. A grain too fine or too coarse for the window is ignored rather than refused. | |
| scope | No | `portfolio` (the default) sums every app the account owns; `app` reports one, and then `app_id` is required. Hidden apps are in the portfolio sum only if the account has not hidden them — they are excluded, matching what the console shows. | |
| split | No | Break the totals down. `country` gives one row per storefront Apple named; `source` gives Apple's own attribution — App Store search, browse, app and web referrers. `source` reports the attribution associated with measured downloads. Rows need not add up to the total; the caveats say so. | |
| app_id | No | Apptail app id, required when `scope` is `app`. Must be one of the user's own apps — get_account lists them. Measured analytics require an authorised App Store Connect connection for the account. | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `30d`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. | |
| compare | No | What to read the window against. `previous` (the default) is the period immediately before — for a calendar month that is the calendar month before, not the same number of days. `year_ago` is the same dates a year earlier, for comparison with the corresponding period in the previous year. | |
| include_apps | No | On `portfolio` scope, also return one row per app so a portfolio move can be attributed to the app that caused it. Default false. |
Output Schema
| Name | Required | Description |
|---|---|---|
| apps | Yes | One row per app, when `include_apps` was set on a portfolio call. Empty otherwise. |
| grain | Yes | Bucket size the series came back at. |
| scope | Yes | Which scope answered: `portfolio` or `app`. |
| split | Yes | Which breakdown was returned: `none`, `country` or `source`. |
| series | Yes | The same metrics bucketed across the window, oldest first. Empty when nothing is connected. |
| totals | Yes | Window totals, one entry per metric, each carrying where it came from and what it may be compared with. |
| window | Yes | The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store Connect reported. Quote these dates, not the requested ones. |
| app_ids | Yes | The apps behind these figures. |
| caveats | Yes | Sentences the reader has to hear before the numbers mean what they look like they mean — apps left out, a window shortened, a breakdown that does not sum. Pass these on; do not summarise them away. |
| coverage | Yes | What the totals do and do not cover. Quote this whenever `connected` is below `apps`. |
| truncated | Yes | True when `split_rows` is shorter than `split_total`. Say so rather than presenting the returned rows as the whole breakdown. |
| split_rows | Yes | The breakdown, biggest by downloads first. Empty when `split` is `none`. |
| split_total | Yes | How many rows the breakdown has in total, which can exceed the number returned. |
| data_through | No | Last day App Store Connect has reported across these apps. Apple lands analytics one to two days late, so this is usually a day or two behind today; a gap of a week or more is a broken connection rather than a lag. |
| requested_window | Yes | What the call asked for, when that differs from what was read. Compare with `window` before quoting a total as covering the period the user named. |
| comparison_window | No | The window `previous` on each metric was measured over. Null when `compare` was `none`. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations cover the safety profile (readOnlyHint, destructiveHint=false, openWorldHint=false), and the description adds real behavioral context beyond them: it returns the actual reporting window, connection coverage, and caveats, and it discloses that hidden apps are excluded from the portfolio sum to match the console. It does not discuss rate limits or auth requirements beyond connection mention.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences: what it reads, what it supports, what it returns, and what it excludes. Front-loaded with the resource set and free of filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 9-parameter read tool with an output schema, this covers scope, exclusions, comparison/split capabilities, and even return caveats. Nothing material to correct invocation is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% with unusually rich per-parameter documentation (grain fallback behavior, period/compare semantics, split row caveats), so the schema already carries the semantics. The description only gestures at 'documented time windows', 'period comparisons' and 'breakdowns by storefront or traffic source', which adds little beyond what the schema says.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (read) and concrete resource set (impressions, store views, downloads, sales, proceeds) scoped to App Store Connect apps on the authenticated account. It is clearly distinct from mutation siblings like add_app or reply_to_reviews, though it does not explicitly contrast itself with the closest read siblings (get_signals, get_app).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear context: owned app or portfolio, documented time windows, comparisons, and breakdowns, plus an explicit exclusion ('Does not provide private analytics for apps outside the account'). It points at get_account for obtaining app ids. It stops short of naming a specific alternative tool for other kinds of reporting.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_reviewsGet ReviewsARead-onlyInspect
Reviews for any app over a window, filtered by storefront, star rating, text, or whether the developer has replied — plus the shape of the period: how many arrived per bucket, what they averaged, and where the app's pooled store rating stood while they did. Reviews exist for every app whether or not App Store Connect is connected, because they are read from the public store. This is the tool for "what are people complaining about": filter it and read the reviews yourself rather than asking for a summary. Every review says whether it can be answered (can_reply) and where an existing reply stands (reply_state); reply_to_reviews is what answers them.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| limit | No | Max reviews, newest first. Default 25, capped at 200. The answer always says how many matched. | |
| app_id | Yes | Apptail app id. get_account for the user's own apps, search_apps for any other. Reviews are readable for any app in the store, including competitors. | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `30d`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. **This tool also takes `all`**, which is the app's whole review history and the only period that reaches a backlog older than three years. Reviews are kept for the life of the app and the console's own reviews screen has no date bound at all, so the ceiling above is about first-party analytics and not about these rows. Send `all` for "what have we never answered" and for anything asking about a review from years ago; the trend then buckets by quarter or year. | |
| rating | No | Only these star ratings, e.g. `[1, 2]` for the critical ones. Omit for all five. The `summary` counts are deliberately NOT filtered by this, so a call asking only for one-stars still learns how many reviews the period actually held. | |
| country | No | Only reviews from this storefront (e.g. de). Omit for every storefront, which is usually right — one storefront's reviews are a thin sample. Send it when `ratings.by_country` on get_app has pointed at a market, which is the follow-up that finds out why it is low. | |
| contains | No | Only reviews whose title or body contains this text, case-insensitively. Title as well as body, because a complaint is as often the headline as the paragraph. This is how you check a hypothesis — "crash", "subscription", "ads" — rather than reading everything. | |
| unanswered | No | Only reviews the developer has never replied to. False or omitted for all of them. Combined with `rating: [1, 2]` this is the work queue the console leads with — and **a backlog has no window**, so send `period: "all"` when you mean all of it. `summary.unanswered` is the whole-history count either way, and a list that is smaller than it is a windowed list, not a shorter backlog. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Reviews returned. |
| total | Yes | Reviews matching the filters in this window. Larger than `count` when the list was capped. |
| trend | Yes | The shape of the period. Compare `average_rating` with `store_rating` bucket by bucket: the first is the mood of what arrived, the second is the running total the store ranks on, and the gap between them is the finding. |
| app_id | Yes | The app the reviews belong to. |
| window | Yes | The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store Connect reported. Quote these dates, not the requested ones. |
| country | No | Storefront the list was narrowed to, or null for every storefront. |
| replies | Yes | Whether replying is available to this account at all. Read it together with `can_reply` on each review: this is about the account's key, that is about the individual review having an App Store Connect id. Both must be true. |
| reviews | Yes | The matching reviews, newest first, in the language they were written in. Read them; do not ask for a summary you can produce better yourself. |
| summary | Yes | The period in figures. `arrived*`, `critical*` and `average_rating*` are flows *in the window*; `all_time`, `unanswered` and `store_rating*` are the app's standing position and ignore it. Mixing the two is how "we got 12 reviews and have 400" becomes one number. |
| app_name | No | Its name. |
| filtered | Yes | True when any of `country`, `rating`, `contains` or `unanswered` narrowed the list. Worth saying out loud: "no reviews" under a filter is not "no reviews". |
| truncated | Yes | True when `count` is below `total`. Say the list is a sample rather than presenting it as everything that was written. |
| console_url | No | The app's Reviews screen in the console. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Adds meaningful context beyond the readOnly/openWorld annotations: reviews are read from the public store and exist for every app regardless of App Store Connect connection, each review carries can_reply and reply_state, and summary counts are deliberately not filtered by rating. Return value specifics are partly delegated to the output schema, so not a full 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with purpose, then progressively deeper detail. Dense but each sentence carries routing or behavioral value; the period/can_reply/reply_state sentence and a few em-dash asides are slightly overstuffed, keeping it from a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given a 9-parameter tool with a live output schema and readOnly annotations, the description covers everything an agent needs: scope, cross-tool routing, the all-period ceiling caveat, and the distinction between windowed lists and whole-history summary counts.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the schema itself is unusually rich, so the baseline is 3, but the description adds selection rationale beyond it: filters map to use cases ('check a hypothesis — crash, subscription, ads'), country is a follow-up to ratings.by_country on get_app, and unanswered + rating [1,2] is the work queue.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Reviews for any app over a window') plus the filterable dimensions and the aggregate shape returned. It distinguishes itself from siblings by naming get_app, search_apps, get_account and reply_to_reviews and clarifying the read-yourself framing.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit routing: 'this is the tool for "what are people complaining about"... rather than asking for a summary', 'get_account for the user's own apps, search_apps for any other', and '`reply_to_reviews` is what answers them'. It also gives when-not guidance for the country filter (one storefront is a thin sample).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_signalsGet SignalsARead-onlyInspect
What actually happened, from the record the product keeps: rank moves on tracked terms, chart entries, review spikes, competitor releases and price changes, plus — for the user's own apps only — week-over-week traffic and conversion shifts, download collapses, new reviews and rating moves. Each carries the two numbers behind it and the sentence that states the finding. This is a stored table read, not a reconstruction, so it is both cheaper and more reliable than diffing rank histories yourself. Covers the user's own apps and the competitors tracked under them; a rival's finding never asks_action.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| limit | No | Max findings, newest first. Default 50, capped at 200. The answer always says how many the window actually holds. | |
| types | No | Only these kinds of finding. Omit for all of them. An unknown name is refused rather than ignored, so a filtered answer is never returned as though it were unfiltered. | |
| app_id | No | Narrow to one of the user's own apps — get_account lists them. Findings about the competitors tracked under it are included, because a rival taking your places is something that happened to you. Omit for the whole portfolio. | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `30d`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. | |
| include_sent | No | Mark the findings that already went out in an alert digest the user has read. Default false. Set it whenever you are about to summarise a period for someone: without it you cannot tell news from something they read at breakfast, and repeating the second as the first is how an assistant stops being believed. | |
| min_severity | No | Floor on how loudly a finding speaks. Default `neutral`, which is everything. `warning` drops a rival shipping a point release, which is worth knowing and not worth acting on. It is NOT the same as "what needs my attention" — read `asks_action` on each finding for that: a competitor taking a run of 1–2★ reviews is a warning about somebody else's week and asks nothing of this user. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Findings returned. |
| total | Yes | Findings the window holds in total. Larger than `count` when the list was capped. |
| digest | No | What the product has already told this person, when `include_sent` was set. Null otherwise, which means "not asked" rather than "never mailed". |
| window | Yes | The window actually read, which is not always the one asked for: an inverted range is swapped, a range past three years is shortened, and first-party figures are pulled back to the last day App Store Connect reported. Quote these dates, not the requested ones. |
| app_ids | Yes | The apps of the user's these findings hang under. |
| by_type | Yes | How many of each kind the window holds — the shape of the period, counted over everything rather than over the capped list. A key is absent when there were none. |
| signals | Yes | The findings, newest first. Each carries the sentence that states it and the two numbers behind it. Empty is a real answer: it means nothing crossed a detector's threshold, which is the only way a threshold means anything. |
| truncated | Yes | True when `count` is below `total`. Say the list is partial rather than presenting it as everything that happened. |
| by_severity | Yes | The severities among the findings returned. Counted over the returned rows, not the window, so it describes the list in hand. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish readOnly/non-destructive/closed-world, so the bar is lower, yet the description adds real context: it is a stored table read (cheap, reliable), it covers the user's own apps plus competitors tracked under them, and a rival's finding never asks_action. It does not discuss rate limits or pagination beyond the schema-stated cap. Solid added value, not exhaustive.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The scope and the cost/reliability rationale are front-loaded, and the enumeration of finding types is dense but useful. A few sentences carry rhetorical flourish ('how an assistant stops being believed') that is informative but slightly padded for a tool description. Still efficient overall.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return-value detail is not required, and the description covers scope (own apps + tracked competitors), cost profile, filtering behavior and the meaning of severity vs asks_action. Nothing an agent needs to invoke it is missing, though it could be tighter about interaction with sibling tools.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so every one of the 8 parameters is already fully documented with defaults, formats and precedence rules. The description adds no per-parameter syntax or meaning beyond that, so the baseline 3 applies — the schema does the heavy lifting.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states concretely what the tool returns: stored findings such as rank moves, chart entries, review spikes, competitor releases, price changes, plus own-app traffic/conversion shifts. That is far more specific than the tool name and lets an agent distinguish it from performance/history tooling. It does not, however, name a sibling tool it should be preferred over, so 4 rather than 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives a clear selection rationale against a competing approach ('This is a stored table read, not a reconstruction, so it is both cheaper and more reliable than diffing rank histories yourself'). The include_sent parameter note also supplies conditional guidance ('Set it whenever you are about to summarise a period'). It stops short of explicit when-not-to-use or sibling routing, so not a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_top_chartsGet Top ChartsCRead-onlyInspect
Get top app charts (free, paid, or grossing) for a store, country, and category.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | How deep to read the chart. Default 25, capped at 100. | |
| store | No | Only the App Store is covered. Google Play is in the data model but nothing is crawled for it, so omit this. | |
| country | No | Storefront to read the chart for (e.g. US, GB, JP). Defaults to US. An unrecognised code falls back to US rather than failing — the reply echoes the storefront actually used, so check it. | |
| category | No | Apple category id as a number, e.g. 6014 Games, 6015 Finance, 6017 Education, 6023 Food & Drink, 6013 Health & Fitness, 6012 Lifestyle. Omit for the overall chart across all categories. An id we do not recognise is ignored and you get the overall chart. | |
| chart_type | No | Which chart. Default free. Use grossing for revenue questions — top free ranks by downloads and says nothing about money. An unrecognised value falls back to free and the reply says which chart it read. |
Output Schema
| Name | Required | Description |
|---|---|---|
| apps | Yes | Chart entries, rank 1 first. Empty when no chart data exists for this country/category combination. |
| count | Yes | Number of entries returned. |
| country | Yes | Storefront the chart was read from. |
| chart_type | Yes | Which chart was read. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, openWorldHint=true, destructiveHint=false, so safety is covered. The description itself adds nothing behavioral — the genuinely useful traits (fallback to US, unrecognised values silently ignored, echoed storefront) live in the schema, not here.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with no waste. It is tight, though arguably too terse for a five-parameter tool with non-obvious fallback semantics.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return values need not be explained, and annotations cover the safety profile. The remaining gap is usage/routing guidance, which the description does not supply for a tool with many overlapping read siblings.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and each parameter is documented in detail, including defaults, caps, and fallback behavior. The description merely names store/country/category without adding syntax or format meaning beyond the schema, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Clear verb+resource: 'Get top app charts' with the chart-type dimension spelled out. It does not distinguish itself from siblings like get_landscape or get_performance, which an agent might reasonably confuse for chart/ranking data.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No when-to-use guidance, no prerequisites, no named alternatives. An agent is left to infer that this is the tool for store ranking questions rather than, say, get_landscape or get_app.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_marketsList MarketsARead-onlyInspect
List markets saved by the authenticated account, including market ids, storefronts, competition ranges, app counts and observed changes for the requested period. Reads saved account configuration and market observations without creating, updating or deleting markets. Returned ids can be used with get_market.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | End date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it. | |
| from | No | Start date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today. | |
| period | No | The window to report on. A calendar month as `2026-07`, a calendar year as `2026`, `last_month`, `this_month`, `last_year`, or a trailing window: `today`, `7d`, `14d`, `30d`, `90d`, `180d`, `365d`, `730d`, `1095d`. Defaults to `30d`. A calendar period is compared against the calendar period before it — July against June, not against the 31 days ending 30 June. Three years is the ceiling. |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | Markets this account keeps. |
| limit | Yes | How many its plan allows. Equal to `count` means the next save is refused; say so before the user tries. |
| window | Yes | The window every change figure below is measured over. |
| markets | Yes | One entry per market, newest first. Pass a `market_id` to get_market with include: ["members"] for the leaderboard behind any of them. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, openWorldHint=false and destructiveHint=false, so the safety profile is covered. The description nevertheless adds real context by specifying what is read ('saved account configuration and market observations') and explicitly ruling out create/update/delete, which is more than the annotations alone convey.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three lean sentences, front-loaded with what is listed and followed by the read-only guarantee and the id hand-off. The enumerated field list is slightly redundant given an output schema exists, but nothing is padded or buried.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a low-risk read tool with annotations covering safety and an output schema covering return shape, the description is nearly complete. The remaining gap is operational detail such as pagination, ordering, or behavior when the account has no saved markets.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, with detailed per-parameter text for to, from and period including defaults and precedence rules, so the schema carries the parameter burden. The description only vaguely alludes to 'the requested period' and adds no syntax or precedence detail beyond the schema, which is the correct baseline of 3.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List markets saved by the authenticated account') plus the scope of what is returned, so the agent knows exactly what this produces. It also implicitly distinguishes itself from the singular sibling get_market by noting the returned ids feed into it.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is clear: call this to enumerate saved markets for a reporting window, and the note that returned ids work with get_market routes the agent to the follow-up call. There is no explicit statement of when not to use it (e.g., versus get_landscape or get_performance for aggregated metrics), so it stops short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remove_competitorsRemove CompetitorsADestructiveIdempotentInspect
Remove one or more competitor tracking relationships for an app owned by the authenticated account. Accepts AppTail app ids, App Store URLs or names. Name and URL resolution can query the public App Store and import shared app records. Removes the selected pairing only; public apps and pairings for other apps remain. Requires write authorisation and returns one outcome per item.
| Name | Required | Description | Default |
|---|---|---|---|
| app_id | Yes | Apptail app id for one of YOUR apps — from get_account. Only this app's competitor set is touched. | |
| competitors | Yes | The rivals to stop tracking, up to 20. Each item is an Apptail app_id (as returned by get_competitors), a store URL, or a name. Ids are the reliable form here — a name is resolved against the whole store, not against what you track. |
Output Schema
| Name | Required | Description |
|---|---|---|
| app_id | Yes | The app these rivals were tracked against. |
| outcomes | Yes | One row per item. `not_tracked` means it was not a competitor of this app to begin with — the end state is what was asked for, but do not report it as a removal. |
| remaining | Yes | How many competitors this app still tracks. |
| removed_count | Yes | How many pairings were actually removed. |
| requested_count | Yes | How many items were sent. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Going beyond the annotations, it discloses that name/URL resolution may query the public App Store and import shared app records (a real side effect), that write authorisation is required, and that one outcome is returned per item. These add genuine behavioral context, though reversibility of the deletion is not addressed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The action is front-loaded in the first sentence, followed by input forms, side effects, scope, and auth requirements in a compact five-sentence block. Every sentence carries information, though there is minor overlap between the body and the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a destructive, open-world mutation with an output schema present, the description covers the key agent concerns: scope, side effects, auth, and per-item results. Return-value details are correctly left to the output schema, so the only minor gap is the lack of note on how failures per item are surfaced.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents both parameters thoroughly, including the id/URL/name forms and the 'up to 20' limit. The description largely repeats the accepted input forms rather than adding new meaning, so the baseline of 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (remove), resource (competitor tracking relationships), and scope (for an app owned by the authenticated account). An agent can immediately distinguish this from add_competitors and get_competitors without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description clarifies that only the selected pairing is removed and that public apps and other apps' pairings remain, which tells the agent what this tool will and won't affect. It does not explicitly name sibling alternatives like add_competitors, but the usage context is clear enough.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remove_keywordsRemove KeywordsADestructiveIdempotentInspect
Remove tracked keywords from an app. Frees the slots against the plan limit; position history is kept, so re-adding a term later does not start from nothing.
| Name | Required | Description | Default |
|---|---|---|---|
| app_id | Yes | Apptail app id for one of YOUR apps — get it from get_account. Not the numeric id in an App Store URL (that is apple_app_id). | |
| keyword_ids | Yes | Apptail keyword ids to stop tracking, as returned by get_keywords. Ids, not terms — two apps can track the same word and only these rows are removed. Frees the slots against your plan limit. Position history is kept, so re-adding a term later does not start from nothing. |
Output Schema
| Name | Required | Description |
|---|---|---|
| app_id | Yes | The app these terms were tracked for. |
| remaining | Yes | How many keywords this app still tracks, across every storefront. |
| removed_count | Yes | How many keywords were actually untracked. Lower than `requested_count` when some IDs were not tracked for this app. |
| requested_count | Yes | How many keyword IDs were passed in. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare destructiveHint=true and idempotentHint=true, so the safety profile is covered. The description adds genuinely useful context beyond them: removal frees plan slots and position history is preserved, so re-adding does not reset progress. It stops short of stating auth requirements or that removal is immediate/permanent for the tracking row.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences with zero waste, front-loading the action and resource before the consequence. Every clause carries information about effect or data retention.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a destructive two-param tool with full schema coverage, annotations, and an output schema, the description supplies the key behavioral facts an agent needs (slot freeing, history retention). It is essentially complete, though a note on whether the action can be undone server-side would close the last gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so both parameters are already fully documented, including the app_id vs apple_app_id distinction and the ids-not-terms caveat. The description merely echoes the keyword_ids semantics, adding no meaning beyond the schema; baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Remove') and resource ('tracked keywords') scoped to an app, which cleanly separates it from siblings like remove_competitors, remove_market, and add_keywords. An agent can identify the tool's function without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by the effect statement ('frees the slots against the plan limit'), but there is no explicit when-to-use guidance, no mention of prerequisites (e.g., needing ids from get_keywords), and no comparison to alternatives such as tag_keywords for organizing rather than removing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
remove_marketRemove MarketADestructiveInspect
Delete a saved market, or just one of its storefronts. Pass store to drop a single storefront and leave the rest of the market with its corpora and its build clocks untouched; omit it to delete the whole market. Deleting frees a slot on the account's plan. The terms themselves are not deleted — they are shared with any other market or tracked app using them.
| Name | Required | Description | Default |
|---|---|---|---|
| store | No | Drop only this storefront, ISO 3166-1 alpha-2. Omit to delete the whole market. Dropping the last storefront deletes the market too, because a market asked nowhere is not a market. | |
| market_id | Yes | Apptail market id — from list_markets. |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| deleted | Yes | True when the whole market is gone. False when only a storefront was dropped and the market remains. |
| market_id | Yes | |
| markets_remaining | Yes | Markets left on the account after this call. |
| removed_storefronts | Yes | What this call removed. |
| remaining_storefronts | Yes | What the market is still asked in. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the destructiveHint=true annotation by disclosing that deletion frees a plan slot, that shared terms survive deletion, and that dropping a single storefront preserves the rest of the market's corpora and build clocks. This is exactly the kind of side-effect and shared-resource context an agent needs before a destructive call.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences, front-loaded with the dual behavior, then consequences. Every sentence carries distinct information (mode selection, plan-slot effect, term sharing).
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be described. Combined with destructiveHint annotations, the description covers mode selection, side effects, shared-resource caveats, and plan implications – everything needed to invoke safely.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already documents both params (baseline 3). The description adds meaning beyond the schema by explaining the storefront-level outcome of `store` (rest of market and build clocks untouched), which the schema does not fully spell out.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (delete a saved market or one storefront) and clearly distinguishes the two modes of operation. An agent can tell it apart from sibling tools like save_market, get_market, and list_markets without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to pass `store` versus omitting it, and what each choice leaves untouched. It doesn't name sibling alternatives (e.g., save_market or remove_keywords) as routing options, so it stops short of full when/when-not guidance, but the operational context is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
reply_to_reviewsReply To ReviewsADestructiveInspect
Create, update or remove public App Store review responses for apps owned by the authenticated account, using its connected App Store Connect API key. Requires write authorisation and eligible review ids. Submitted text is sent verbatim under the developer's name. Obtain approval for the exact text or requested removal before submitting. dry_run validates without publication. Accepts up to ten items and returns per-item outcomes; pending means Apple has accepted a response but has not published it.
| Name | Required | Description | Default |
|---|---|---|---|
| dry_run | No | Check every item and report what would happen, without publishing anything. **Worth using by default here** — it confirms the reviews resolve, the account's key reaches their apps and the text fits, before anything reaches the store. | |
| replies | Yes | The replies, up to 10 per call. Send them in one call rather than one at a time: each is answered separately, so a batch where one app is not covered still posts the rest and says which it did not. |
Output Schema
| Name | Required | Description |
|---|---|---|
| dry_run | Yes | True when nothing was sent to Apple. Every outcome is then what *would* have happened. |
| outcomes | Yes | One row per item, in the order they were sent. Read `status` per row, then `state` on the ones that posted. |
| max_items | Yes | The per-call cap. A request above it is refused as a whole rather than silently trimmed. |
| failed_count | Yes | How many did not do what was asked. Never report a batch as done while this is above zero — say which ones and why. |
| posted_count | Yes | How many replies Apple took. Read each one's `state` before calling any of them published. |
| removed_count | Yes | How many replies were taken down. |
| requested_count | Yes | How many items were sent. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the annotations (destructiveHint, openWorldHint), the description adds substantial operational context: text is published verbatim under the developer's name, removal is publicly visible once published, calls are batched up to ten items, per-item outcomes are returned, and a 'pending' result means Apple accepted but has not yet published.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The definition is front-loaded with the action and scope, then moves to prerequisites, then to behavioural details. It is dense but every sentence carries weight; the only mild redundancy is restating the ten-item limit that the schema also documents.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a destructive, multi-item mutation tool with strong annotations and a dedicated output schema, the description covers the remaining burden: approval prerequisites, dry_run semantics, partial-batch failure behaviour, and the pending outcome. An agent has everything needed to invoke it safely.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents dry_run, body, remove, review_id and the batching limit in detail. The description largely restates that information (up to ten items, dry_run validates without publishing) and adds little the schema does not already say, so the baseline of 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence names a specific verb set (create, update, remove) and a specific resource (public App Store review responses) with scope (apps owned by the authenticated account). It is immediately distinguishable from the read-only sibling get_reviews, and the auth mechanism (connected App Store Connect API key) is stated.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives clear prerequisites (write authorisation, eligible review ids, approval of exact text before submitting) and recommends dry_run for validation. It does not explicitly route the agent to a sibling for obtaining review ids, though the schema's review_id description points at get_reviews, which covers the gap somewhat.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
save_marketSave MarketADestructiveInspect
Create or update a saved market in the authenticated account from a query, countries and optional per-storefront seed terms. An existing name updates that market, adding storefronts or replacing seed configuration and rebuilding requested storefronts. Persists account configuration and queues background corpus collection using public App Store data. Requires write authorisation. Returns market_id, build status, affected storefronts and caveats; collection may still be pending.
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | What to call it. Defaults to `query`. Passing the name of a market this account already has ADDS the new storefronts to it rather than creating a second one. | |
| query | Yes | The phrase to build the market from, e.g. "AI calorie tracking". Used as the seed in every storefront unless `seeds` overrides it. | |
| seeds | No | Per-storefront seed terms, keyed by country code: {"de": ["kalorienzähler"]}. OPTIONAL, and optional per storefront. A storefront you leave out is not seeded from `query`: its terms are read off the localised listings of the leaders of the storefront that WAS phrased, and each one is searched there before it is kept — so it ends up with the local phrasing rather than an English phrase nobody types. Pass a storefront explicitly when you know the local phrase (to use the supplied local terms) or when you want to ask a different question there. Up to five terms each. | |
| countries | Yes | Storefronts to ask it in, ISO 3166-1 alpha-2 (e.g. ["US","DE"]). Each keeps its own corpus; nothing is blended across them. |
Output Schema
| Name | Required | Description |
|---|---|---|
| name | Yes | |
| slug | Yes | Its address in the console. |
| status | Yes | `building` immediately after a save. A market is building while ANY storefront is; each one lands separately. A build that dies leaves the storefront `failed` (or `ready` with `failed_at` set when a previous corpus survives) — it never stays `building` for ever. |
| caveats | Yes | What the caller needs to hear — that the build is not finished, or that a storefront was seeded from an English phrase. |
| created | Yes | True for a new market, false when storefronts were added to one that already existed. |
| market_id | Yes | Pass it to get_market to read the market once it has built. |
| console_url | No | The market in the console. |
| storefronts | Yes | Every storefront this market is now asked in, including ones it already had. |
| added_storefronts | Yes | The ones this call added or rebuilt. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations: it discloses that an existing name replaces seed configuration and rebuilds storefronts (matching destructiveHint=true), that write authorisation is required, that corpus collection is queued as a background job which may still be pending, and what the response contains.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the create/update purpose, then layers update semantics, side effects, auth and returns in dense but purposeful sentences with no filler. Slightly long, but every sentence carries distinct information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Covers the mutation path, prerequisites (write auth), asynchronous side effects and caveats for a four-parameter tool with nested seeds. With an output schema present, its restatement of return fields is a bonus rather than a necessity, leaving nothing essential missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so name, query, seeds and countries are already fully documented in the schema; the description only restates the parameter list rather than adding format or constraint detail. Baseline 3 applies when the schema carries the semantics.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb pair (create or update) and resource (saved market) plus the inputs it is built from, so an agent can distinguish it from siblings like get_market, edit_market_corpus and list_markets without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Clearly explains the create-vs-update branch: an existing name updates that market, adds storefronts, or replaces seed configuration and rebuilds. It gives strong context for the primary path but never names alternative tools (e.g. edit_market_corpus) or an explicit when-not-to-use condition.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_appsSearch AppsAInspect
Search for iOS apps by name, bundle ID, or App Store URL. Supports all languages including Cyrillic, Chinese, etc. If the app is not in our database, it will be fetched from the App Store, persisted as a shared public app record, and queued for enrichment. This does not add the app to your account or competitor watchlist. Auto-detects the likely App Store region from the query language (e.g. Cyrillic → Russia, Chinese → China). To add what you find, add_competitors and add_app take the same name or URL directly — you do not have to look up an app_id first.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max results. Default 10, capped at 25. | |
| query | Yes | App name in any language, a bundle id (com.example.app), or a full App Store URL. Searches Apptail first and falls back to the App Store, importing anything it finds — so this returns a usable app_id even for an app we have never seen. This is how you turn a name a user typed into an app_id. | |
| country | No | Storefront to search (e.g. "us", "ru"). Omit and it is guessed from the script the query is written in — Cyrillic implies ru, and so on. Pass it explicitly when the user names a market, because the guess is about language and not about where they sell. |
Output Schema
| Name | Required | Description |
|---|---|---|
| apps | Yes | Matching apps, best match first. Empty when nothing was found. |
| hint | No | Present when nothing was found: suggests how to retry. |
| note | No | Present when the results need a caveat, e.g. data still being fetched in the background. |
| count | Yes | Number of apps returned. |
| source | Yes | Where the results came from. "app_store" means they were just imported and are still being enriched. |
| country | No | Storefront that was searched. Null when answered from the local database, which is not country-specific. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations only say readOnlyHint=false and openWorldHint=true; the description supplies the real behavioral story the agent needs: a miss triggers an App Store fetch, persistence of a shared public app record, and an enrichment queue. That explains why a 'search' is not read-only, which the annotations alone leave opaque.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Six sentences, front-loaded with purpose and searchable forms, then side effects, then routing. Every sentence carries information, though the closing note that no app_id lookup is needed slightly overlaps what the query parameter description already says.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For an open-world, non-read-only lookup tool with 3 params, an output schema, and full schema coverage, the description covers the import side effects, region inference, and sibling routing. Nothing an agent needs to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds genuine meaning: the region is auto-detected from the query script (Cyrillic→ru, Chinese→China) and it warns that the guess reflects language, not market, so country should be passed explicitly when a market is named. It goes beyond restating the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Names a specific verb and resource ('Search for iOS apps') and spells out the accepted query forms: name, bundle ID, or App Store URL. It is clearly distinguishable from get_app, add_app, and other siblings by being the lookup/import entry point.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states what this does NOT do ('does not add the app to your account or competitor watchlist') and routes the agent to the correct alternatives, add_competitors and add_app, noting they accept the same name or URL. This is exactly the when/when-not/alternatives guidance the dimension asks for.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
tag_keywordsTag KeywordsADestructiveInspect
Put tracked keywords under a tag, in bulk — or clear their tag — and/or say whether they are brand terms for Search Visibility. The tag is named, not looked up: a name that does not exist yet is created. A keyword carries at most one tag, so assigning replaces whatever it had. brand is separate from tags: own (your app's name) and competitor (a rival's name) leave the terms out of the visibility score, none counts them whatever detection says, auto hands them back to detection. Send brand without tag to change only that — the tags stay as they are. This is the tool for organising a large corpus after reading it with get_keywords, which is drudgery in the console and one call here.
| Name | Required | Description | Default |
|---|---|---|---|
| tag | No | The tag name to put them under. Created if it does not exist. Omit — or send null — to clear the tag off these keywords instead. | |
| brand | No | Whether these terms are somebody's name, for Search Visibility (get_keywords `include: ["visibility"]`). `own` = your app's name, `competitor` = a rival's: both are listed and left out of the score, because you rank #1 for your own name whatever your ASO does. `none` = count them anyway. `auto` = let title-word detection decide again. Detection is shown on each row's `brand.reason`; override it when it is wrong (a transliterated rival name, a generic word it took for a name). Omit to leave the brand decision alone. Sent without `tag`, only the brand decision changes. | |
| color | No | Hex colour for a tag being created, e.g. "#2563eb". Ignored when the tag already exists. Omit and one is picked from the palette that the app is not already using. | |
| app_id | Yes | Apptail app id for one of YOUR apps — from get_account. Tags belong to an app. | |
| keyword_ids | Yes | Apptail keyword ids to tag, as returned by get_keywords. Ids, not terms. Up to 500 in one call. A keyword the app does not track is reported back rather than silently ignored. |
Output Schema
| Name | Required | Description |
|---|---|---|
| tag | No | The tag the keywords now carry. Null when the action was `cleared`. |
| tags | Yes | Every tag this app now has, so a follow-up call does not have to guess at names. |
| brand | No | The brand decision applied (`own`, `competitor`, `none`, `auto`), or null when none was sent. Search Visibility recomputes on the next get_keywords read. |
| action | Yes | `assigned` when a tag was set, `cleared` when the tag was taken off, `untouched` when only `brand` was sent and the tags were left as they were. |
| app_id | Yes | The app whose corpus was changed. |
| tag_created | Yes | True when this call created the tag rather than reusing one. Worth mentioning: a typo makes a second tag rather than an error. |
| unknown_ids | Yes | Ids this app does not track, so nothing was changed for them. Empty on a clean run. Never present these as tagged. |
| changed_count | Yes | How many keywords had their tag changed. 0 on a brand-only call. |
| requested_count | Yes | How many keyword ids were sent. |
| brand_changed_count | Yes | How many keywords took the brand decision. 0 when `brand` was not sent. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare destructiveHint=true and openWorldHint=false, and the description is consistent with them while adding real behavioural context: a non-existent tag name is created, a keyword holds at most one tag so assignment replaces the prior one, the 500-id batch limit, and that untracked keywords are reported back rather than silently dropped. That is substantive disclosure beyond the structured fields.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the core action and the tag-vs-brand distinction before the detail, and every sentence carries operational information. It is somewhat long and partly restates the brand enum values already documented in the schema, which keeps it from a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return values need not be explained, and the description still covers the app scoping, tag auto-creation, batch limit, replacement semantics, and error reporting. Nothing an agent needs to invoke this destructive mutation correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% so the baseline is 3, but the description goes further by explaining cross-parameter semantics the schema does not: how tag and brand interact (brand alone leaves tags intact), and what omitting each field does (clear the tag / leave the brand decision alone). It adds meaning beyond the per-field descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (bulk-tag tracked keywords) plus the secondary operations (clear tag, set brand), and explicitly separates itself from the read-side sibling by saying it is for organising after reading with get_keywords. An agent can distinguish it from add_keywords/remove_keywords without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Names the alternative workflow (get_keywords for reading, this tool for bulk organisation) and gives conditional usage modes: omit tag to clear, send brand without tag to change only the brand decision, override auto detection when it is wrong. The when-to-use context is explicit rather than inferred.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
- Changed
get_keywords2 fields changed- added
Output schema / properties / visibility / properties / overall / properties / comparedAdded value: +{ + "description": "The apps in `compare_with` across storefronts — the console's competitor table column. Your weights, never theirs: their own download split is not yours to read, and a different weighting would not compare. Empty when nothing was compared.", + "items": { + "properties": { + "app_id": { + "description": "Apptail id of the app from `compare_with`.", + "type": "integer" + }, + "band": { + "type": [ + "string", + "null" + ] + }, + "change": { + "description": "Like-for-like, as `change` above.", + "type": [ + "number", + "null" + ] + }, + "name": { + "description": "Its name.", + "type": [ + "string", + "null" + ] + }, + "previous": { + "type": [ + "number", + "null" + ] + }, + "score": { + "description": "Its overall on YOUR non-brand terms, weighted by YOUR storefronts' `weight` — directly comparable with `score` above. 0 = nothing in the top 50 on them.", + "type": [ + "number", + "null" + ] + } + }, + "type": "object" + }, + "type": "array" +} - changed
Output schema / properties / visibility / properties / overall / requiredPrevious value: -[ - "terms_scored", - "storefronts_scored", - "weights_source", - "weights_month", - "formula_version" -]New value: +[ + "terms_scored", + "storefronts_scored", + "weights_source", + "weights_month", + "compared", + "formula_version" +]
- Changed
get_landscape2 fields changed- added
Output schema / properties / members / items / properties / visibilityAdded value: +{ + "description": "Search Visibility, 0–100, on the account's **tracked non-brand terms** in this storefront: the same score get_keywords gives your app, computed for this one — 100 = #1 on every term, the mean of the last 7 days. **Not `share` rescaled, and not in its order**: `share` is over this landscape's wider corpus (it adds the terms the app ranks top 10 for untracked, and brand terms) at each app's best place in the window. 0 is a measurement — nothing in the top 50 on those terms. Null when the account's terms here cannot be scored or the read failed — top-level `visibility.reason` / `caveats` say which.", + "type": [ + "number", + "null" + ] +} - added
Output schema / properties / visibilityAdded value: +{ + "description": "The basis of each member's `visibility`. Null when it could not be read just now — then every member's `visibility` is null too, which is unknown, not zero.", + "properties": { + "rank": { + "description": "Your place among the apps scoring above zero on those terms. Null with no score, or at zero.", + "type": [ + "integer", + "null" + ] + }, + "ranked": { + "description": "How many apps score above zero on those terms — the whole top 50, not only the listed members.", + "type": "integer" + }, + "reason": { + "description": "Why there is no score, with the fix.", + "type": [ + "string", + "null" + ] + }, + "score": { + "description": "Your app's Search Visibility here — the same number as get_keywords `visibility.score` for this storefront.", + "type": [ + "number", + "null" + ] + }, + "terms_scored": { + "description": "The tracked non-brand terms every member's `visibility` is over.", + "type": "integer" + } + }, + "type": [ + "object", + "null" + ] +}
- Changed
get_market3 fields changed- added
Output schema / properties / entered / items / properties / visibilityAdded value: +{ + "description": "Search Visibility, 0–100, on the account's **tracked non-brand terms** in this storefront: the same score get_keywords gives your app, computed for this one — 100 = #1 on every term, the mean of the last 7 days. **Not `share` rescaled, and not in its order**: `share` is over this landscape's wider corpus (it adds the terms the app ranks top 10 for untracked, and brand terms) at each app's best place in the window. 0 is a measurement — nothing in the top 50 on those terms. Null when the account's terms here cannot be scored or the read failed — top-level `visibility.reason` / `caveats` say which.", + "type": [ + "number", + "null" + ] +} - added
Output schema / properties / left / items / properties / visibilityAdded value: +{ + "description": "Search Visibility, 0–100, on the account's **tracked non-brand terms** in this storefront: the same score get_keywords gives your app, computed for this one — 100 = #1 on every term, the mean of the last 7 days. **Not `share` rescaled, and not in its order**: `share` is over this landscape's wider corpus (it adds the terms the app ranks top 10 for untracked, and brand terms) at each app's best place in the window. 0 is a measurement — nothing in the top 50 on those terms. Null when the account's terms here cannot be scored or the read failed — top-level `visibility.reason` / `caveats` say which.", + "type": [ + "number", + "null" + ] +} - added
Output schema / properties / members / items / properties / visibilityAdded value: +{ + "description": "Search Visibility, 0–100, on the account's **tracked non-brand terms** in this storefront: the same score get_keywords gives your app, computed for this one — 100 = #1 on every term, the mean of the last 7 days. **Not `share` rescaled, and not in its order**: `share` is over this landscape's wider corpus (it adds the terms the app ranks top 10 for untracked, and brand terms) at each app's best place in the window. 0 is a measurement — nothing in the top 50 on those terms. Null when the account's terms here cannot be scored or the read failed — top-level `visibility.reason` / `caveats` say which.", + "type": [ + "number", + "null" + ] +}
1 tool update
- Changed
get_keywords3 fields changed- added
Output schema / properties / visibility / properties / overallAdded value: +{ + "description": "The app's Search Visibility across those storefronts, weighted by where its downloads come from (`weights_source`). **Lead with this for \"is our ASO working\" without a storefront in the question;** with one, lead with `score`. Null at portfolio scope or when the read failed.", + "properties": { + "band": { + "description": "Same cut-offs as `visibility.score.band`.", + "enum": [ + "dominant", + "strong", + "visible", + "weak", + "invisible" + ], + "type": [ + "string", + "null" + ] + }, + "change": { + "description": "The storefronts' like-for-like changes, weighted, over storefronts that have one. Null = no storefront had a baseline — not a change of zero.", + "type": [ + "number", + "null" + ] + }, + "coverage": { + "description": "0–1: the share of downloads the scored storefronts hold before rescaling. Low means the overall rests on a small part of where downloads come from. Null with equal weights.", + "type": [ + "number", + "null" + ] + }, + "effective_rank": { + "description": "The single place that earns this score on every term.", + "type": [ + "number", + "null" + ] + }, + "formula_version": { + "description": "Changes when the formula does; 1 now.", + "type": "string" + }, + "previous": { + "description": "The same weighting over each storefront's `previous`, over storefronts that have one.", + "type": [ + "number", + "null" + ] + }, + "reason": { + "description": "Why there is no overall score.", + "type": [ + "string", + "null" + ] + }, + "score": { + "description": "0–100: the storefront scores weighted by each line's `weight`. Null when no storefront has a score — `reason` says why.", + "type": [ + "number", + "null" + ] + }, + "storefronts_scored": { + "description": "Storefronts that carry weight in `score`.", + "type": "integer" + }, + "terms_scored": { + "description": "Non-brand terms across the storefronts that carry weight.", + "type": "integer" + }, + "weights_month": { + "description": "YYYY-MM the weights belong to. They change only when the month does.", + "type": "string" + }, + "weights_source": { + "description": "`downloads`: each storefront's share of the app's App Store Connect downloads over the 90 days before `weights_month` began, rescaled over the scored storefronts and fixed for the month. `equal`: the app is not connected to App Store Connect on this account, or had no downloads in a scored storefront.", + "enum": [ + "downloads", + "equal" + ], + "type": "string" + } + }, + "required": [ + "terms_scored", + "storefronts_scored", + "weights_source", + "weights_month", + "formula_version" + ], + "type": [ + "object", + "null" + ] +} - changed
Output schema / properties / visibility / properties / storefronts / descriptionPrevious value: -"One line per storefront the app tracks terms in — score, change, band — so \"where is it working\" is one call. Each is its own reading over its own terms; never add them up. Empty at portfolio scope."New value: +"One line per storefront the app tracks terms in — score, change, band, weight — so \"where is it working\" is one call. Each is its own reading over its own terms; never add them up — `overall` weights them. Empty at portfolio scope." - added
Output schema / properties / visibility / properties / storefronts / items / properties / weightAdded value: +{ + "description": "0–1: what this storefront counts for in `visibility.overall.score` (see `weights_source` there). Null = it has no score, so it is not in the overall.", + "type": [ + "number", + "null" + ] +}
2 tool updates
- Changed
get_keywords14 fields changed- changed
Input schema / properties / include / descriptionPrevious value: -"Extra blocks. `history` adds a day-by-day series per term, capped at 25 terms — pair it with `keyword_ids` for specific terms. `visibility` adds the daily top-1/3/10/30/50/100 counts and their change, which is the number that survives a corpus changing size. `shared` adds the terms two of your own apps are both on, and needs `scope: \"portfolio\"`."New value: +"Extra blocks. `history` adds a day-by-day series per term, capped at 25 terms — pair it with `keyword_ids` for specific terms. `visibility` adds **Search Visibility** — the 0–100 answer to \"is our ASO working here\", its like-for-like change and the terms that moved it, plus a line per storefront — and each row's `points_change` and `brand`; also the daily top-N counts, which are over every term the app appeared for, not over this corpus. `shared` adds the terms two of your own apps are both on, and needs `scope: \"portfolio\"`." - added
Output schema / properties / keywords / items / properties / brandAdded value: +{ + "description": "Whether this term is somebody's name, for Search Visibility. Present only with `include: [\"visibility\"]` at app scope.", + "properties": { + "kind": { + "description": "Null = scored. `own` = the app's own name; `competitor` = the name of the app ranking #1 for it. Either is listed and left out of Search Visibility.", + "enum": [ + "own", + "competitor" + ], + "type": [ + "string", + "null" + ] + }, + "override": { + "description": "The owner's decision, set with tag_keywords `brand`; `none` = counted whatever detection says. Null = detection decides.", + "enum": [ + "own", + "competitor", + "none" + ], + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "Why detection flagged it, e.g. \"matches “Fuelio: MPG & Mileage Tracker”, which ranks #1\".", + "type": [ + "string", + "null" + ] + } + }, + "type": [ + "object", + "null" + ] +} - added
Output schema / properties / keywords / items / properties / points_changeAdded value: +{ + "description": "What this term did to Search Visibility in its storefront over the window, in points — its exact share of `visibility.score.change`, so the rows' values add up to it. Present only with `include: [\"visibility\"]` at app scope; null for a brand term, a term not measured in both weeks, or when visibility was not asked for.", + "type": [ + "number", + "null" + ] +} - changed
Output schema / properties / visibility / descriptionPrevious value: -"Terms in each band at the end of the window, cumulative, when `include: [\"visibility\"]` was asked for. **A state, not a sum** — the last day that has a row, because summing thirty days of \"terms in the top ten\" counts the same term thirty times. Null when it was not asked for, or when nothing has been rolled up for this app yet."New value: +"When `include: [\"visibility\"]` was asked for: Search Visibility (`score`, `storefronts`) and the daily band counts. **The band counts are not over this corpus**: they count every term the app appeared for that day in this storefront, tracked or not, so they move with whatever anybody crawled and can disagree with the rows (a corpus whose rows show 27 terms in the top 10 can read `top10: 20`). Read them as reach across the store, never as the corpus's state. Each is the last day with a row — a state, not a sum — and null when nothing has been rolled up. The whole block is null when visibility was not asked for." - changed
Output schema / properties / visibility / properties / ranked / typePrevious value: -"integer"New value: +[ + "integer", + "null" +] - added
Output schema / properties / visibility / properties / scoreAdded value: +{ + "description": "**Search Visibility** in the storefront asked about: 0–100, 100 = #1 on every non-brand tracked term, with its like-for-like `change`, the terms that moved it (`biggest_losses`, `biggest_gains`) and the rivals in `compare_with` scored on the same terms. **Lead with this for \"is our ASO working\"** — counting risers and fallers points the wrong way when a term at #1 drops to #7. Over every tracked term in the storefront, whatever filter the rows had. Null at portfolio scope (niches do not add up) or when the read failed — the caveats say which.", + "properties": { + "band": { + "description": "Banded on effective rank: dominant 60+ (≈#1–2 everywhere, owns the first screen), strong 35–59 (≈#2–4), visible 18–34 (≈#4–10), weak 9–17 (≈#10–25), invisible 0–8 (beyond #25). Null with `score`.", + "enum": [ + "dominant", + "strong", + "visible", + "weak", + "invisible" + ], + "type": [ + "string", + "null" + ] + }, + "biggest_gains": { + "description": "The terms that added the most points, best first, at most 5.", + "items": { + "properties": { + "keyword_id": { + "description": "Apptail keyword id.", + "type": "integer" + }, + "place_now": { + "description": "Its place on the last day of the headline week. Null = not in the top 50 (it was measured).", + "type": [ + "integer", + "null" + ] + }, + "place_then": { + "description": "Its place on the last day of the earlier week. Null = not in the top 50 (it was measured).", + "type": [ + "integer", + "null" + ] + }, + "points_change": { + "description": "What it did to the score, in points. These add up to `change` across all terms.", + "type": "number" + }, + "term": { + "description": "The term.", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "biggest_losses": { + "description": "The terms that cost the most points, worst first, at most 5 — the answer to \"why did it fall\". Counted over the whole storefront corpus, whatever filter the rows had.", + "items": { + "properties": { + "keyword_id": { + "description": "Apptail keyword id.", + "type": "integer" + }, + "place_now": { + "description": "Its place on the last day of the headline week. Null = not in the top 50 (it was measured).", + "type": [ + "integer", + "null" + ] + }, + "place_then": { + "description": "Its place on the last day of the earlier week. Null = not in the top 50 (it was measured).", + "type": [ + "integer", + "null" + ] + }, + "points_change": { + "description": "What it did to the score, in points. These add up to `change` across all terms.", + "type": "number" + }, + "term": { + "description": "The term.", + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "change": { + "description": "**Like-for-like** change in points: over the terms measured in both weeks only, so adding or removing terms cannot move it. Each row's `points_change` is its exact share. Null when the earlier week has no baseline — not a change of zero.", + "type": [ + "number", + "null" + ] + }, + "compared": { + "description": "The apps in `compare_with`, scored on this app's corpus — the same rule as the rows' `competitors`. Empty when nothing was compared.", + "items": { + "properties": { + "app_id": { + "description": "Apptail id of the rival from `compare_with`.", + "type": "integer" + }, + "band": { + "type": [ + "string", + "null" + ] + }, + "change": { + "type": [ + "number", + "null" + ] + }, + "name": { + "description": "Its name.", + "type": [ + "string", + "null" + ] + }, + "previous": { + "type": [ + "number", + "null" + ] + }, + "score": { + "description": "Its score on YOUR terms, same weights, same weeks.", + "type": [ + "number", + "null" + ] + } + }, + "type": "object" + }, + "type": "array" + }, + "corpus_change": { + "description": "The list moving rather than the app. History is recomputed over today's list, so a term added last week still has its earlier places if anyone crawled it; only terms with no earlier crawl land here.", + "properties": { + "added": { + "description": "Terms scored now that were not measured in the earlier week.", + "type": "integer" + }, + "effect": { + "description": "What they did to `score − previous`, in points. Never part of `change`.", + "type": [ + "number", + "null" + ] + }, + "removed": { + "description": "Terms measured in the earlier week that are not now.", + "type": "integer" + } + }, + "type": "object" + }, + "country": { + "description": "The storefront this score is for.", + "type": "string" + }, + "effective_rank": { + "description": "The single place that would earn this score on every term: S = 21 is \"as visible as ranking about #8 everywhere\". The sentence to put beside the number. Null for no score or a score of 0.", + "type": [ + "number", + "null" + ] + }, + "formula_version": { + "description": "Changes when a constant behind the score changes; scores from different versions do not compare.", + "type": "string" + }, + "gained": { + "description": "Sum of the positive `points_change` — what the terms that improved added.", + "type": [ + "number", + "null" + ] + }, + "last_measured_on": { + "description": "The most recent full-depth crawl of any of these terms, `YYYY-MM-DD`.", + "type": [ + "string", + "null" + ] + }, + "lost": { + "description": "Sum of the negative `points_change`. `gained + lost = change`.", + "type": [ + "number", + "null" + ] + }, + "previous": { + "description": "The same score over the 7 days before the window began. `score − previous = change + corpus_change.effect`.", + "type": [ + "number", + "null" + ] + }, + "reason": { + "description": "Why `score` is null, with the fix — \"Track 5 or more non-brand terms in DE to get a score\". Null when there is a score.", + "type": [ + "string", + "null" + ] + }, + "score": { + "description": "0–100: the app's weighted search places over the most its non-brand tracked terms allow. **100 = #1 on every one.** The mean of the last 7 days of the window. Null when fewer than 5 non-brand terms were measured — `reason` says why — and never 0 for missing: 0 is a measurement, nothing in the top 50.", + "type": [ + "number", + "null" + ] + }, + "terms_brand_excluded": { + "description": "Brand terms listed and left out: the app's own name (it ranks #1 whatever its ASO does) and rivals' names where that rival ranks #1. Detected from title words; an owner can override per term with tag_keywords `brand`. Rows carry `brand`.", + "type": "integer" + }, + "terms_scored": { + "description": "Non-brand terms measured in the last 7 days — what `score` is over. Quote it beside the score.", + "type": "integer" + }, + "terms_unmeasured": { + "description": "Non-brand terms with no full-depth crawl in the last 7 days. Out of this week's score — not counted as zero.", + "type": "integer" + }, + "weeks": { + "description": "The two 7-day spans `score` and `previous` are over, `YYYY-MM-DD`.", + "properties": { + "now_from": { + "type": "string" + }, + "now_to": { + "type": "string" + }, + "then_from": { + "type": "string" + }, + "then_to": { + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "country", + "biggest_losses", + "biggest_gains", + "terms_scored", + "terms_brand_excluded", + "terms_unmeasured", + "corpus_change", + "compared", + "weeks", + "formula_version" + ], + "type": [ + "object", + "null" + ] +} - added
Output schema / properties / visibility / properties / storefrontsAdded value: +{ + "description": "One line per storefront the app tracks terms in — score, change, band — so \"where is it working\" is one call. Each is its own reading over its own terms; never add them up. Empty at portfolio scope.", + "items": { + "properties": { + "band": { + "enum": [ + "dominant", + "strong", + "visible", + "weak", + "invisible" + ], + "type": [ + "string", + "null" + ] + }, + "change": { + "description": "Like-for-like change in points. Null = no baseline.", + "type": [ + "number", + "null" + ] + }, + "country": { + "description": "Storefront code.", + "type": "string" + }, + "reason": { + "description": "Why there is no score here, with the fix.", + "type": [ + "string", + "null" + ] + }, + "score": { + "description": "0–100, as `visibility.score.score`. Null with a `reason`.", + "type": [ + "number", + "null" + ] + }, + "terms_scored": { + "description": "Non-brand terms the score is over.", + "type": "integer" + } + }, + "required": [ + "country", + "terms_scored" + ], + "type": "object" + }, + "type": "array" +} - changed
Output schema / properties / visibility / properties / top1 / typePrevious value: -"integer"New value: +[ + "integer", + "null" +] - changed
Output schema / properties / visibility / properties / top10 / typePrevious value: -"integer"New value: +[ + "integer", + "null" +] - changed
Output schema / properties / visibility / properties / top100 / typePrevious value: -"integer"New value: +[ + "integer", + "null" +] - changed
Output schema / properties / visibility / properties / top3 / typePrevious value: -"integer"New value: +[ + "integer", + "null" +] - changed
Output schema / properties / visibility / properties / top30 / typePrevious value: -"integer"New value: +[ + "integer", + "null" +] - changed
Output schema / properties / visibility / properties / top50 / typePrevious value: -"integer"New value: +[ + "integer", + "null" +] - added
Output schema / properties / visibility / requiredAdded value: +[ + "storefronts" +]
- Changed
tag_keywords7 fields changed- added
Input schema / properties / brandAdded value: +{ + "description": "Whether these terms are somebody's name, for Search Visibility (get_keywords `include: [\"visibility\"]`). `own` = your app's name, `competitor` = a rival's: both are listed and left out of the score, because you rank #1 for your own name whatever your ASO does. `none` = count them anyway. `auto` = let title-word detection decide again. Detection is shown on each row's `brand.reason`; override it when it is wrong (a transliterated rival name, a generic word it took for a name). Omit to leave the brand decision alone. Sent without `tag`, only the brand decision changes.", + "enum": [ + "own", + "competitor", + "none", + "auto" + ], + "type": "string" +} - changed
Output schema / properties / action / descriptionPrevious value: -"`assigned` when a tag was set, `cleared` when the tag was taken off."New value: +"`assigned` when a tag was set, `cleared` when the tag was taken off, `untouched` when only `brand` was sent and the tags were left as they were." - changed
Output schema / properties / action / enumPrevious value: -[ - "assigned", - "cleared" -]New value: +[ + "assigned", + "cleared", + "untouched" +] - added
Output schema / properties / brandAdded value: +{ + "description": "The brand decision applied (`own`, `competitor`, `none`, `auto`), or null when none was sent. Search Visibility recomputes on the next get_keywords read.", + "type": [ + "string", + "null" + ] +} - added
Output schema / properties / brand_changed_countAdded value: +{ + "description": "How many keywords took the brand decision. 0 when `brand` was not sent.", + "type": "integer" +} - changed
Output schema / properties / changed_count / descriptionPrevious value: -"How many keywords were actually changed."New value: +"How many keywords had their tag changed. 0 on a brand-only call." - changed
Output schema / requiredPrevious value: -[ - "app_id", - "action", - "tag_created", - "changed_count", - "requested_count", - "unknown_ids", - "tags" -]New value: +[ + "app_id", + "action", + "tag_created", + "brand_changed_count", + "changed_count", + "requested_count", + "unknown_ids", + "tags" +]
25 tool updates
- First observed
add_app - First observed
add_competitors - First observed
add_keywords - First observed
discover_keywords - First observed
edit_market_corpus - First observed
explain_period - First observed
get_account - First observed
get_app - First observed
get_competitors - First observed
get_keyword_serp - First observed
get_keywords - First observed
get_landscape - First observed
get_market - First observed
get_performance - First observed
get_reviews - First observed
get_signals - First observed
get_top_charts - First observed
list_markets - First observed
remove_competitors - First observed
remove_keywords - First observed
remove_market - First observed
reply_to_reviews - First observed
save_market - First observed
search_apps - First observed
tag_keywords
Related MCP Connectors
App Store and Google Play keyword ranks, keyword research and competitors for your apps.
App Store & Google Play keyword ranks, rivals, store pages, reviews, charts and AI visibility
App Store Optimization data for AI agents: keyword ranks, scores, competitors, charts
App Store keyword research, rank tracking and App Store Connect performance for your iOS apps.
Related MCP Servers
- AlicenseBqualityBmaintenanceEnables App Store and Google Play keyword rank tracking, competitor comparisons, and AI visibility checks through natural language, without requiring store credentials.8532 npm1MIT
- AlicenseNot gradedqualityAmaintenanceApp Store and Google Play ASO: keyword rankings, competitors, reviews, audits and an action queue.90AGPL 3.0
- AlicenseAqualityAmaintenanceConnects App Store and Google Play keyword data to AI assistants, enabling ASO tasks like rank tracking, keyword analysis, competitor visibility, and top charts through natural language.30407 npmMIT

AppmanAi MCPofficial
AlicenseNot gradedqualityBmaintenanceEnables AI assistants to query live App Store and Google Play data — keyword demand, rank history, top charts, competitor sets, reviews and ratings — across 100+ countries, directly inside a chat instead of a dashboard. Exposes 35 read-only tools covering app search, ASO keyword profiling, ranking trajectories, and worldwide market comparisons through a remote OAuth-authenticated endpoint.2MIT
Glama MCP Gateway
Add one secure layer between your agents and this server.