storelift-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +75 -0
  2. package/index.mjs +133 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # storelift-mcp
2
+
3
+ App Store & Google Play ranking data as an MCP server. Ask your assistant which
4
+ keywords you dropped on, who outranks you, and whether AI assistants mention
5
+ your app at all — without opening a dashboard.
6
+
7
+ Backed by [Storelift](https://storelift.vomcore.com), which measures keyword
8
+ ranks, rivals and AI visibility nightly. **No store credentials required** —
9
+ ranks are read from the public storefront.
10
+
11
+ ## Requirements
12
+
13
+ - A **Studio** plan (the Public API is enabled on that plan)
14
+ - An API key: Storelift → Settings → API keys → **Generate key**
15
+
16
+ ## Install
17
+
18
+ Claude Code:
19
+
20
+ ```bash
21
+ claude mcp add storelift -e STORELIFT_API_KEY=sl_live_... -- npx -y storelift-mcp
22
+ ```
23
+
24
+ Claude Desktop (`claude_desktop_config.json`):
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "storelift": {
30
+ "command": "npx",
31
+ "args": ["-y", "storelift-mcp"],
32
+ "env": { "STORELIFT_API_KEY": "sl_live_..." }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Tools
39
+
40
+ | Tool | Returns |
41
+ |---|---|
42
+ | `list_apps` | tracked apps (id, name, countries, keywords) |
43
+ | `get_keywords` | keyword ranks for one app in one country |
44
+ | `get_rivals` | apps ranking **above** you, with their rank and yours |
45
+ | `get_ai_visibility` | whether assistants name your app, per engine (Claude / ChatGPT / Gemini) |
46
+ | `get_history` | rank history, `[day, rank]` points |
47
+
48
+ ## Reading the data
49
+
50
+ Keyword results carry **three distinct states**, and they are not the same thing:
51
+
52
+ - `measured: false` — the query **could not be measured**
53
+ - `rank: null` (with `measured: true`) — measured, but **absent** from the top results
54
+ - `rank: <number>` — the rank
55
+
56
+ Counting an unmeasured day as zero produces a false chart. The tool descriptions
57
+ repeat this so the model does not flatten the three into one.
58
+
59
+ AI visibility is an **observation, not a ranking**: a model's knowledge is frozen
60
+ at a date and the answer is not identical every time. Read the trend, not a
61
+ single measurement.
62
+
63
+ ## Configuration
64
+
65
+ | Variable | Default | Purpose |
66
+ |---|---|---|
67
+ | `STORELIFT_API_KEY` | — | required; the server exits with a message if unset |
68
+ | `STORELIFT_API` | `https://storelift.vomcore.com` | override the API base |
69
+
70
+ No dependencies — a single file speaking JSON-RPC over stdio. Runs with
71
+ `node index.mjs` just as well as through `npx`.
72
+
73
+ ## License
74
+
75
+ MIT
package/index.mjs ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ // Storelift MCP sunucusu.
3
+ //
4
+ // NE İŞE YARIYOR: Claude (ya da MCP konuşan başka bir istemci) uygulamanın
5
+ // sıralarını, rakiplerini ve AI görünürlüğünü DOĞRUDAN okuyabiliyor.
6
+ // "Kelimece'nin TR'de düşen kelimeleri hangileri" diye sorup cevabı panoya
7
+ // bakmadan alıyorsun.
8
+ //
9
+ // KURULUM (Claude Desktop → claude_desktop_config.json):
10
+ // {
11
+ // "mcpServers": {
12
+ // "storelift": {
13
+ // "command": "npx",
14
+ // "args": ["-y", "storelift-mcp"],
15
+ // "env": { "STORELIFT_API_KEY": "sl_live_..." }
16
+ // }
17
+ // }
18
+ // }
19
+ //
20
+ // BAĞIMLILIK YOK: stdio üzerinden JSON-RPC elle konuşuluyor. Tek dosya, npm
21
+ // kurulumu gerektirmeden `node index.mjs` ile de çalışır.
22
+
23
+ const BASE = process.env.STORELIFT_API || "https://storelift.vomcore.com";
24
+ const KEY = process.env.STORELIFT_API_KEY;
25
+
26
+ if (!KEY) {
27
+ console.error("STORELIFT_API_KEY tanımlı değil. Ayarlar → API anahtarları'ndan bir anahtar üret.");
28
+ process.exit(1);
29
+ }
30
+
31
+ async function api(path) {
32
+ const res = await fetch(BASE + path, { headers: { "x-api-key": KEY } });
33
+ const body = await res.json().catch(() => ({}));
34
+ if (!res.ok) throw new Error(body.error || `HTTP ${res.status}`);
35
+ return body;
36
+ }
37
+
38
+ const TOOLS = [
39
+ {
40
+ name: "list_apps",
41
+ description: "Storelift hesabındaki izlenen uygulamaları listeler (id, ad, ülkeler, kelimeler).",
42
+ inputSchema: { type: "object", properties: {} },
43
+ run: () => api("/v1/apps"),
44
+ },
45
+ {
46
+ name: "get_keywords",
47
+ description:
48
+ "Bir uygulamanın bir ülkedeki kelime sıralarını verir. ÜÇ DURUM AYRIDIR: measured=false ölçülemedi, rank=null ölçüldü ama ilk sonuçlarda yok, rank=sayı sıra. Bunları birbirine karıştırma.",
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ appId: { type: "string", description: "list_apps'ten gelen uygulama kimliği" },
53
+ country: { type: "string", description: "ülke kodu, ör. tr" },
54
+ platform: { type: "string", enum: ["ios", "android"] },
55
+ },
56
+ required: ["appId"],
57
+ },
58
+ run: (a) => api(`/v1/apps/${a.appId}/keywords?country=${a.country || ""}&platform=${a.platform || "ios"}`),
59
+ },
60
+ {
61
+ name: "get_rivals",
62
+ description:
63
+ "Aramada uygulamanın ÜSTÜNDE çıkan uygulamalar. Her rakip için hangi kelimede kaçıncı olduğu ve bizim sıramız birlikte döner (ourRank null ise o kelimede hiç görünmüyoruz).",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: { appId: { type: "string" }, country: { type: "string" }, platform: { type: "string", enum: ["ios", "android"] } },
67
+ required: ["appId"],
68
+ },
69
+ run: (a) => api(`/v1/apps/${a.appId}/rivals?country=${a.country || ""}&platform=${a.platform || "ios"}`),
70
+ },
71
+ {
72
+ name: "get_ai_visibility",
73
+ description:
74
+ "Asistanlara sorulan doğal sorularda uygulamanın anılıp anılmadığı — motor başına (Claude/ChatGPT/Gemini). Bu bir sıralama değil gözlemdir; tek ölçüm kanıt değildir, eğilime bak.",
75
+ inputSchema: { type: "object", properties: { appId: { type: "string" }, country: { type: "string" } }, required: ["appId"] },
76
+ run: (a) => api(`/v1/apps/${a.appId}/ai?country=${a.country || ""}`),
77
+ },
78
+ {
79
+ name: "get_history",
80
+ description: "Bir ülkedeki kelime sıra geçmişi. Seri noktaları [gün, sıra] biçiminde; sıra null ise o gün ilk sonuçlarda yoktuk.",
81
+ inputSchema: { type: "object", properties: { appId: { type: "string" }, country: { type: "string" } }, required: ["appId"] },
82
+ run: (a) => api(`/v1/apps/${a.appId}/history?country=${a.country || ""}`),
83
+ },
84
+ ];
85
+
86
+ const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
87
+ const ok = (id, result) => send({ jsonrpc: "2.0", id, result });
88
+ const err = (id, message) => send({ jsonrpc: "2.0", id, error: { code: -32000, message } });
89
+
90
+ async function handle(req) {
91
+ const { id, method, params } = req;
92
+ if (method === "initialize") {
93
+ return ok(id, {
94
+ protocolVersion: "2024-11-05",
95
+ capabilities: { tools: {} },
96
+ serverInfo: { name: "storelift", version: "1.0.0" },
97
+ });
98
+ }
99
+ if (method === "notifications/initialized") return; // bildirim, yanıt beklemez
100
+ if (method === "tools/list") {
101
+ return ok(id, { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
102
+ }
103
+ if (method === "tools/call") {
104
+ const tool = TOOLS.find((t) => t.name === params?.name);
105
+ if (!tool) return err(id, `bilinmeyen araç: ${params?.name}`);
106
+ try {
107
+ const data = await tool.run(params.arguments || {});
108
+ return ok(id, { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] });
109
+ } catch (e) {
110
+ // Hata metni araca DÖNÜYOR, sessizce yutulmuyor: model "veri yok" ile
111
+ // "okuyamadım" arasındaki farkı görmezse yanlış sonuç anlatır.
112
+ return ok(id, { content: [{ type: "text", text: `HATA: ${e.message}` }], isError: true });
113
+ }
114
+ }
115
+ if (id !== undefined) err(id, `desteklenmeyen metot: ${method}`);
116
+ }
117
+
118
+ let buf = "";
119
+ process.stdin.setEncoding("utf8");
120
+ process.stdin.on("data", async (chunk) => {
121
+ buf += chunk;
122
+ let i;
123
+ while ((i = buf.indexOf("\n")) >= 0) {
124
+ const line = buf.slice(0, i).trim();
125
+ buf = buf.slice(i + 1);
126
+ if (!line) continue;
127
+ try {
128
+ await handle(JSON.parse(line));
129
+ } catch {
130
+ /* bozuk satır yok sayılır — akış kapanmamalı */
131
+ }
132
+ }
133
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "storelift-mcp",
3
+ "version": "1.0.0",
4
+ "description": "App Store & Google Play keyword ranks, rivals and AI visibility as an MCP server",
5
+ "type": "module",
6
+ "bin": {
7
+ "storelift-mcp": "index.mjs"
8
+ },
9
+ "files": [
10
+ "index.mjs",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "modelcontextprotocol",
16
+ "mcp-server",
17
+ "claude",
18
+ "aso",
19
+ "app-store-optimization",
20
+ "app-store",
21
+ "google-play",
22
+ "keyword-rank",
23
+ "ai-visibility"
24
+ ],
25
+ "homepage": "https://storelift.vomcore.com",
26
+ "bugs": {
27
+ "email": "hello@vomcore.com"
28
+ },
29
+ "author": "Vomcore",
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }