Skip to main content
Glama
README.md
# sensortower-mcp

[![npm](https://img.shields.io/npm/v/sensortower-mcp)](https://www.npmjs.com/package/sensortower-mcp)
[![CI](https://github.com/mike-computer/sensortower_mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/mike-computer/sensortower_mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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

A3.7/5.0

Scored across 29 tools

Disambiguation4/5

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.

Naming Consistency5/5

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.

Tool Count3/5

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.

Completeness5/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues