pabal-web-mcp 1.3.4 → 1.3.6
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.
- package/dist/bin/mcp-server.js +278 -244
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -2380,6 +2380,8 @@ import fs11 from "fs";
|
|
|
2380
2380
|
import path11 from "path";
|
|
2381
2381
|
import { z as z6 } from "zod";
|
|
2382
2382
|
import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
|
|
2383
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2384
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2383
2385
|
|
|
2384
2386
|
// src/utils/registered-apps.util.ts
|
|
2385
2387
|
import fs10 from "fs";
|
|
@@ -2413,45 +2415,28 @@ function findRegisteredApp(slug, filePath) {
|
|
|
2413
2415
|
const app = apps.find((a) => a.slug === slug);
|
|
2414
2416
|
return { app, path: usedPath };
|
|
2415
2417
|
}
|
|
2416
|
-
function getSupportedLocalesForSlug(slug, platform, filePath) {
|
|
2417
|
-
const { app, path: usedPath } = findRegisteredApp(slug, filePath);
|
|
2418
|
-
if (!app) return { supportedLocales: [], path: usedPath };
|
|
2419
|
-
if (platform === "ios") {
|
|
2420
|
-
return {
|
|
2421
|
-
supportedLocales: app.appStore?.supportedLocales || [],
|
|
2422
|
-
path: usedPath
|
|
2423
|
-
};
|
|
2424
|
-
}
|
|
2425
|
-
return {
|
|
2426
|
-
supportedLocales: app.googlePlay?.supportedLocales || [],
|
|
2427
|
-
path: usedPath
|
|
2428
|
-
};
|
|
2429
|
-
}
|
|
2430
2418
|
|
|
2431
2419
|
// src/tools/keyword-research.ts
|
|
2432
2420
|
var TOOL_NAME = "keyword-research";
|
|
2433
2421
|
var keywordResearchInputSchema = z6.object({
|
|
2434
2422
|
slug: z6.string().trim().describe("Product slug"),
|
|
2435
|
-
locale: z6.string().trim().describe(
|
|
2423
|
+
locale: z6.string().trim().describe(
|
|
2424
|
+
"Locale code (e.g., en-US, ko-KR). Stored under .aso/keywordResearch/products/[slug]/locales/."
|
|
2425
|
+
),
|
|
2436
2426
|
platform: z6.enum(["ios", "android"]).default("ios").describe("Store to target ('ios' or 'android'). Run separately per platform."),
|
|
2437
2427
|
country: z6.string().length(2).optional().describe(
|
|
2438
2428
|
"Two-letter store country code. If omitted, derived from locale region (e.g., ko-KR -> kr), else 'us'."
|
|
2439
2429
|
),
|
|
2440
|
-
seedKeywords: z6.array(z6.string().trim()).default([])
|
|
2430
|
+
seedKeywords: z6.array(z6.string().trim()).default([]),
|
|
2441
2431
|
competitorApps: z6.array(
|
|
2442
2432
|
z6.object({
|
|
2443
2433
|
appId: z6.string().trim().describe("App ID (package name or iOS ID/bundle)"),
|
|
2444
2434
|
platform: z6.enum(["ios", "android"])
|
|
2445
2435
|
})
|
|
2446
|
-
).default([])
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
"Optional JSON string with research results (e.g., from mcp-appstore tools). If provided, saves it to the output path."
|
|
2451
|
-
),
|
|
2452
|
-
researchDataPath: z6.string().trim().optional().describe(
|
|
2453
|
-
"Optional path to a JSON file containing research results. If set, file content is saved to the output path (preferred to avoid escape errors)."
|
|
2454
|
-
)
|
|
2436
|
+
).default([]),
|
|
2437
|
+
serverCwd: z6.string().trim().optional().describe("Path to mcp-appstore cwd (default: external-tools/mcp-appstore)"),
|
|
2438
|
+
serverCommand: z6.array(z6.string()).optional().describe("Command + args to start mcp-appstore (default: ['node','server.js'])"),
|
|
2439
|
+
numSuggestions: z6.number().int().positive().max(50).default(20).describe("Number of keyword suggestions per strategy (default: 20)")
|
|
2455
2440
|
});
|
|
2456
2441
|
var jsonSchema6 = zodToJsonSchema6(keywordResearchInputSchema, {
|
|
2457
2442
|
name: "KeywordResearchInput",
|
|
@@ -2460,86 +2445,28 @@ var jsonSchema6 = zodToJsonSchema6(keywordResearchInputSchema, {
|
|
|
2460
2445
|
var inputSchema6 = jsonSchema6.definitions?.KeywordResearchInput || jsonSchema6;
|
|
2461
2446
|
var keywordResearchTool = {
|
|
2462
2447
|
name: TOOL_NAME,
|
|
2463
|
-
description:
|
|
2464
|
-
|
|
2465
|
-
Run this before improve-public. It gives a concrete MCP-powered research plan and a storage path under .aso/keywordResearch/products/[slug]/locales/[locale]/. Optionally writes a template or saves raw JSON from mcp-appstore tools.`,
|
|
2448
|
+
description: "Runs mcp-appstore calls to collect 10\u201315 keywords per locale and saves to .aso/keywordResearch/products/[slug]/locales/[locale]/keyword-research-[platform]-[country].json.",
|
|
2466
2449
|
inputSchema: inputSchema6
|
|
2467
2450
|
};
|
|
2468
|
-
function buildTemplate({
|
|
2469
|
-
slug,
|
|
2470
|
-
locale,
|
|
2471
|
-
platform,
|
|
2472
|
-
country,
|
|
2473
|
-
seedKeywords,
|
|
2474
|
-
competitorApps
|
|
2475
|
-
}) {
|
|
2476
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2477
|
-
return {
|
|
2478
|
-
meta: {
|
|
2479
|
-
slug,
|
|
2480
|
-
locale,
|
|
2481
|
-
platform,
|
|
2482
|
-
country,
|
|
2483
|
-
seedKeywords,
|
|
2484
|
-
competitorApps,
|
|
2485
|
-
source: "mcp-appstore",
|
|
2486
|
-
updatedAt: timestamp
|
|
2487
|
-
},
|
|
2488
|
-
plan: {
|
|
2489
|
-
steps: [
|
|
2490
|
-
"Start mcp-appstore server (node server.js in external-tools/mcp-appstore).",
|
|
2491
|
-
"Confirm app IDs/locales: get_app_details(appId from config/registered-apps) to lock country/lang and 3\u20135 competitors.",
|
|
2492
|
-
"Discover competitors: search_app(term=seed keyword, num=10\u201320), get_similar_apps(appId=top competitor, num=10).",
|
|
2493
|
-
"Collect candidates (all of these, num=20\u201330): suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search; suggest_keywords_by_apps(apps=[top competitors]).",
|
|
2494
|
-
"Score shortlist: get_keyword_scores for 15\u201330 candidates per platform/country.",
|
|
2495
|
-
"Context check: analyze_reviews (num=100\u2013200) and fetch_reviews (num=50\u2013100) on top apps for language/tone cues.",
|
|
2496
|
-
"If keywordSuggestions/similar/reviews are sparse, rerun calls (add more competitors/seeds) until you have 10\u201315 strong keywords.",
|
|
2497
|
-
"For any recommended keyword without scores, rerun get_keyword_scores to fill traffic/difficulty.",
|
|
2498
|
-
"Keep rationale/nextActions in English by default unless you intentionally localize them."
|
|
2499
|
-
],
|
|
2500
|
-
note: "Run per platform/country. Save raw tool outputs plus curated top keywords (target 10\u201315 per locale: 2\u20133 high-traffic core, 4\u20136 mid-competition, 4\u20136 longtail)."
|
|
2501
|
-
},
|
|
2502
|
-
data: {
|
|
2503
|
-
raw: {
|
|
2504
|
-
searchApp: [],
|
|
2505
|
-
getAppDetails: [],
|
|
2506
|
-
similarApps: [],
|
|
2507
|
-
keywordSuggestions: {
|
|
2508
|
-
bySeeds: [],
|
|
2509
|
-
byCategory: [],
|
|
2510
|
-
bySimilarity: [],
|
|
2511
|
-
byCompetition: [],
|
|
2512
|
-
bySearchHints: [],
|
|
2513
|
-
byApps: []
|
|
2514
|
-
},
|
|
2515
|
-
keywordScores: [],
|
|
2516
|
-
reviewsAnalysis: [],
|
|
2517
|
-
reviewsRaw: []
|
|
2518
|
-
},
|
|
2519
|
-
summary: {
|
|
2520
|
-
recommendedKeywords: [],
|
|
2521
|
-
rationale: "",
|
|
2522
|
-
nextActions: "Feed 10\u201315 mixed keywords (core/mid/longtail) into improve-public Stage 1."
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
};
|
|
2526
|
-
}
|
|
2527
|
-
function saveJsonFile({
|
|
2528
|
-
researchDir,
|
|
2529
|
-
fileName,
|
|
2530
|
-
payload
|
|
2531
|
-
}) {
|
|
2532
|
-
fs11.mkdirSync(researchDir, { recursive: true });
|
|
2533
|
-
const outputPath = path11.join(researchDir, fileName);
|
|
2534
|
-
fs11.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2535
|
-
return outputPath;
|
|
2536
|
-
}
|
|
2537
2451
|
function normalizeKeywords(raw) {
|
|
2538
2452
|
if (!raw) return [];
|
|
2539
2453
|
if (Array.isArray(raw)) {
|
|
2540
|
-
return raw.map((k) => k.trim()).filter(
|
|
2454
|
+
return raw.map((k) => k.trim()).filter(Boolean);
|
|
2455
|
+
}
|
|
2456
|
+
return raw.split(",").map((k) => k.trim()).filter(Boolean);
|
|
2457
|
+
}
|
|
2458
|
+
async function callToolJSON(client, name, args) {
|
|
2459
|
+
const result = await client.callTool({ name, arguments: args });
|
|
2460
|
+
const text = result.content?.[0]?.text;
|
|
2461
|
+
if (!text) return null;
|
|
2462
|
+
try {
|
|
2463
|
+
return JSON.parse(text);
|
|
2464
|
+
} catch {
|
|
2465
|
+
return text;
|
|
2541
2466
|
}
|
|
2542
|
-
|
|
2467
|
+
}
|
|
2468
|
+
function ensureDir(dir) {
|
|
2469
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
2543
2470
|
}
|
|
2544
2471
|
async function handleKeywordResearch(input) {
|
|
2545
2472
|
const {
|
|
@@ -2549,34 +2476,28 @@ async function handleKeywordResearch(input) {
|
|
|
2549
2476
|
country,
|
|
2550
2477
|
seedKeywords = [],
|
|
2551
2478
|
competitorApps = [],
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
researchDataPath
|
|
2479
|
+
serverCwd,
|
|
2480
|
+
serverCommand,
|
|
2481
|
+
numSuggestions = 20
|
|
2556
2482
|
} = input;
|
|
2557
2483
|
const { config, locales } = loadProductLocales(slug);
|
|
2558
2484
|
const primaryLocale = resolvePrimaryLocale(config, locales);
|
|
2559
2485
|
const primaryLocaleData = locales[primaryLocale];
|
|
2560
|
-
const { app: registeredApp
|
|
2561
|
-
const
|
|
2486
|
+
const { app: registeredApp } = findRegisteredApp(slug);
|
|
2487
|
+
const resolvedCountry = country || (locale.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
|
|
2562
2488
|
const autoSeeds = [];
|
|
2563
|
-
|
|
2564
|
-
if (primaryLocaleData?.aso?.title) {
|
|
2565
|
-
autoSeeds.push(primaryLocaleData.aso.title);
|
|
2566
|
-
}
|
|
2489
|
+
if (primaryLocaleData?.aso?.title) autoSeeds.push(primaryLocaleData.aso.title);
|
|
2567
2490
|
const parsedKeywords = normalizeKeywords(primaryLocaleData?.aso?.keywords);
|
|
2568
2491
|
autoSeeds.push(...parsedKeywords.slice(0, 5));
|
|
2569
2492
|
if (config?.name) autoSeeds.push(config.name);
|
|
2570
2493
|
if (config?.tagline) autoSeeds.push(config.tagline);
|
|
2571
|
-
if (
|
|
2572
|
-
if (
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2494
|
+
if (registeredApp?.name) autoSeeds.push(registeredApp.name);
|
|
2495
|
+
if (platform === "ios" && registeredApp?.appStore?.name)
|
|
2496
|
+
autoSeeds.push(registeredApp.appStore.name);
|
|
2497
|
+
if (platform === "android" && registeredApp?.googlePlay?.name)
|
|
2498
|
+
autoSeeds.push(registeredApp.googlePlay.name);
|
|
2499
|
+
const seeds = Array.from(/* @__PURE__ */ new Set([...seedKeywords, ...autoSeeds])).slice(0, 10);
|
|
2500
|
+
const autoCompetitors = [];
|
|
2580
2501
|
if (platform === "ios") {
|
|
2581
2502
|
if (config?.appStoreAppId) {
|
|
2582
2503
|
autoCompetitors.push({ appId: String(config.appStoreAppId), platform });
|
|
@@ -2593,17 +2514,212 @@ async function handleKeywordResearch(input) {
|
|
|
2593
2514
|
platform
|
|
2594
2515
|
});
|
|
2595
2516
|
}
|
|
2596
|
-
} else
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2517
|
+
} else {
|
|
2518
|
+
if (config?.packageName) {
|
|
2519
|
+
autoCompetitors.push({ appId: config.packageName, platform });
|
|
2520
|
+
} else if (registeredApp?.googlePlay?.packageName) {
|
|
2521
|
+
autoCompetitors.push({
|
|
2522
|
+
appId: registeredApp.googlePlay.packageName,
|
|
2523
|
+
platform
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
const competitors = Array.from(
|
|
2528
|
+
new Map(
|
|
2529
|
+
[...competitorApps, ...autoCompetitors].map((c) => [
|
|
2530
|
+
`${c.platform}:${c.appId}`,
|
|
2531
|
+
c
|
|
2532
|
+
])
|
|
2533
|
+
).values()
|
|
2534
|
+
).slice(0, 5);
|
|
2535
|
+
const cwd = serverCwd || path11.resolve(process.cwd(), "external-tools", "mcp-appstore");
|
|
2536
|
+
const cmd = serverCommand?.[0] || "node";
|
|
2537
|
+
const args = serverCommand?.length ? serverCommand.slice(1) : ["server.js"];
|
|
2538
|
+
const transport = new StdioClientTransport({ command: cmd, args, cwd });
|
|
2539
|
+
const client = new Client(
|
|
2540
|
+
{ name: "keyword-research", version: "1.0.0" },
|
|
2541
|
+
{ capabilities: { tools: {} } }
|
|
2542
|
+
);
|
|
2543
|
+
await client.connect(transport);
|
|
2544
|
+
const raw = {
|
|
2545
|
+
searchApp: [],
|
|
2546
|
+
getAppDetails: [],
|
|
2547
|
+
similarApps: [],
|
|
2548
|
+
keywordSuggestions: {
|
|
2549
|
+
bySeeds: [],
|
|
2550
|
+
byCategory: [],
|
|
2551
|
+
bySimilarity: [],
|
|
2552
|
+
byCompetition: [],
|
|
2553
|
+
bySearchHints: [],
|
|
2554
|
+
byApps: []
|
|
2555
|
+
},
|
|
2556
|
+
keywordScores: [],
|
|
2557
|
+
reviewsAnalysis: [],
|
|
2558
|
+
reviewsRaw: []
|
|
2559
|
+
};
|
|
2560
|
+
for (const term of seeds.slice(0, 3)) {
|
|
2561
|
+
const res = await callToolJSON(client, "search_app", {
|
|
2562
|
+
term,
|
|
2563
|
+
platform,
|
|
2564
|
+
num: 20,
|
|
2565
|
+
country: resolvedCountry
|
|
2566
|
+
});
|
|
2567
|
+
raw.searchApp.push({
|
|
2568
|
+
query: term,
|
|
2569
|
+
platform,
|
|
2570
|
+
country: resolvedCountry,
|
|
2571
|
+
results: res?.results ?? res
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
for (const comp of competitors.slice(0, 5)) {
|
|
2575
|
+
const res = await callToolJSON(client, "get_app_details", {
|
|
2576
|
+
appId: comp.appId,
|
|
2577
|
+
platform: comp.platform,
|
|
2578
|
+
country: resolvedCountry
|
|
2579
|
+
});
|
|
2580
|
+
raw.getAppDetails.push(res);
|
|
2581
|
+
}
|
|
2582
|
+
if (competitors.length > 0) {
|
|
2583
|
+
const first = competitors[0];
|
|
2584
|
+
const res = await callToolJSON(client, "get_similar_apps", {
|
|
2585
|
+
appId: first.appId,
|
|
2586
|
+
platform,
|
|
2587
|
+
num: 10,
|
|
2588
|
+
country: resolvedCountry
|
|
2589
|
+
});
|
|
2590
|
+
raw.similarApps = res?.similarApps || res || [];
|
|
2591
|
+
}
|
|
2592
|
+
const firstComp = competitors[0]?.appId;
|
|
2593
|
+
const appsList = competitors.map((c) => c.appId);
|
|
2594
|
+
const suggestCalls = [
|
|
2595
|
+
[
|
|
2596
|
+
"suggest_keywords_by_seeds",
|
|
2597
|
+
{
|
|
2598
|
+
keywords: seeds,
|
|
2599
|
+
platform,
|
|
2600
|
+
num: numSuggestions,
|
|
2601
|
+
country: resolvedCountry
|
|
2602
|
+
},
|
|
2603
|
+
"bySeeds"
|
|
2604
|
+
],
|
|
2605
|
+
[
|
|
2606
|
+
"suggest_keywords_by_category",
|
|
2607
|
+
firstComp ? {
|
|
2608
|
+
appId: firstComp,
|
|
2609
|
+
platform,
|
|
2610
|
+
num: numSuggestions,
|
|
2611
|
+
country: resolvedCountry
|
|
2612
|
+
} : null,
|
|
2613
|
+
"byCategory"
|
|
2614
|
+
],
|
|
2615
|
+
[
|
|
2616
|
+
"suggest_keywords_by_similarity",
|
|
2617
|
+
firstComp ? {
|
|
2618
|
+
appId: firstComp,
|
|
2619
|
+
platform,
|
|
2620
|
+
num: numSuggestions,
|
|
2621
|
+
country: resolvedCountry
|
|
2622
|
+
} : null,
|
|
2623
|
+
"bySimilarity"
|
|
2624
|
+
],
|
|
2625
|
+
[
|
|
2626
|
+
"suggest_keywords_by_competition",
|
|
2627
|
+
firstComp ? {
|
|
2628
|
+
appId: firstComp,
|
|
2629
|
+
platform,
|
|
2630
|
+
num: numSuggestions,
|
|
2631
|
+
country: resolvedCountry
|
|
2632
|
+
} : null,
|
|
2633
|
+
"byCompetition"
|
|
2634
|
+
],
|
|
2635
|
+
[
|
|
2636
|
+
"suggest_keywords_by_search",
|
|
2637
|
+
{
|
|
2638
|
+
keywords: seeds.slice(0, 5),
|
|
2639
|
+
platform,
|
|
2640
|
+
num: numSuggestions,
|
|
2641
|
+
country: resolvedCountry
|
|
2642
|
+
},
|
|
2643
|
+
"bySearchHints"
|
|
2644
|
+
],
|
|
2645
|
+
[
|
|
2646
|
+
"suggest_keywords_by_apps",
|
|
2647
|
+
appsList.length ? {
|
|
2648
|
+
apps: appsList,
|
|
2649
|
+
platform,
|
|
2650
|
+
num: numSuggestions,
|
|
2651
|
+
country: resolvedCountry
|
|
2652
|
+
} : null,
|
|
2653
|
+
"byApps"
|
|
2654
|
+
]
|
|
2655
|
+
];
|
|
2656
|
+
for (const [tool, args2, key] of suggestCalls) {
|
|
2657
|
+
if (!args2) continue;
|
|
2658
|
+
const res = await callToolJSON(client, tool, args2);
|
|
2659
|
+
const suggestions = res?.suggestions || res || [];
|
|
2660
|
+
raw.keywordSuggestions[key] = suggestions;
|
|
2661
|
+
}
|
|
2662
|
+
for (const comp of competitors.slice(0, 2)) {
|
|
2663
|
+
const analysis = await callToolJSON(client, "analyze_reviews", {
|
|
2664
|
+
appId: comp.appId,
|
|
2665
|
+
platform: comp.platform,
|
|
2666
|
+
country: resolvedCountry,
|
|
2667
|
+
num: 150
|
|
2668
|
+
});
|
|
2669
|
+
raw.reviewsAnalysis.push(analysis);
|
|
2670
|
+
const rawReviews = await callToolJSON(client, "fetch_reviews", {
|
|
2671
|
+
appId: comp.appId,
|
|
2672
|
+
platform: comp.platform,
|
|
2673
|
+
country: resolvedCountry,
|
|
2674
|
+
num: 80
|
|
2675
|
+
});
|
|
2676
|
+
raw.reviewsRaw.push(rawReviews);
|
|
2677
|
+
}
|
|
2678
|
+
const candidateSet = /* @__PURE__ */ new Set();
|
|
2679
|
+
const addCandidates = (arr) => {
|
|
2680
|
+
if (!arr) return;
|
|
2681
|
+
if (Array.isArray(arr)) {
|
|
2682
|
+
arr.forEach((v) => {
|
|
2683
|
+
if (typeof v === "string") candidateSet.add(v);
|
|
2684
|
+
if (v?.keyword) candidateSet.add(String(v.keyword));
|
|
2685
|
+
if (v?.suggestions) addCandidates(v.suggestions);
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
seeds.forEach((s) => candidateSet.add(s));
|
|
2690
|
+
Object.values(raw.keywordSuggestions).forEach((arr) => addCandidates(arr));
|
|
2691
|
+
const candidates = Array.from(candidateSet).slice(0, 30);
|
|
2692
|
+
for (const kw of candidates) {
|
|
2693
|
+
const res = await callToolJSON(client, "get_keyword_scores", {
|
|
2694
|
+
keyword: kw,
|
|
2695
|
+
platform,
|
|
2696
|
+
country: resolvedCountry
|
|
2602
2697
|
});
|
|
2698
|
+
raw.keywordScores.push(res);
|
|
2603
2699
|
}
|
|
2604
|
-
const
|
|
2605
|
-
|
|
2606
|
-
|
|
2700
|
+
const scored = raw.keywordScores.map((s) => {
|
|
2701
|
+
const trafficScore = typeof s?.scores?.traffic?.score === "number" ? s.scores.traffic.score : parseFloat(s?.traffic?.score ?? s?.scores?.traffic?.score ?? "0") || 0;
|
|
2702
|
+
const difficultyScore = typeof s?.scores?.difficulty?.score === "number" ? s.scores.difficulty.score : parseFloat(
|
|
2703
|
+
s?.difficulty?.score ?? s?.scores?.difficulty?.score ?? "0"
|
|
2704
|
+
) || 0;
|
|
2705
|
+
return {
|
|
2706
|
+
keyword: s.keyword || s?.scores?.keyword,
|
|
2707
|
+
trafficScore,
|
|
2708
|
+
difficultyScore,
|
|
2709
|
+
raw: s
|
|
2710
|
+
};
|
|
2711
|
+
}).filter((s) => s.keyword);
|
|
2712
|
+
const recommended = scored.sort((a, b) => b.trafficScore - a.trafficScore).slice(0, 15);
|
|
2713
|
+
const summary = {
|
|
2714
|
+
recommendedKeywords: recommended.map((r) => ({
|
|
2715
|
+
keyword: r.keyword,
|
|
2716
|
+
trafficScore: r.trafficScore,
|
|
2717
|
+
difficultyScore: r.difficultyScore
|
|
2718
|
+
})),
|
|
2719
|
+
rationale: "Sorted by traffic score (desc). Blend of core/high-traffic and mid/longtail technical terms.",
|
|
2720
|
+
nextActions: "Feed top 10\u201315 into improve-public Stage 1. If any sections are empty (suggestions/reviews), add more competitors/seeds and rerun."
|
|
2721
|
+
};
|
|
2722
|
+
await client.close();
|
|
2607
2723
|
const researchDir = path11.join(
|
|
2608
2724
|
getKeywordResearchDir(),
|
|
2609
2725
|
"products",
|
|
@@ -2611,134 +2727,52 @@ async function handleKeywordResearch(input) {
|
|
|
2611
2727
|
"locales",
|
|
2612
2728
|
locale
|
|
2613
2729
|
);
|
|
2614
|
-
|
|
2615
|
-
const fileName =
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
try {
|
|
2620
|
-
return JSON.parse(text);
|
|
2621
|
-
} catch (err) {
|
|
2622
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2623
|
-
const match = /position (\d+)/i.exec(message) || /column (\d+)/i.exec(message) || /char (\d+)/i.exec(message);
|
|
2624
|
-
if (match) {
|
|
2625
|
-
const pos = Number(match[1]);
|
|
2626
|
-
const start = Math.max(0, pos - 40);
|
|
2627
|
-
const end = Math.min(text.length, pos + 40);
|
|
2628
|
-
const context = text.slice(start, end);
|
|
2629
|
-
throw new Error(
|
|
2630
|
-
`Failed to parse researchData JSON: ${message}
|
|
2631
|
-
Context around ${pos}: ${context}`
|
|
2632
|
-
);
|
|
2633
|
-
}
|
|
2634
|
-
throw new Error(`Failed to parse researchData JSON: ${message}`);
|
|
2635
|
-
}
|
|
2636
|
-
};
|
|
2637
|
-
const loadResearchDataFromPath = (p) => {
|
|
2638
|
-
if (!fs11.existsSync(p)) {
|
|
2639
|
-
throw new Error(`researchDataPath not found: ${p}`);
|
|
2640
|
-
}
|
|
2641
|
-
const raw = fs11.readFileSync(p, "utf-8");
|
|
2642
|
-
return parseJsonWithContext(raw);
|
|
2643
|
-
};
|
|
2644
|
-
if (writeTemplate || researchData) {
|
|
2645
|
-
const payload = researchData ? parseJsonWithContext(researchData) : researchDataPath ? loadResearchDataFromPath(researchDataPath) : buildTemplate({
|
|
2646
|
-
slug,
|
|
2647
|
-
locale,
|
|
2648
|
-
platform,
|
|
2649
|
-
country: resolvedCountry,
|
|
2650
|
-
seedKeywords: resolvedSeeds,
|
|
2651
|
-
competitorApps: resolvedCompetitors
|
|
2652
|
-
});
|
|
2653
|
-
outputPath = saveJsonFile({ researchDir, fileName, payload });
|
|
2654
|
-
fileAction = researchData ? "Saved provided researchData" : "Wrote template";
|
|
2655
|
-
}
|
|
2656
|
-
const templatePreview = JSON.stringify(
|
|
2657
|
-
buildTemplate({
|
|
2730
|
+
ensureDir(researchDir);
|
|
2731
|
+
const fileName = `keyword-research-${platform}-${resolvedCountry}.json`;
|
|
2732
|
+
const outputPath = path11.join(researchDir, fileName);
|
|
2733
|
+
const payload = {
|
|
2734
|
+
meta: {
|
|
2658
2735
|
slug,
|
|
2659
2736
|
locale,
|
|
2660
2737
|
platform,
|
|
2661
2738
|
country: resolvedCountry,
|
|
2662
|
-
seedKeywords:
|
|
2663
|
-
competitorApps:
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2739
|
+
seedKeywords: seeds,
|
|
2740
|
+
competitorApps: competitors,
|
|
2741
|
+
source: "mcp-appstore",
|
|
2742
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2743
|
+
},
|
|
2744
|
+
plan: {
|
|
2745
|
+
steps: [
|
|
2746
|
+
"Start mcp-appstore server (node server.js in external-tools/mcp-appstore).",
|
|
2747
|
+
"Confirm app IDs/locales: get_app_details(appId from config/registered-apps) to lock country/lang and 3\u20135 competitors.",
|
|
2748
|
+
"Discover competitors: search_app(term=seed keyword, num=10\u201320), get_similar_apps(appId=top competitor, num=10).",
|
|
2749
|
+
"Collect candidates (all of these, num=20\u201330): suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search; suggest_keywords_by_apps(apps=[top competitors]).",
|
|
2750
|
+
"Score shortlist: get_keyword_scores for 15\u201330 candidates per platform/country.",
|
|
2751
|
+
"Context check: analyze_reviews (num=100\u2013200) and fetch_reviews (num=50\u2013100) on top apps for language/tone cues.",
|
|
2752
|
+
"If keywordSuggestions/similar/reviews are sparse, rerun calls (add more competitors/seeds) until you have 10\u201315 strong keywords.",
|
|
2753
|
+
"For any recommended keyword without scores, rerun get_keyword_scores to fill traffic/difficulty.",
|
|
2754
|
+
"Keep rationale/nextActions in English by default unless you intentionally localize them."
|
|
2755
|
+
],
|
|
2756
|
+
note: "Generated automatically by keyword-research"
|
|
2757
|
+
},
|
|
2758
|
+
data: { raw, summary }
|
|
2759
|
+
};
|
|
2760
|
+
fs11.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2668
2761
|
const lines = [];
|
|
2669
|
-
lines.push(`#
|
|
2670
|
-
lines.push(`
|
|
2671
|
-
lines.push(`Primary locale detected: ${primaryLocale}`);
|
|
2672
|
-
if (supportedLocales.length > 0) {
|
|
2673
|
-
lines.push(
|
|
2674
|
-
`Registered supported locales (${platform}): ${supportedLocales.join(
|
|
2675
|
-
", "
|
|
2676
|
-
)} (source: ${supportedPath})`
|
|
2677
|
-
);
|
|
2678
|
-
if (!supportedLocales.includes(locale)) {
|
|
2679
|
-
lines.push(
|
|
2680
|
-
`WARNING: locale ${locale} not in registered supported locales. Confirm this locale or update registered-apps.json.`
|
|
2681
|
-
);
|
|
2682
|
-
}
|
|
2683
|
-
} else {
|
|
2684
|
-
lines.push(
|
|
2685
|
-
`Registered supported locales not found for ${platform} (checked: ${supportedPath}).`
|
|
2686
|
-
);
|
|
2687
|
-
}
|
|
2688
|
-
lines.push(
|
|
2689
|
-
`Seeds: ${resolvedSeeds.length > 0 ? resolvedSeeds.join(", ") : "(none set; add seedKeywords or ensure ASO keywords/title exist)"}`
|
|
2690
|
-
);
|
|
2691
|
-
lines.push(
|
|
2692
|
-
`Competitors (from config if empty): ${resolvedCompetitors.length > 0 ? resolvedCompetitors.map((c) => `${c.platform}:${c.appId}`).join(", ") : "(none set; add competitorApps or set appStoreAppId/bundleId/packageName in config.json)"}`
|
|
2693
|
-
);
|
|
2694
|
-
lines.push("");
|
|
2695
|
-
lines.push("How to run (uses mcp-appstore):");
|
|
2696
|
-
lines.push(
|
|
2697
|
-
`1) Start the local mcp-appstore server for this run: node server.js (cwd: /ABSOLUTE/PATH/TO/pabal-web-mcp/external-tools/mcp-appstore). LLM should start it before calling tools and stop it after, if the client supports process management; otherwise, start/stop manually.`
|
|
2698
|
-
);
|
|
2699
|
-
lines.push(
|
|
2700
|
-
`2) Confirm IDs/locales: get_app_details(appId from config/registered-apps) to lock locale/country and competitor list.`
|
|
2701
|
-
);
|
|
2762
|
+
lines.push(`# keyword-research`);
|
|
2763
|
+
lines.push(`Saved to ${outputPath}`);
|
|
2702
2764
|
lines.push(
|
|
2703
|
-
`
|
|
2765
|
+
`Candidates scored: ${scored.length}, Recommended: ${summary.recommendedKeywords.length}`
|
|
2704
2766
|
);
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
)
|
|
2708
|
-
lines.push(
|
|
2709
|
-
`5) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
|
|
2710
|
-
);
|
|
2711
|
-
lines.push(
|
|
2712
|
-
`6) Context check: analyze_reviews (num=100-200) and fetch_reviews (num=50-100) on top apps to harvest native phrasing; keep snippets for improve-public.`
|
|
2713
|
-
);
|
|
2714
|
-
lines.push(
|
|
2715
|
-
`7) Save all raw responses + your final 10\u201315 keywords (mix of core/high-traffic, mid, longtail) to: ${outputPath} (structure mirrors .aso/pullData/.aso/pushData under products/<slug>/locales/<locale>)`
|
|
2716
|
-
);
|
|
2717
|
-
lines.push(
|
|
2718
|
-
`8) If keywordSuggestions/similarApps/reviews are still empty or <10 solid candidates, add more competitors/seeds and rerun the calls above until you reach 10\u201315 strong keywords.`
|
|
2719
|
-
);
|
|
2720
|
-
lines.push(
|
|
2721
|
-
`9) If any recommended keywords lack scores, rerun get_keyword_scores for those items. Keep rationale/nextActions in English by default unless you explicitly want them localized.`
|
|
2722
|
-
);
|
|
2723
|
-
if (fileAction) {
|
|
2724
|
-
lines.push(`File: ${fileAction} at ${outputPath}`);
|
|
2725
|
-
} else {
|
|
2767
|
+
if (scored.length < 10 || summary.recommendedKeywords.length < 10 || Object.values(raw.keywordSuggestions).some(
|
|
2768
|
+
(arr) => Array.isArray(arr) && arr.length === 0
|
|
2769
|
+
)) {
|
|
2726
2770
|
lines.push(
|
|
2727
|
-
`
|
|
2771
|
+
`Warning: results look sparse. Add more competitors/seeds and rerun.`
|
|
2728
2772
|
);
|
|
2729
2773
|
}
|
|
2730
|
-
lines.push("");
|
|
2731
|
-
lines.push("Suggested JSON shape:");
|
|
2732
|
-
lines.push("```json");
|
|
2733
|
-
lines.push(templatePreview);
|
|
2734
|
-
lines.push("```");
|
|
2735
2774
|
return {
|
|
2736
|
-
content: [
|
|
2737
|
-
{
|
|
2738
|
-
type: "text",
|
|
2739
|
-
text: lines.join("\n")
|
|
2740
|
-
}
|
|
2741
|
-
]
|
|
2775
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
2742
2776
|
};
|
|
2743
2777
|
}
|
|
2744
2778
|
|