Skip to main content
Glama

AppTail – ASO & Keyword Research

Get Keywords

get_keywords
Read-only

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.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
toNoEnd date, `YYYY-MM-DD`, inclusive. Send it alone and the window is the three years before it.
fromNoStart date, `YYYY-MM-DD`, inclusive. Overrides `period` when both are sent. Send it alone and the window runs to today.
sortNo`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.
limitNoMax terms returned, after sorting. Default 100, capped at 300. The answer always says how many the corpus holds.
scopeNo`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_idNoApptail 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).
periodNoThe 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_idNoOnly keywords carrying this tag. Tag ids come back on the keywords in this tool's own output. Omit for the whole corpus.
countryNoStorefront 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.
includeNoExtra 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"`.
containsNoOnly 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_idsNoOnly 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_withNoApptail 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

TableJSON Schema
NameRequiredDescriptionDefault
sortYesThe order the list came back in.
countYesTerms returned.
scopeYesWhich scope answered: `app` or `portfolio`.
totalYesTerms the corpus holds in this storefront. Larger than `count` when the list was capped.
moversYesThe 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.
sharedYesTerms 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.
windowYesThe 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_idsYesThe apps these ranks were read for.
caveatsYesSentences 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.
countryYesStorefront the ranks were measured in.
coverageYesHow 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.
keywordsYesThe 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.
truncatedYesTrue when `count` is below `total`. Say the list is partial rather than reporting it as the whole corpus.
visibilityNoWhen `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_idsYesThe competitor ids actually measured. Shorter than what was sent when the cap or an unknown id dropped one.
compare_truncatedYesTrue when competitor ids were dropped for exceeding the cap of 5.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed2 schema fields changed
    • addedOutput schema / properties / visibility / properties / overall / properties / compared
      Added 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"
      +}
    • changedOutput schema / properties / visibility / properties / overall / required
      Previous value: -[
      -  "terms_scored",
      -  "storefronts_scored",
      -  "weights_source",
      -  "weights_month",
      -  "formula_version"
      -]New value: +[
      +  "terms_scored",
      +  "storefronts_scored",
      +  "weights_source",
      +  "weights_month",
      +  "compared",
      +  "formula_version"
      +]
  2. Changed3 schema fields changed
    • addedOutput schema / properties / visibility / properties / overall
      Added 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"
      +  ]
      +}
    • changedOutput schema / properties / visibility / properties / storefronts / description
      Previous 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."
    • addedOutput schema / properties / visibility / properties / storefronts / items / properties / weight
      Added 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"
      +  ]
      +}
  3. Changed14 schema fields changed
    • changedInput schema / properties / include / description
      Previous 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\"`."
    • addedOutput schema / properties / keywords / items / properties / brand
      Added 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"
      +  ]
      +}
    • addedOutput schema / properties / keywords / items / properties / points_change
      Added 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"
      +  ]
      +}
    • changedOutput schema / properties / visibility / description
      Previous 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."
    • changedOutput schema / properties / visibility / properties / ranked / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • addedOutput schema / properties / visibility / properties / score
      Added 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"
      +  ]
      +}
    • addedOutput schema / properties / visibility / properties / storefronts
      Added 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"
      +}
    • changedOutput schema / properties / visibility / properties / top1 / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • changedOutput schema / properties / visibility / properties / top10 / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • changedOutput schema / properties / visibility / properties / top100 / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • changedOutput schema / properties / visibility / properties / top3 / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • changedOutput schema / properties / visibility / properties / top30 / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • changedOutput schema / properties / visibility / properties / top50 / type
      Previous value: -"integer"New value: +[
      +  "integer",
      +  "null"
      +]
    • addedOutput schema / properties / visibility / required
      Added value: +[
      +  "storefronts"
      +]
  4. First observed

TDQS

A4.6/5.0
Behavior4/5

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.

Conciseness4/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines5/5

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.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources