sensortower-mcp
# sensortower-mcp
[](https://www.npmjs.com/package/sensortower-mcp)
[](https://github.com/mike-computer/sensortower_mcp/actions/workflows/ci.yml)
[](LICENSE)
An [MCP](https://modelcontextprotocol.io) server for the
[SensorTower](https://sensortower.com) APIs: app download and revenue
estimates, store leaderboards, App Store featuring and ASO, reviews, Apple
Search Ads, and audience insights. 29 tools over ~76 endpoints, with the API's
unit, shape and error quirks already handled.
You need a SensorTower API key. What each tool can return depends on what your
organisation's subscription covers.
## Quick start
Requires Node.js 22 or newer.
```bash
npx -y sensortower-mcp --help
```
Then register it with your MCP client, passing the key through the client's
`env` block.
### Claude Code
```bash
claude mcp add sensortower -e SENSORTOWER_API_KEY=your-key -- npx -y sensortower-mcp
```
Or check a project-scoped `.mcp.json` into your repo and keep the key in your
shell environment. Claude Code expands `${VAR}` in this file:
```json
{
"mcpServers": {
"sensortower": {
"command": "npx",
"args": ["-y", "sensortower-mcp"],
"env": { "SENSORTOWER_API_KEY": "${SENSORTOWER_API_KEY}" }
}
}
}
```
### Claude Desktop
In `claude_desktop_config.json`:
```json
{
"mcpServers": {
"sensortower": {
"command": "npx",
"args": ["-y", "sensortower-mcp"],
"env": { "SENSORTOWER_API_KEY": "your-key" }
}
}
}
```
### Cursor
In `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project), using the
same `mcpServers` block as Claude Desktop.
### VS Code
In `.vscode/mcp.json`. VS Code asks for the key once and stores it securely:
```json
{
"inputs": [
{ "type": "promptString", "id": "sensortower-key", "description": "SensorTower API key", "password": true }
],
"servers": {
"sensortower": {
"type": "stdio",
"command": "npx",
"args": ["-y", "sensortower-mcp"],
"env": { "SENSORTOWER_API_KEY": "${input:sensortower-key}" }
}
}
}
```
## The API key
Read from, in order:
1. `SENSORTOWER_API_KEY` in the environment. This is how an MCP client should
supply it (see Quick start).
2. `--env-file <path>`: a dotenv-style file that defines `SENSORTOWER_API_KEY`,
e.g. `"args": ["-y", "sensortower-mcp", "--env-file", "/abs/path/.env"]`.
The file must exist: Node.js itself intercepts `--env-file` and exits with
`node: <path>: not found` when it does not.
3. A `.env` file found by walking up from the working directory, stopping at the
first directory that contains `.git`. The working directory is whatever the
MCP client launches the server in, which is often not your project, so
prefer option 1.
There is deliberately **no flag or tool argument that takes the key**.
SensorTower sends it as a URL *query parameter*, so anything in `argv` would
leak into `ps` output and shell history. Every URL this server emits — including
inside error messages — has the token replaced with `<TOKEN>`.
If the key is missing the server still starts and still lists its tools; the
error surfaces on the first call, where it is readable.
## Two things worth knowing before you spend anything
**Quota is charged per HTTP request, not per row**, against a shared monthly
organisation limit. One request covering three months costs exactly what one
covering a single day costs. Widen date ranges, batch `app_ids` (100 per
request), and do not loop.
**`sensortower_api_usage` is free**, and is the only endpoint that genuinely
validates the key — `/v1/{os}/apps` returns 200 with full metadata even for a
garbage token, so a revoked key looks healthy there and fails everywhere else.
Every tool accepts `dry_run: true`, which prints the exact URL it would call and
charges nothing.
## Tools
| Tool | What it answers |
|---|---|
| `sensortower_api_usage` | How much quota is left (free) |
| `sensortower_reference` | Valid category ids, chart types, networks, review tags, segment formats (offline) |
| `sensortower_raw_get` | Any endpoint without a dedicated tool |
| `sensortower_app_metadata` | Name, publisher, rating, and the unified_app_id other tools need |
| `sensortower_app_estimates` | Downloads and revenue over time |
| `sensortower_app_active_users` | DAU / WAU / MAU |
| `sensortower_app_rank` | Current category ranks, or a rank history |
| `sensortower_app_version_history` | Releases, or store-listing changes |
| `sensortower_app_demographics` | Age and gender of an app's users |
| `sensortower_app_in_app_purchases` | Top IAP SKUs (iOS) |
| `sensortower_app_overlap` | What else this app's users use |
| `sensortower_app_retention` | Retention curves |
| `sensortower_top_charts` | The store top chart for a category |
| `sensortower_top_apps` | Leaderboard by downloads, revenue or active users |
| `sensortower_top_publishers` | Leaderboard by publisher |
| `sensortower_store_summary` | Category-wide totals (or game genres) |
| `sensortower_market_size` | Market totals sliced by any dimension |
| `sensortower_top_advertisers` | Ad share of voice, or one app's ad rank |
| `sensortower_top_creatives` | The most-seen ad creatives |
| `sensortower_featured` | Apple "Today" stories, or a category's featured apps |
| `sensortower_featured_history` | One app's featuring history and download impact |
| `sensortower_keywords` | Who ranks for a term, or what terms an app ranks for |
| `sensortower_keyword_history` | Keyword rank and traffic over time |
| `sensortower_reviews` | Individual reviews with sentiment and tags |
| `sensortower_ratings` | Star ratings over time |
| `sensortower_review_breakdown` | Review counts by rating, sentiment or tag |
| `sensortower_search_ads` | Apple Search Ads share of voice |
| `sensortower_audience_demographics` | Age and gender of an audience segment |
| `sensortower_audience_affinity` | What a segment is into: personas, channels, brands, apps |
## What the server normalises for you
The raw API is inconsistent in ways that are easy to get wrong and expensive to
get wrong twice:
- **Revenue is in cents everywhere.** Converted, and renamed `*_usd`.
- **`sales_report_estimates` returns three different key sets** depending on
`os` (iOS `iu/ir/au/ar` with country in `cc`; Android `u/r` with country in
`c`; unified spelled out). All three collapse to
`app_id / country / date / downloads / revenue_usd`.
- **`custom_tags` is 213 keys and ~94% of every leaderboard row.** Dropped
unless `keep_custom_tags: true`.
- **Artwork URLs are 75–85% of a featured payload** (~1 MB per category-week).
Dropped unless `keep_artwork: true`.
- **Leaderboards arrive ordered but unnumbered, and carry only ids.** Rank
numbers and app names are added.
- **`featured/impacts` `*_series` arrays carry no dates** — index 0 is
`start_date`, one element per day. `series: true` zips them onto real dates.
- **`version_history` returns a dict keyed by `"YYYY-MM-DD HH:MM:SS UTC"`**, and
`app_update_history` returns `[date, {mostly nulls}]` pairs. Both flattened.
- Results are capped at 60 KB with a note telling the caller to narrow the
query, because tool output lands directly in a model's context window.
## Errors are made actionable, not swallowed
A failing call returns `isError` with the redacted URL and, where the failure
mode is a known trap, a hint:
- `422 Bundle unsupported bundle: retention` → the OpenAPI contract's
`/v1/facets/metrics?suffix` path keys are not bundle values; use
`retention_monthly`.
- `401 ... is not authorized for the user` → a **subscription** limit, not a bad
key. Do not retry.
- `404` with an HTML body → the path is wrong; several endpoints are iOS-only.
- `422 App not found.` → likely a store id sent to a unified endpoint.
A 422 body enumerates the valid values for whatever you got wrong, and is more
authoritative than the OpenAPI contract. It is passed through verbatim.
## Development
```bash
git clone https://github.com/mike-computer/sensortower_mcp.git
cd sensortower_mcp
npm install # also builds dist/ via `prepare`
npm test # build + unit, integration and stdio tests
npm run test:coverage # same, with an 80% line-coverage floor
npm run inspector # poke at the tools in the MCP Inspector
```
The test suite needs no API key and spends no quota: `fetch` is mocked, and the
stdio tests run with no key at all. It covers every tool's exact request URL
(a dry-run table that fails if a tool has no case), normalisation against
API-shaped fixtures, retry and error-hint behaviour, key lookup order, and a
check that the key never appears in any tool output.
`test/live.mjs` is an opt-in check against the real API. It calls only
`sensortower_api_usage`, which is free:
```bash
ST_LIVE=1 SENSORTOWER_API_KEY=your-key node test/live.mjs
```
## Releasing
```bash
npm version patch # or minor / major; also syncs server.json
git push --follow-tags
```
Pushing the `v*` tag runs `.github/workflows/release.yml`. It publishes to npm
through Trusted Publishing, with provenance, and then publishes `server.json`
to the [MCP Registry](https://registry.modelcontextprotocol.io).
## License
[MIT](LICENSE). This is an unofficial project. It is not affiliated with or
endorsed by Sensor Tower Inc. "SensorTower" is used only to describe the API
this server talks to.
TDQS
Scored across 29 tools
Each tool maps to a distinct resource+action, and the descriptions explicitly pre-empt the trickiest overlaps (app_demographics vs audience_demographics licensing split, reviews vs ratings vs review_breakdown, app_active_users vs top_apps leaderboards). A few boundaries remain thin (app_retention vs app_active_users, featured vs featured_history) but descriptions make them selectable.
Every tool uses a uniform sensortower_ snake_case prefix followed by a readable noun phrase, with grouping prefixes (app_*, top_*, audience_*, keyword*) that telegraph the resource family. No mixed conventions or ambiguous verbs anywhere.
29 tools is on the heavy side, but the descriptions justify most of them as distinct curated endpoints over a very large analytics API, with sensortower_raw_get explicitly serving the ~50 uncovered endpoints. Still, at nearly 30 dedicated tools it sits above the comfortable range and invites selection errors.
The surface covers apps, estimates, ranks, demographics, retention, IAP, overlap, charts, leaderboards, publishers, store/market aggregates, advertisers/creatives, featured, keywords, reviews/ratings, search ads and audience segments. The raw_get escape hatch plus the offline reference tool close any residual gaps.