Metadata-Version: 2.5
Name: kranked-mcp
Version: 0.2.3
Summary: MCP server exposing App Store keyword intelligence — difficulty, popularity, and live rank — to any MCP client. The GUI-free companion to the Kranked ASO app.
Project-URL: Homepage, https://github.com/akoskomuves/kranked
Author-email: Akos Komuves <akos@tallpoppy.xyz>
License: MIT
License-File: LICENSE
Keywords: app-store,app-store-optimization,aso,ios,keywords,mcp
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Requires-Dist: mcp<3,>=2
Requires-Dist: uvicorn[standard]>=0.31
Provides-Extra: appleads
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'appleads'
Description-Content-Type: text/markdown

# Kranked MCP

An [MCP](https://modelcontextprotocol.io) server that exposes **App Store keyword
intelligence** — keyword difficulty, popularity, and live rank — as tools any MCP
client (Claude, etc.) can call. It's the headless companion to the
[Kranked](https://github.com/akoskomuves/kranked) ASO app: same scoring, no GUI.

Stateless and zero-config: every tool is a live call to Apple's public endpoints. Ranks
come from the App Store's own search ordering; the iTunes Search API is used for app
discovery and metadata, and search-hints for keyword popularity. **No database, no API
keys.** Runs locally via `uvx` or as a hosted server.

Speaks MCP **2026-07-28** (Python SDK v2), and stays backwards compatible with 2025-era
clients from the same server. Era is selected per request from the `MCP-Protocol-Version`
header, so over stdio — which has no headers — the server answers the 2025 `initialize`
handshake. Nothing to configure either way; clients negotiate it themselves.

## Tools (free / open core)

| Tool | What it answers |
|---|---|
| `search_apps` | Find apps (and their `app_id`) matching a term |
| `check_keyword` | **One-shot report:** difficulty + popularity + KEI + competitors, and your app's rank |
| `keyword_difficulty` | How hard a keyword is to rank for (0–100), with the top-10 competitors |
| `keyword_popularity` | How searched a keyword is (0–100) |
| `popular_keywords` | The most-searched keywords in a category, ranked |
| `keyword_suggestions` | Apple's autocomplete hints for a seed term |

All tools take a two-letter `country` (default `us`).

### How the scores work

- **Difficulty (0–100)** — analyzes the top-10 ranking apps' review volume and rating
  quality. More established competitors = harder. Labeled Very Easy → Very Hard.
- **Popularity (0–100)** — two sources, and every response says which one it used.
  With Apple Ads credentials configured it is Apple's **own `searchPopularity`**, a real
  search-volume index. Without them, `keyword_popularity` falls back to a free suggest-based
  proxy (80 = Apple auto-suggests the exact term, 50 = a related term, 20 = neither) and
  `popular_keywords` reports `source: unconfigured` rather than inventing numbers. The two
  scales are not comparable — check `source` before ranking terms against each other.
- **KEI** — Keyword Efficiency Index = popularity / difficulty. Higher is a better bet.
- **Rank** comes from the App Store's own search ordering, not the iTunes Search API. The
  documented Search API does not return the store's ordering: measured across five US terms
  it matched on 29/60 positions against 60/60 for the store endpoint, and it caps out around
  150–185 results instead of 200. The Search API is still used for app discovery and metadata.

### Real popularity numbers (BYOK)

`searchPopularity` comes from Apple's own Ads Platform API, so it needs an Apple Search Ads
account — **your** account, keys stay on your machine, no ad spend required. Install the
extra and set the credentials:

```bash
uvx --with 'kranked-mcp[appleads]' kranked-mcp
```

| Env var | Purpose |
|---|---|
| `KRANKED_APPLEADS_CLIENT_ID` | Ads API client id |
| `KRANKED_APPLEADS_TEAM_ID` | Ads API team id |
| `KRANKED_APPLEADS_KEY_ID` | Ads API key id |
| `KRANKED_APPLEADS_ORG_ID` | your Ads account (org) id |
| `KRANKED_APPLEADS_PRIVATE_KEY` | the ES256 signing key as PEM text… |
| `KRANKED_APPLEADS_PRIVATE_KEY_PATH` | …or a path to the PEM file instead |

All five are required (either key form counts as one).

Unconfigured is a normal state: the tools degrade to the free proxy instead of failing.

## Install

On PyPI — nothing to clone or build. `uvx` fetches and runs it on demand. Needs Python
3.10+ and [uv](https://docs.astral.sh/uv/) (`brew install uv`). Add to your MCP client
config:

```json
{
  "mcpServers": {
    "kranked": {
      "command": "uvx",
      "args": ["kranked-mcp"]
    }
  }
}
```

For Claude Code: `claude mcp add kranked -- uvx kranked-mcp`

### Source

The sdist on PyPI carries the full source and the test suite — it's on the
[Download files](https://pypi.org/project/kranked-mcp/#files) tab. Unpack it and
`uv sync`, then point your client at that directory:

```json
{
  "mcpServers": {
    "kranked": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/kranked-mcp", "run", "kranked-mcp"]
    }
  }
}
```

Run the tests — they're offline (pure scoring plus server wiring driven through the real
ASGI app), so they don't hit Apple and can't flake on rate limiting:

```bash
uv run pytest
```

## Self-hosting (remote MCP)

Kranked runs in two modes from the same code:

| Mode | Transport | Use | Command |
|---|---|---|---|
| **Local** | stdio | one user, on your machine | `kranked-mcp` |
| **Hosted** | streamable-HTTP at `/mcp` | shared server, many users | `kranked-mcp-serve` |

The hosted server is stateless, so it scales horizontally — no sticky routing, no session
affinity. Since MCP 2026-07-28 that is the protocol's own model rather than an opt-in: there
is no `initialize` handshake and no session id, and clients negotiate up front with
`server/discover` instead. Deploy the included `Dockerfile` to any container host (Railway,
Fly, Render, …); it reads `PORT` from the environment and exposes `GET /health` for liveness
checks.

`server/discover` and `tools/list` carry a one-hour public cache hint (`ttlMs` /
`cacheScope`), so clients can hold the tool list instead of re-listing it every session.

### Deploy to Railway

Deployment is declared in `.railway/railway.ts` (Railway Infrastructure as Code): GitHub
source, Dockerfile build, `/health` healthcheck, restart policy. To deploy:

```bash
# one-time
npm i -g @railway/cli && railway login

# from the repo root
railway init            # create/select a project
railway up              # build the Dockerfile and deploy
railway domain          # get a public https URL
```

The upstream repo is connected as the service's GitHub source, so pushing to `main` deploys.
Set `KRANKED_CACHE_TTL` in the service's Variables if you want a longer/shorter cache.

> **Verify against the live server, not the build log.** `check_keyword` for "keyword tracker"
> should report `total_results` around 247. A number in the low 180s means the old iTunes
> Search API path is answering and the deploy did not take. This is not theoretical: before
> auto-deploy was connected, the server sat on 2026-08-01 code for a month, and served old
> ranks for a full day after the fix was merged, because pushing looked like deploying.

### Changing deployment config

`.railway/railway.ts` replaced the deprecated `railway.toml` (which stops working
2026-12-01). To edit it:

```bash
npm install railway --no-save   # needed only for plan/apply; not a project dependency
railway config plan             # preview
railway config apply --yes      # push
```

Two things worth knowing if you regenerate it. `railway config migrate` is **lossy** — it
comments out `builder` and `dockerfilePath`, which would have silently left this service on
Railpack rather than the Dockerfile. And `railway config pull` returns only what lives in
Railway's own settings, so anything that used to come from `railway.toml` has to be carried
across by hand. `restartPolicyType` is deliberately absent: `ON_FAILURE` is Railway's
default, so it round-trips as null and leaves the plan permanently dirty.

Then point a client at the URL:

```bash
claude mcp add --transport http kranked https://your-app.up.railway.app/mcp
```

### Config

| Env var | Default | Purpose |
|---|---|---|
| `PORT` / `HOST` | `8000` / `0.0.0.0` | bind address |
| `KRANKED_CACHE_TTL` | `3600` | seconds to cache upstream responses (`0` disables) |
| `KRANKED_CACHE_MAX` | `2000` | max cached entries |

**Caching is not optional for a hosted deployment.** All users' requests leave from one IP,
so without it you hit Apple's per-IP throttle immediately. Difficulty/popularity change
slowly, so cached results stay useful for hours. For multiple instances, move the cache to Redis.

Setting `KRANKED_API_KEY` gates every endpoint except `GET /health` behind a bearer token;
unauthenticated callers get a 401. Leave it unset and the server is open, which is what you
want for a local stdio run. Usage metering for a paid tier is still roadmap.

## Rate limiting

The iTunes API throttles unauthenticated callers per IP with a 403/429 and a non-JSON
body. The server serializes requests through a global throttle and retries throttles
with exponential backoff + jitter, so it degrades gracefully instead of surfacing bogus
decoding errors.

## License

MIT © Akos Komuves
