pabal-web-mcp 1.3.7 → 1.3.8
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 +481 -352
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -212,7 +212,7 @@ var asoToPublicTool = {
|
|
|
212
212
|
name: "aso-to-public",
|
|
213
213
|
description: `Converts ASO data from pullData to public/products/[slug]/ structure.
|
|
214
214
|
|
|
215
|
-
**IMPORTANT:**
|
|
215
|
+
**IMPORTANT:** Always use 'search-app' tool first to resolve the exact slug before calling this tool. The user may provide an approximate name, bundleId, or packageName - search-app will find and return the correct slug. Never pass user input directly as slug.
|
|
216
216
|
|
|
217
217
|
This tool:
|
|
218
218
|
1. Loads ASO data from .aso/pullData/products/[slug]/store/ (path from ~/.config/pabal-mcp/config.json dataDir)
|
|
@@ -665,7 +665,7 @@ var publicToAsoTool = {
|
|
|
665
665
|
name: "public-to-aso",
|
|
666
666
|
description: `Prepares ASO data from public/products/[slug]/ to pushData format.
|
|
667
667
|
|
|
668
|
-
**IMPORTANT:**
|
|
668
|
+
**IMPORTANT:** Always use 'search-app' tool first to resolve the exact slug before calling this tool. The user may provide an approximate name, bundleId, or packageName - search-app will find and return the correct slug. Never pass user input directly as slug.
|
|
669
669
|
|
|
670
670
|
This tool:
|
|
671
671
|
1. Loads ASO data from public/products/[slug]/config.json + locales/
|
|
@@ -1727,7 +1727,7 @@ var improvePublicTool = {
|
|
|
1727
1727
|
name: "improve-public",
|
|
1728
1728
|
description: `Optimizes locale JSON in public/products/[slug]/locales for ASO.
|
|
1729
1729
|
|
|
1730
|
-
**IMPORTANT:**
|
|
1730
|
+
**IMPORTANT:** Always use 'search-app' tool first to resolve the exact slug before calling this tool. The user may provide an approximate name, bundleId, or packageName - search-app will find and return the correct slug. Never pass user input directly as slug.
|
|
1731
1731
|
|
|
1732
1732
|
**CRITICAL: Only processes existing locale files. Does NOT create new locale files.**
|
|
1733
1733
|
- Only improves locales that already exist in public/products/[slug]/locales/
|
|
@@ -2378,10 +2378,8 @@ Writing style reference for ${locale}: Found ${posts.length} existing post(s) us
|
|
|
2378
2378
|
// src/tools/keyword-research.ts
|
|
2379
2379
|
import fs11 from "fs";
|
|
2380
2380
|
import path11 from "path";
|
|
2381
|
-
import { z as
|
|
2382
|
-
import { zodToJsonSchema as
|
|
2383
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2384
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2381
|
+
import { z as z7 } from "zod";
|
|
2382
|
+
import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
|
|
2385
2383
|
|
|
2386
2384
|
// src/utils/registered-apps.util.ts
|
|
2387
2385
|
import fs10 from "fs";
|
|
@@ -2415,58 +2413,302 @@ function findRegisteredApp(slug, filePath) {
|
|
|
2415
2413
|
const app = apps.find((a) => a.slug === slug);
|
|
2416
2414
|
return { app, path: usedPath };
|
|
2417
2415
|
}
|
|
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
|
+
}
|
|
2418
2430
|
|
|
2419
|
-
// src/tools/
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2431
|
+
// src/tools/search-app.ts
|
|
2432
|
+
import { z as z6 } from "zod";
|
|
2433
|
+
import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
|
|
2434
|
+
var TOOL_NAME = "search-app";
|
|
2435
|
+
var searchAppInputSchema = z6.object({
|
|
2436
|
+
query: z6.string().trim().optional().describe(
|
|
2437
|
+
"Search term (slug, bundleId, packageName, name). Returns all apps if empty."
|
|
2425
2438
|
),
|
|
2426
|
-
|
|
2427
|
-
|
|
2439
|
+
store: z6.enum(["all", "appStore", "googlePlay"]).default("all").describe("Store filter (default: all)")
|
|
2440
|
+
});
|
|
2441
|
+
var jsonSchema6 = zodToJsonSchema6(searchAppInputSchema, {
|
|
2442
|
+
name: "SearchAppInput",
|
|
2443
|
+
$refStrategy: "none"
|
|
2444
|
+
});
|
|
2445
|
+
var inputSchema6 = jsonSchema6.definitions?.SearchAppInput || jsonSchema6;
|
|
2446
|
+
var searchAppTool = {
|
|
2447
|
+
name: TOOL_NAME,
|
|
2448
|
+
description: `Search registered apps from registered-apps.json.
|
|
2449
|
+
|
|
2450
|
+
- Called without query: Returns all app list
|
|
2451
|
+
- Called with query: Search by slug, bundleId, packageName, name
|
|
2452
|
+
- Use store filter to narrow results to appStore or googlePlay only`,
|
|
2453
|
+
inputSchema: inputSchema6
|
|
2454
|
+
};
|
|
2455
|
+
function matchesQuery(app, query) {
|
|
2456
|
+
const lowerQuery = query.toLowerCase();
|
|
2457
|
+
if (app.slug.toLowerCase().includes(lowerQuery)) return true;
|
|
2458
|
+
if (app.name?.toLowerCase().includes(lowerQuery)) return true;
|
|
2459
|
+
if (app.appStore?.bundleId?.toLowerCase().includes(lowerQuery)) return true;
|
|
2460
|
+
if (app.appStore?.name?.toLowerCase().includes(lowerQuery)) return true;
|
|
2461
|
+
if (app.googlePlay?.packageName?.toLowerCase().includes(lowerQuery))
|
|
2462
|
+
return true;
|
|
2463
|
+
if (app.googlePlay?.name?.toLowerCase().includes(lowerQuery)) return true;
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2466
|
+
function filterByStore(apps, store) {
|
|
2467
|
+
if (store === "all") return apps;
|
|
2468
|
+
return apps.filter((app) => {
|
|
2469
|
+
if (store === "appStore") return !!app.appStore;
|
|
2470
|
+
if (store === "googlePlay") return !!app.googlePlay;
|
|
2471
|
+
return true;
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
function formatAppInfo(app) {
|
|
2475
|
+
const lines = [];
|
|
2476
|
+
lines.push(`\u{1F4F1} **${app.name || app.slug}** (\`${app.slug}\`)`);
|
|
2477
|
+
if (app.appStore) {
|
|
2478
|
+
lines.push(` \u{1F34E} App Store: \`${app.appStore.bundleId || "N/A"}\``);
|
|
2479
|
+
if (app.appStore.appId) {
|
|
2480
|
+
lines.push(` App ID: ${app.appStore.appId}`);
|
|
2481
|
+
}
|
|
2482
|
+
if (app.appStore.name) {
|
|
2483
|
+
lines.push(` Name: ${app.appStore.name}`);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
if (app.googlePlay) {
|
|
2487
|
+
lines.push(
|
|
2488
|
+
` \u{1F916} Google Play: \`${app.googlePlay.packageName || "N/A"}\``
|
|
2489
|
+
);
|
|
2490
|
+
if (app.googlePlay.name) {
|
|
2491
|
+
lines.push(` Name: ${app.googlePlay.name}`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return lines.join("\n");
|
|
2495
|
+
}
|
|
2496
|
+
async function handleSearchApp(input) {
|
|
2497
|
+
const { query, store = "all" } = input;
|
|
2498
|
+
try {
|
|
2499
|
+
const { apps: allApps, path: configPath } = loadRegisteredApps();
|
|
2500
|
+
let results;
|
|
2501
|
+
if (!query) {
|
|
2502
|
+
results = allApps;
|
|
2503
|
+
} else {
|
|
2504
|
+
const { app: exactMatch } = findRegisteredApp(query);
|
|
2505
|
+
const partialMatches = allApps.filter((app) => matchesQuery(app, query));
|
|
2506
|
+
const seenSlugs = /* @__PURE__ */ new Set();
|
|
2507
|
+
results = [];
|
|
2508
|
+
if (exactMatch && !seenSlugs.has(exactMatch.slug)) {
|
|
2509
|
+
results.push(exactMatch);
|
|
2510
|
+
seenSlugs.add(exactMatch.slug);
|
|
2511
|
+
}
|
|
2512
|
+
for (const app of partialMatches) {
|
|
2513
|
+
if (!seenSlugs.has(app.slug)) {
|
|
2514
|
+
results.push(app);
|
|
2515
|
+
seenSlugs.add(app.slug);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
results = filterByStore(results, store);
|
|
2520
|
+
if (results.length === 0) {
|
|
2521
|
+
const message = query ? `No apps found matching "${query}".` : "No apps registered.";
|
|
2522
|
+
return {
|
|
2523
|
+
content: [
|
|
2524
|
+
{
|
|
2525
|
+
type: "text",
|
|
2526
|
+
text: `\u274C ${message}
|
|
2527
|
+
|
|
2528
|
+
\u{1F4A1} Register apps in ${configPath}`
|
|
2529
|
+
}
|
|
2530
|
+
],
|
|
2531
|
+
_meta: { apps: [], count: 0, configPath }
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
const header = query ? `\u{1F50D} Search results for "${query}": ${results.length} app(s)` : `\u{1F4CB} Registered app list: ${results.length} app(s)`;
|
|
2535
|
+
const appList = results.map(formatAppInfo).join("\n\n");
|
|
2536
|
+
return {
|
|
2537
|
+
content: [
|
|
2538
|
+
{
|
|
2539
|
+
type: "text",
|
|
2540
|
+
text: `${header}
|
|
2541
|
+
|
|
2542
|
+
${appList}
|
|
2543
|
+
|
|
2544
|
+
---
|
|
2545
|
+
Config: ${configPath}`
|
|
2546
|
+
}
|
|
2547
|
+
],
|
|
2548
|
+
_meta: { apps: results, count: results.length, configPath }
|
|
2549
|
+
};
|
|
2550
|
+
} catch (error) {
|
|
2551
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2552
|
+
return {
|
|
2553
|
+
content: [
|
|
2554
|
+
{
|
|
2555
|
+
type: "text",
|
|
2556
|
+
text: `\u274C App search failed: ${message}`
|
|
2557
|
+
}
|
|
2558
|
+
]
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// src/tools/keyword-research.ts
|
|
2564
|
+
var TOOL_NAME2 = "keyword-research";
|
|
2565
|
+
var keywordResearchInputSchema = z7.object({
|
|
2566
|
+
slug: z7.string().trim().describe("Product slug"),
|
|
2567
|
+
locale: z7.string().trim().describe("Locale code (e.g., en-US, ko-KR). Used for storage under .aso/keywordResearch/products/[slug]/locales/."),
|
|
2568
|
+
platform: z7.enum(["ios", "android"]).default("ios").describe("Store to target ('ios' or 'android'). Run separately per platform."),
|
|
2569
|
+
country: z7.string().length(2).optional().describe(
|
|
2428
2570
|
"Two-letter store country code. If omitted, derived from locale region (e.g., ko-KR -> kr), else 'us'."
|
|
2429
2571
|
),
|
|
2430
|
-
seedKeywords:
|
|
2431
|
-
competitorApps:
|
|
2432
|
-
|
|
2433
|
-
appId:
|
|
2434
|
-
platform:
|
|
2572
|
+
seedKeywords: z7.array(z7.string().trim()).default([]).describe("Seed keywords to start from."),
|
|
2573
|
+
competitorApps: z7.array(
|
|
2574
|
+
z7.object({
|
|
2575
|
+
appId: z7.string().trim().describe("App ID (package name or iOS ID/bundle)"),
|
|
2576
|
+
platform: z7.enum(["ios", "android"])
|
|
2435
2577
|
})
|
|
2436
|
-
).default([]),
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2578
|
+
).default([]).describe("Known competitor apps to probe."),
|
|
2579
|
+
filename: z7.string().trim().optional().describe("Override output filename. Defaults to keyword-research-[platform]-[country].json"),
|
|
2580
|
+
writeTemplate: z7.boolean().default(false).describe("If true, write a JSON template at the output path."),
|
|
2581
|
+
researchData: z7.string().trim().optional().describe(
|
|
2582
|
+
"Optional JSON string with research results (e.g., from mcp-appstore tools). If provided, saves it to the output path."
|
|
2583
|
+
),
|
|
2584
|
+
researchDataPath: z7.string().trim().optional().describe(
|
|
2585
|
+
"Optional path to a JSON file containing research results. If set, file content is saved to the output path (preferred to avoid escape errors)."
|
|
2586
|
+
)
|
|
2440
2587
|
});
|
|
2441
|
-
var
|
|
2588
|
+
var jsonSchema7 = zodToJsonSchema7(keywordResearchInputSchema, {
|
|
2442
2589
|
name: "KeywordResearchInput",
|
|
2443
2590
|
$refStrategy: "none"
|
|
2444
2591
|
});
|
|
2445
|
-
var
|
|
2592
|
+
var inputSchema7 = jsonSchema7.definitions?.KeywordResearchInput || jsonSchema7;
|
|
2446
2593
|
var keywordResearchTool = {
|
|
2447
|
-
name:
|
|
2448
|
-
description:
|
|
2449
|
-
|
|
2594
|
+
name: TOOL_NAME2,
|
|
2595
|
+
description: `Prep + persist keyword research ahead of improve-public using mcp-appstore outputs.
|
|
2596
|
+
|
|
2597
|
+
**IMPORTANT:** Always use 'search-app' tool first to resolve the exact slug before calling this tool. The user may provide an approximate name, bundleId, or packageName - search-app will find and return the correct slug. Never pass user input directly as slug.
|
|
2598
|
+
|
|
2599
|
+
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.`,
|
|
2600
|
+
inputSchema: inputSchema7
|
|
2450
2601
|
};
|
|
2602
|
+
function buildTemplate({
|
|
2603
|
+
slug,
|
|
2604
|
+
locale,
|
|
2605
|
+
platform,
|
|
2606
|
+
country,
|
|
2607
|
+
seedKeywords,
|
|
2608
|
+
competitorApps
|
|
2609
|
+
}) {
|
|
2610
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2611
|
+
return {
|
|
2612
|
+
meta: {
|
|
2613
|
+
slug,
|
|
2614
|
+
locale,
|
|
2615
|
+
platform,
|
|
2616
|
+
country,
|
|
2617
|
+
seedKeywords,
|
|
2618
|
+
competitorApps,
|
|
2619
|
+
source: "mcp-appstore",
|
|
2620
|
+
updatedAt: timestamp
|
|
2621
|
+
},
|
|
2622
|
+
plan: {
|
|
2623
|
+
steps: [
|
|
2624
|
+
"1. SETUP: Start mcp-appstore server (node server.js in external-tools/mcp-appstore).",
|
|
2625
|
+
"2. APP IDENTITY: get_app_details(appId) \u2192 confirm exact app name, category, current keywords, and store listing quality.",
|
|
2626
|
+
"3. COMPETITOR DISCOVERY: search_app(term=seed, num=15) + get_similar_apps(appId=your app, num=20) \u2192 identify 5-10 direct competitors (same category, similar size) + 3-5 aspirational competitors (top performers).",
|
|
2627
|
+
"4. COMPETITOR KEYWORD MINING: For top 5 competitors, run suggest_keywords_by_apps \u2192 extract keywords they rank for but you don't.",
|
|
2628
|
+
"5. KEYWORD EXPANSION (run ALL, num=30 each): suggest_keywords_by_seeds (your app name + core features), by_category (your primary category), by_similarity (semantic variations), by_competition (gap analysis), by_search (autocomplete/trending).",
|
|
2629
|
+
"6. KEYWORD SCORING: get_keyword_scores for ALL candidates (50-100 keywords) \u2192 filter by: traffic \u226510, difficulty \u226470, relevance to your app.",
|
|
2630
|
+
"7. REVIEW INTELLIGENCE: analyze_reviews(num=200) + fetch_reviews(num=100) on top 3 competitors \u2192 extract: user pain points, feature requests, emotional language, native phrases users actually use.",
|
|
2631
|
+
"8. KEYWORD CATEGORIZATION: Group into tiers - Tier1 (3-5): high traffic (\u22651000), high relevance, moderate difficulty (\u226450); Tier2 (5-7): medium traffic (100-1000), exact feature match; Tier3 (5-8): longtail (<100 traffic), very low difficulty (\u226430), high conversion intent.",
|
|
2632
|
+
"9. LOCALIZATION CHECK: Verify keywords are natural in target locale - avoid direct translations, prefer native expressions found in reviews.",
|
|
2633
|
+
"10. GAP ANALYSIS: Compare your current keywords vs competitor keywords \u2192 identify missed opportunities and over-saturated terms to avoid.",
|
|
2634
|
+
"11. VALIDATION: For final 15-20 keywords, ensure each has: score data, clear user intent, natural locale fit, and specific rationale for inclusion.",
|
|
2635
|
+
"Keep rationale/nextActions in English by default unless you intentionally localize them."
|
|
2636
|
+
],
|
|
2637
|
+
selectionCriteria: {
|
|
2638
|
+
tier1_core: "High traffic (\u22651000), relevance score \u22650.8, difficulty \u226450, brand-safe",
|
|
2639
|
+
tier2_feature: "Medium traffic (100-1000), exact feature/benefit match, difficulty \u226460",
|
|
2640
|
+
tier3_longtail: "Low traffic (<100), very low difficulty (\u226430), high purchase/download intent phrases",
|
|
2641
|
+
avoid: "Generic terms (difficulty \u226580), irrelevant categories, competitor brand names, terms with no search volume"
|
|
2642
|
+
},
|
|
2643
|
+
qualityChecks: [
|
|
2644
|
+
"Each keyword has traffic + difficulty scores (no gaps)",
|
|
2645
|
+
"Mix of 3 tiers represented (not all longtail, not all high-competition)",
|
|
2646
|
+
"Keywords validated against actual user language from reviews",
|
|
2647
|
+
"No duplicate semantic meanings (e.g., 'photo edit' and 'edit photo')",
|
|
2648
|
+
"Locale-appropriate phrasing verified"
|
|
2649
|
+
],
|
|
2650
|
+
note: "Run per platform/country. Target 15-20 keywords per locale with clear tier distribution. Save ALL raw data for audit trail."
|
|
2651
|
+
},
|
|
2652
|
+
data: {
|
|
2653
|
+
raw: {
|
|
2654
|
+
searchApp: [],
|
|
2655
|
+
getAppDetails: [],
|
|
2656
|
+
similarApps: [],
|
|
2657
|
+
keywordSuggestions: {
|
|
2658
|
+
bySeeds: [],
|
|
2659
|
+
byCategory: [],
|
|
2660
|
+
bySimilarity: [],
|
|
2661
|
+
byCompetition: [],
|
|
2662
|
+
bySearchHints: [],
|
|
2663
|
+
byApps: []
|
|
2664
|
+
},
|
|
2665
|
+
keywordScores: [],
|
|
2666
|
+
reviewsAnalysis: [],
|
|
2667
|
+
reviewsRaw: []
|
|
2668
|
+
},
|
|
2669
|
+
summary: {
|
|
2670
|
+
recommendedKeywords: [],
|
|
2671
|
+
keywordsByTier: {
|
|
2672
|
+
tier1_core: [],
|
|
2673
|
+
tier2_feature: [],
|
|
2674
|
+
tier3_longtail: []
|
|
2675
|
+
},
|
|
2676
|
+
competitorInsights: {
|
|
2677
|
+
topCompetitors: [],
|
|
2678
|
+
keywordGaps: [],
|
|
2679
|
+
userLanguagePatterns: []
|
|
2680
|
+
},
|
|
2681
|
+
rationale: "",
|
|
2682
|
+
confidence: {
|
|
2683
|
+
dataQuality: "",
|
|
2684
|
+
localeRelevance: "",
|
|
2685
|
+
competitivePosition: ""
|
|
2686
|
+
},
|
|
2687
|
+
nextActions: [
|
|
2688
|
+
"Feed tiered keywords into improve-public Stage 1 (prioritize Tier1 for title, Tier2-3 for keyword field)",
|
|
2689
|
+
"Monitor keyword rankings post-update",
|
|
2690
|
+
"Re-run research quarterly or after major competitor changes"
|
|
2691
|
+
]
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
function saveJsonFile({
|
|
2697
|
+
researchDir,
|
|
2698
|
+
fileName,
|
|
2699
|
+
payload
|
|
2700
|
+
}) {
|
|
2701
|
+
fs11.mkdirSync(researchDir, { recursive: true });
|
|
2702
|
+
const outputPath = path11.join(researchDir, fileName);
|
|
2703
|
+
fs11.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2704
|
+
return outputPath;
|
|
2705
|
+
}
|
|
2451
2706
|
function normalizeKeywords(raw) {
|
|
2452
2707
|
if (!raw) return [];
|
|
2453
2708
|
if (Array.isArray(raw)) {
|
|
2454
|
-
return raw.map((k) => k.trim()).filter(
|
|
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;
|
|
2709
|
+
return raw.map((k) => k.trim()).filter((k) => k.length > 0);
|
|
2466
2710
|
}
|
|
2467
|
-
|
|
2468
|
-
function ensureDir(dir) {
|
|
2469
|
-
fs11.mkdirSync(dir, { recursive: true });
|
|
2711
|
+
return raw.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
|
|
2470
2712
|
}
|
|
2471
2713
|
async function handleKeywordResearch(input) {
|
|
2472
2714
|
const {
|
|
@@ -2476,28 +2718,36 @@ async function handleKeywordResearch(input) {
|
|
|
2476
2718
|
country,
|
|
2477
2719
|
seedKeywords = [],
|
|
2478
2720
|
competitorApps = [],
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2721
|
+
filename,
|
|
2722
|
+
writeTemplate = false,
|
|
2723
|
+
researchData,
|
|
2724
|
+
researchDataPath
|
|
2482
2725
|
} = input;
|
|
2726
|
+
const searchResult = await handleSearchApp({ query: slug, store: "all" });
|
|
2727
|
+
const registeredApps = searchResult._meta?.apps || [];
|
|
2728
|
+
const registeredApp = registeredApps.length > 0 ? registeredApps[0] : void 0;
|
|
2483
2729
|
const { config, locales } = loadProductLocales(slug);
|
|
2484
2730
|
const primaryLocale = resolvePrimaryLocale(config, locales);
|
|
2485
2731
|
const primaryLocaleData = locales[primaryLocale];
|
|
2486
|
-
const {
|
|
2487
|
-
const resolvedCountry = country || (locale.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
|
|
2732
|
+
const { supportedLocales, path: supportedPath } = getSupportedLocalesForSlug(slug, platform);
|
|
2488
2733
|
const autoSeeds = [];
|
|
2489
|
-
|
|
2734
|
+
const autoCompetitors = [];
|
|
2735
|
+
if (primaryLocaleData?.aso?.title) {
|
|
2736
|
+
autoSeeds.push(primaryLocaleData.aso.title);
|
|
2737
|
+
}
|
|
2490
2738
|
const parsedKeywords = normalizeKeywords(primaryLocaleData?.aso?.keywords);
|
|
2491
2739
|
autoSeeds.push(...parsedKeywords.slice(0, 5));
|
|
2492
2740
|
if (config?.name) autoSeeds.push(config.name);
|
|
2493
2741
|
if (config?.tagline) autoSeeds.push(config.tagline);
|
|
2494
|
-
if (registeredApp?.name) autoSeeds.push(registeredApp.name);
|
|
2495
|
-
if (
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2742
|
+
if (!config?.name && registeredApp?.name) autoSeeds.push(registeredApp.name);
|
|
2743
|
+
if (!primaryLocaleData?.aso?.title) {
|
|
2744
|
+
if (platform === "ios" && registeredApp?.appStore?.name) {
|
|
2745
|
+
autoSeeds.push(registeredApp.appStore.name);
|
|
2746
|
+
}
|
|
2747
|
+
if (platform === "android" && registeredApp?.googlePlay?.name) {
|
|
2748
|
+
autoSeeds.push(registeredApp.googlePlay.name);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2501
2751
|
if (platform === "ios") {
|
|
2502
2752
|
if (config?.appStoreAppId) {
|
|
2503
2753
|
autoCompetitors.push({ appId: String(config.appStoreAppId), platform });
|
|
@@ -2514,267 +2764,17 @@ async function handleKeywordResearch(input) {
|
|
|
2514
2764
|
platform
|
|
2515
2765
|
});
|
|
2516
2766
|
}
|
|
2517
|
-
} else {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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] || "npm";
|
|
2537
|
-
const args = serverCommand?.length ? serverCommand.slice(1) : ["start"];
|
|
2538
|
-
if (!fs11.existsSync(cwd)) {
|
|
2539
|
-
throw new Error(
|
|
2540
|
-
`mcp-appstore cwd not found: ${cwd}. Set 'serverCwd' to the absolute path (e.g., /Users/you/pabal-web-mcp/external-tools/mcp-appstore).`
|
|
2541
|
-
);
|
|
2542
|
-
}
|
|
2543
|
-
const serverJsPath = path11.join(cwd, "server.js");
|
|
2544
|
-
const usingNodeServer = cmd === "node" || args.includes("server.js");
|
|
2545
|
-
if (usingNodeServer && !fs11.existsSync(serverJsPath)) {
|
|
2546
|
-
throw new Error(
|
|
2547
|
-
`mcp-appstore server.js not found under ${cwd}. Set 'serverCommand' (e.g., ["npm","start"] or ["node","/abs/path/server.js"]).`
|
|
2548
|
-
);
|
|
2549
|
-
}
|
|
2550
|
-
const transport = new StdioClientTransport({ command: cmd, args, cwd });
|
|
2551
|
-
const client = new Client(
|
|
2552
|
-
{ name: "keyword-research", version: "1.0.0" },
|
|
2553
|
-
{ capabilities: { tools: {} } }
|
|
2554
|
-
);
|
|
2555
|
-
await client.connect(transport);
|
|
2556
|
-
const raw = {
|
|
2557
|
-
searchApp: [],
|
|
2558
|
-
getAppDetails: [],
|
|
2559
|
-
similarApps: [],
|
|
2560
|
-
searchAppFallbackApps: [],
|
|
2561
|
-
keywordSuggestions: {
|
|
2562
|
-
bySeeds: [],
|
|
2563
|
-
byCategory: [],
|
|
2564
|
-
bySimilarity: [],
|
|
2565
|
-
byCompetition: [],
|
|
2566
|
-
bySearchHints: [],
|
|
2567
|
-
byApps: []
|
|
2568
|
-
},
|
|
2569
|
-
keywordScores: [],
|
|
2570
|
-
reviewsAnalysis: [],
|
|
2571
|
-
reviewsRaw: []
|
|
2572
|
-
};
|
|
2573
|
-
const fallbackAppIds = [];
|
|
2574
|
-
for (const term of seeds.slice(0, 3)) {
|
|
2575
|
-
const res = await callToolJSON(client, "search_app", {
|
|
2576
|
-
term,
|
|
2577
|
-
platform,
|
|
2578
|
-
num: 20,
|
|
2579
|
-
country: resolvedCountry
|
|
2580
|
-
});
|
|
2581
|
-
raw.searchApp.push({
|
|
2582
|
-
query: term,
|
|
2583
|
-
platform,
|
|
2584
|
-
country: resolvedCountry,
|
|
2585
|
-
results: res?.results ?? res
|
|
2586
|
-
});
|
|
2587
|
-
const apps = (res?.results ?? res) || [];
|
|
2588
|
-
apps.slice(0, 5).forEach((app) => {
|
|
2589
|
-
const appId = app?.appId || app?.id;
|
|
2590
|
-
if (appId) fallbackAppIds.push(String(appId));
|
|
2591
|
-
});
|
|
2592
|
-
}
|
|
2593
|
-
raw.searchAppFallbackApps = Array.from(new Set(fallbackAppIds));
|
|
2594
|
-
for (const comp of competitors.slice(0, 5)) {
|
|
2595
|
-
const res = await callToolJSON(client, "get_app_details", {
|
|
2596
|
-
appId: comp.appId,
|
|
2597
|
-
platform: comp.platform,
|
|
2598
|
-
country: resolvedCountry
|
|
2599
|
-
});
|
|
2600
|
-
raw.getAppDetails.push(res);
|
|
2601
|
-
}
|
|
2602
|
-
const firstCompId = competitors[0]?.appId || raw.searchAppFallbackApps[0];
|
|
2603
|
-
if (firstCompId) {
|
|
2604
|
-
const res = await callToolJSON(client, "get_similar_apps", {
|
|
2605
|
-
appId: firstCompId,
|
|
2606
|
-
platform,
|
|
2607
|
-
num: 10,
|
|
2608
|
-
country: resolvedCountry
|
|
2609
|
-
});
|
|
2610
|
-
raw.similarApps = res?.similarApps || res || [];
|
|
2611
|
-
}
|
|
2612
|
-
const firstComp = firstCompId;
|
|
2613
|
-
const appsList = competitors.length ? competitors.map((c) => c.appId) : raw.searchAppFallbackApps.slice(0, 5);
|
|
2614
|
-
const suggestCalls = [
|
|
2615
|
-
[
|
|
2616
|
-
"suggest_keywords_by_seeds",
|
|
2617
|
-
{
|
|
2618
|
-
keywords: seeds,
|
|
2619
|
-
platform,
|
|
2620
|
-
num: numSuggestions,
|
|
2621
|
-
country: resolvedCountry
|
|
2622
|
-
},
|
|
2623
|
-
"bySeeds"
|
|
2624
|
-
],
|
|
2625
|
-
[
|
|
2626
|
-
"suggest_keywords_by_category",
|
|
2627
|
-
firstComp ? {
|
|
2628
|
-
appId: firstComp,
|
|
2629
|
-
platform,
|
|
2630
|
-
num: numSuggestions,
|
|
2631
|
-
country: resolvedCountry
|
|
2632
|
-
} : null,
|
|
2633
|
-
"byCategory"
|
|
2634
|
-
],
|
|
2635
|
-
[
|
|
2636
|
-
"suggest_keywords_by_similarity",
|
|
2637
|
-
firstComp ? {
|
|
2638
|
-
appId: firstComp,
|
|
2639
|
-
platform,
|
|
2640
|
-
num: numSuggestions,
|
|
2641
|
-
country: resolvedCountry
|
|
2642
|
-
} : null,
|
|
2643
|
-
"bySimilarity"
|
|
2644
|
-
],
|
|
2645
|
-
[
|
|
2646
|
-
"suggest_keywords_by_competition",
|
|
2647
|
-
firstComp ? {
|
|
2648
|
-
appId: firstComp,
|
|
2649
|
-
platform,
|
|
2650
|
-
num: numSuggestions,
|
|
2651
|
-
country: resolvedCountry
|
|
2652
|
-
} : null,
|
|
2653
|
-
"byCompetition"
|
|
2654
|
-
],
|
|
2655
|
-
[
|
|
2656
|
-
"suggest_keywords_by_search",
|
|
2657
|
-
{
|
|
2658
|
-
keywords: seeds.slice(0, 5),
|
|
2659
|
-
platform,
|
|
2660
|
-
num: numSuggestions,
|
|
2661
|
-
country: resolvedCountry
|
|
2662
|
-
},
|
|
2663
|
-
"bySearchHints"
|
|
2664
|
-
],
|
|
2665
|
-
[
|
|
2666
|
-
"suggest_keywords_by_apps",
|
|
2667
|
-
appsList.length ? {
|
|
2668
|
-
apps: appsList,
|
|
2669
|
-
platform,
|
|
2670
|
-
num: numSuggestions,
|
|
2671
|
-
country: resolvedCountry
|
|
2672
|
-
} : null,
|
|
2673
|
-
"byApps"
|
|
2674
|
-
]
|
|
2675
|
-
];
|
|
2676
|
-
for (const [tool, args2, key] of suggestCalls) {
|
|
2677
|
-
if (!args2) continue;
|
|
2678
|
-
const res = await callToolJSON(client, tool, args2);
|
|
2679
|
-
const suggestions = res?.suggestions || res || [];
|
|
2680
|
-
raw.keywordSuggestions[key] = suggestions;
|
|
2681
|
-
}
|
|
2682
|
-
for (const comp of competitors.slice(0, 2)) {
|
|
2683
|
-
const analysis = await callToolJSON(client, "analyze_reviews", {
|
|
2684
|
-
appId: comp.appId,
|
|
2685
|
-
platform: comp.platform,
|
|
2686
|
-
country: resolvedCountry,
|
|
2687
|
-
num: 150
|
|
2688
|
-
});
|
|
2689
|
-
raw.reviewsAnalysis.push(analysis);
|
|
2690
|
-
const rawReviews = await callToolJSON(client, "fetch_reviews", {
|
|
2691
|
-
appId: comp.appId,
|
|
2692
|
-
platform: comp.platform,
|
|
2693
|
-
country: resolvedCountry,
|
|
2694
|
-
num: 80
|
|
2695
|
-
});
|
|
2696
|
-
raw.reviewsRaw.push(rawReviews);
|
|
2697
|
-
}
|
|
2698
|
-
if (competitors.length === 0 && raw.searchAppFallbackApps.length > 0) {
|
|
2699
|
-
const fb = raw.searchAppFallbackApps[0];
|
|
2700
|
-
const analysis = await callToolJSON(client, "analyze_reviews", {
|
|
2701
|
-
appId: fb,
|
|
2702
|
-
platform,
|
|
2703
|
-
country: resolvedCountry,
|
|
2704
|
-
num: 150
|
|
2705
|
-
});
|
|
2706
|
-
raw.reviewsAnalysis.push(analysis);
|
|
2707
|
-
const rawReviews = await callToolJSON(client, "fetch_reviews", {
|
|
2708
|
-
appId: fb,
|
|
2709
|
-
platform,
|
|
2710
|
-
country: resolvedCountry,
|
|
2711
|
-
num: 80
|
|
2712
|
-
});
|
|
2713
|
-
raw.reviewsRaw.push(rawReviews);
|
|
2714
|
-
}
|
|
2715
|
-
const candidateSet = /* @__PURE__ */ new Set();
|
|
2716
|
-
const addCandidates = (arr) => {
|
|
2717
|
-
if (!arr) return;
|
|
2718
|
-
if (Array.isArray(arr)) {
|
|
2719
|
-
arr.forEach((v) => {
|
|
2720
|
-
if (typeof v === "string") candidateSet.add(v);
|
|
2721
|
-
if (v?.keyword) candidateSet.add(String(v.keyword));
|
|
2722
|
-
if (v?.suggestions) addCandidates(v.suggestions);
|
|
2723
|
-
});
|
|
2724
|
-
}
|
|
2725
|
-
};
|
|
2726
|
-
seeds.forEach((s) => candidateSet.add(s));
|
|
2727
|
-
Object.values(raw.keywordSuggestions).forEach((arr) => addCandidates(arr));
|
|
2728
|
-
raw.reviewsAnalysis.forEach((ra) => {
|
|
2729
|
-
const freq = ra?.analysis?.keywordFrequency || ra?.keywordFrequency;
|
|
2730
|
-
if (freq && typeof freq === "object") {
|
|
2731
|
-
addCandidates(Object.keys(freq));
|
|
2732
|
-
}
|
|
2733
|
-
});
|
|
2734
|
-
const candidates = Array.from(candidateSet).slice(0, 30);
|
|
2735
|
-
for (const kw of candidates) {
|
|
2736
|
-
const res = await callToolJSON(client, "get_keyword_scores", {
|
|
2737
|
-
keyword: kw,
|
|
2738
|
-
platform,
|
|
2739
|
-
country: resolvedCountry
|
|
2767
|
+
} else if (platform === "android" && config?.packageName) {
|
|
2768
|
+
autoCompetitors.push({ appId: config.packageName, platform });
|
|
2769
|
+
} else if (platform === "android" && registeredApp?.googlePlay?.packageName) {
|
|
2770
|
+
autoCompetitors.push({
|
|
2771
|
+
appId: registeredApp.googlePlay.packageName,
|
|
2772
|
+
platform
|
|
2740
2773
|
});
|
|
2741
|
-
raw.keywordScores.push(res);
|
|
2742
2774
|
}
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
s?.difficulty?.score ?? s?.scores?.difficulty?.score ?? "0"
|
|
2747
|
-
) || 0;
|
|
2748
|
-
return {
|
|
2749
|
-
keyword: s.keyword || s?.scores?.keyword,
|
|
2750
|
-
trafficScore,
|
|
2751
|
-
difficultyScore,
|
|
2752
|
-
raw: s
|
|
2753
|
-
};
|
|
2754
|
-
}).filter((s) => s.keyword);
|
|
2755
|
-
const recommended = scored.sort((a, b) => b.trafficScore - a.trafficScore).slice(0, 15);
|
|
2756
|
-
const recommendedKeywords = recommended.map((r) => r.keyword).filter(Boolean);
|
|
2757
|
-
const top1 = recommended[0];
|
|
2758
|
-
const top3 = recommendedKeywords.slice(0, 3);
|
|
2759
|
-
const rationaleText = top1 && top1.keyword ? `Highest traffic: ${top1.keyword} (traffic ${top1.trafficScore.toFixed(
|
|
2760
|
-
2
|
|
2761
|
-
)}, difficulty ${top1.difficultyScore.toFixed(
|
|
2762
|
-
2
|
|
2763
|
-
)}). Blend core/high-traffic and mid/longtail terms for coverage.` : "Sorted by traffic score (desc). Blend of core/high-traffic and mid/longtail technical terms.";
|
|
2764
|
-
const nextActionsText = recommendedKeywords.length ? `Use top keywords in improve-public Stage 1. Emphasize: ${top3.join(
|
|
2765
|
-
", "
|
|
2766
|
-
)}. Keep app name in English; target 10\u201315 keywords in keyword fields.` : "Run again to produce at least 10\u201315 keywords, then feed into improve-public Stage 1.";
|
|
2767
|
-
const summary = {
|
|
2768
|
-
recommendedKeywords,
|
|
2769
|
-
recommendedDetails: recommended.map((r) => ({
|
|
2770
|
-
keyword: r.keyword,
|
|
2771
|
-
trafficScore: r.trafficScore,
|
|
2772
|
-
difficultyScore: r.difficultyScore
|
|
2773
|
-
})),
|
|
2774
|
-
rationale: rationaleText,
|
|
2775
|
-
nextActions: nextActionsText
|
|
2776
|
-
};
|
|
2777
|
-
await client.close();
|
|
2775
|
+
const resolvedSeeds = seedKeywords.length > 0 ? seedKeywords : Array.from(new Set(autoSeeds));
|
|
2776
|
+
const resolvedCompetitors = competitorApps.length > 0 ? competitorApps : autoCompetitors;
|
|
2777
|
+
const resolvedCountry = country || (locale?.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
|
|
2778
2778
|
const researchDir = path11.join(
|
|
2779
2779
|
getKeywordResearchDir(),
|
|
2780
2780
|
"products",
|
|
@@ -2782,52 +2782,172 @@ async function handleKeywordResearch(input) {
|
|
|
2782
2782
|
"locales",
|
|
2783
2783
|
locale
|
|
2784
2784
|
);
|
|
2785
|
-
|
|
2786
|
-
const fileName =
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2785
|
+
const defaultFileName = `keyword-research-${platform}-${resolvedCountry}.json`;
|
|
2786
|
+
const fileName = filename || defaultFileName;
|
|
2787
|
+
let outputPath = path11.join(researchDir, fileName);
|
|
2788
|
+
let fileAction;
|
|
2789
|
+
const parseJsonWithContext = (text) => {
|
|
2790
|
+
try {
|
|
2791
|
+
return JSON.parse(text);
|
|
2792
|
+
} catch (err) {
|
|
2793
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2794
|
+
const match = /position (\d+)/i.exec(message) || /column (\d+)/i.exec(message) || /char (\d+)/i.exec(message);
|
|
2795
|
+
if (match) {
|
|
2796
|
+
const pos = Number(match[1]);
|
|
2797
|
+
const start = Math.max(0, pos - 40);
|
|
2798
|
+
const end = Math.min(text.length, pos + 40);
|
|
2799
|
+
const context = text.slice(start, end);
|
|
2800
|
+
throw new Error(
|
|
2801
|
+
`Failed to parse researchData JSON: ${message}
|
|
2802
|
+
Context around ${pos}: ${context}`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
throw new Error(`Failed to parse researchData JSON: ${message}`);
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
const loadResearchDataFromPath = (p) => {
|
|
2809
|
+
if (!fs11.existsSync(p)) {
|
|
2810
|
+
throw new Error(`researchDataPath not found: ${p}`);
|
|
2811
|
+
}
|
|
2812
|
+
const raw = fs11.readFileSync(p, "utf-8");
|
|
2813
|
+
return parseJsonWithContext(raw);
|
|
2814
|
+
};
|
|
2815
|
+
if (writeTemplate || researchData) {
|
|
2816
|
+
const payload = researchData ? parseJsonWithContext(researchData) : researchDataPath ? loadResearchDataFromPath(researchDataPath) : buildTemplate({
|
|
2790
2817
|
slug,
|
|
2791
2818
|
locale,
|
|
2792
2819
|
platform,
|
|
2793
2820
|
country: resolvedCountry,
|
|
2794
|
-
seedKeywords:
|
|
2795
|
-
competitorApps:
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
},
|
|
2813
|
-
data: { raw, summary }
|
|
2814
|
-
};
|
|
2815
|
-
fs11.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
2821
|
+
seedKeywords: resolvedSeeds,
|
|
2822
|
+
competitorApps: resolvedCompetitors
|
|
2823
|
+
});
|
|
2824
|
+
outputPath = saveJsonFile({ researchDir, fileName, payload });
|
|
2825
|
+
fileAction = researchData ? "Saved provided researchData" : "Wrote template";
|
|
2826
|
+
}
|
|
2827
|
+
const templatePreview = JSON.stringify(
|
|
2828
|
+
buildTemplate({
|
|
2829
|
+
slug,
|
|
2830
|
+
locale,
|
|
2831
|
+
platform,
|
|
2832
|
+
country: resolvedCountry,
|
|
2833
|
+
seedKeywords: resolvedSeeds,
|
|
2834
|
+
competitorApps: resolvedCompetitors
|
|
2835
|
+
}),
|
|
2836
|
+
null,
|
|
2837
|
+
2
|
|
2838
|
+
);
|
|
2816
2839
|
const lines = [];
|
|
2817
|
-
lines.push(`#
|
|
2818
|
-
lines.push(`
|
|
2840
|
+
lines.push(`# Keyword research plan (${slug})`);
|
|
2841
|
+
lines.push(`Locale: ${locale} | Platform: ${platform} | Country: ${resolvedCountry}`);
|
|
2842
|
+
lines.push(`Primary locale detected: ${primaryLocale}`);
|
|
2843
|
+
if (supportedLocales.length > 0) {
|
|
2844
|
+
lines.push(
|
|
2845
|
+
`Registered supported locales (${platform}): ${supportedLocales.join(
|
|
2846
|
+
", "
|
|
2847
|
+
)} (source: ${supportedPath})`
|
|
2848
|
+
);
|
|
2849
|
+
if (!supportedLocales.includes(locale)) {
|
|
2850
|
+
lines.push(
|
|
2851
|
+
`WARNING: locale ${locale} not in registered supported locales. Confirm this locale or update registered-apps.json.`
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
} else {
|
|
2855
|
+
lines.push(
|
|
2856
|
+
`Registered supported locales not found for ${platform} (checked: ${supportedPath}).`
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
lines.push(
|
|
2860
|
+
`Seeds: ${resolvedSeeds.length > 0 ? resolvedSeeds.join(", ") : "(none set; add seedKeywords or ensure ASO keywords/title exist)"}`
|
|
2861
|
+
);
|
|
2862
|
+
lines.push(
|
|
2863
|
+
`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)"}`
|
|
2864
|
+
);
|
|
2865
|
+
lines.push("");
|
|
2866
|
+
lines.push("## Research Workflow (mcp-appstore)");
|
|
2867
|
+
lines.push("");
|
|
2868
|
+
lines.push("### Phase 1: Setup & Discovery");
|
|
2869
|
+
lines.push(
|
|
2870
|
+
`1) Start mcp-appstore server: node server.js (cwd: external-tools/mcp-appstore)`
|
|
2871
|
+
);
|
|
2872
|
+
lines.push(
|
|
2873
|
+
`2) get_app_details(appId) \u2192 confirm app identity, category, current metadata`
|
|
2874
|
+
);
|
|
2875
|
+
lines.push(
|
|
2876
|
+
`3) search_app(term=seed, num=15, platform=${platform}, country=${resolvedCountry}) \u2192 find direct competitors`
|
|
2877
|
+
);
|
|
2878
|
+
lines.push(
|
|
2879
|
+
`4) get_similar_apps(appId=your app, num=20) \u2192 discover related apps in your space`
|
|
2880
|
+
);
|
|
2881
|
+
lines.push("");
|
|
2882
|
+
lines.push("### Phase 2: Keyword Mining (run ALL of these)");
|
|
2883
|
+
lines.push(
|
|
2884
|
+
`5) suggest_keywords_by_apps(apps=[top 5 competitors]) \u2192 steal competitor keywords`
|
|
2885
|
+
);
|
|
2886
|
+
lines.push(
|
|
2887
|
+
`6) suggest_keywords_by_seeds(seeds=[app name, core features], num=30)`
|
|
2888
|
+
);
|
|
2889
|
+
lines.push(
|
|
2890
|
+
`7) suggest_keywords_by_category(category=your primary category, num=30)`
|
|
2891
|
+
);
|
|
2892
|
+
lines.push(
|
|
2893
|
+
`8) suggest_keywords_by_similarity + by_competition + by_search (num=30 each)`
|
|
2894
|
+
);
|
|
2895
|
+
lines.push("");
|
|
2896
|
+
lines.push("### Phase 3: Scoring & Filtering");
|
|
2897
|
+
lines.push(
|
|
2898
|
+
`9) get_keyword_scores for ALL candidates (50-100 keywords) \u2192 get traffic & difficulty`
|
|
2899
|
+
);
|
|
2900
|
+
lines.push(
|
|
2901
|
+
`10) Filter: traffic \u226510, difficulty \u226470, relevant to your app's core value`
|
|
2902
|
+
);
|
|
2903
|
+
lines.push("");
|
|
2904
|
+
lines.push("### Phase 4: User Language Intelligence");
|
|
2905
|
+
lines.push(
|
|
2906
|
+
`11) analyze_reviews(appId=top 3 competitors, num=200) \u2192 sentiment & themes`
|
|
2907
|
+
);
|
|
2908
|
+
lines.push(
|
|
2909
|
+
`12) fetch_reviews(appId=top 3 competitors, num=100) \u2192 extract exact phrases users say`
|
|
2910
|
+
);
|
|
2911
|
+
lines.push(
|
|
2912
|
+
`13) Cross-reference keywords with review language \u2192 validate natural phrasing`
|
|
2913
|
+
);
|
|
2914
|
+
lines.push("");
|
|
2915
|
+
lines.push("### Phase 5: Final Selection");
|
|
2819
2916
|
lines.push(
|
|
2820
|
-
`
|
|
2917
|
+
`14) Categorize into tiers: Tier1 (3-5 high-traffic core), Tier2 (5-7 feature-match), Tier3 (5-8 longtail)`
|
|
2821
2918
|
);
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
)
|
|
2919
|
+
lines.push(
|
|
2920
|
+
`15) Validate each keyword has: score, intent, locale fit, inclusion rationale`
|
|
2921
|
+
);
|
|
2922
|
+
lines.push(
|
|
2923
|
+
`16) Save to: ${outputPath}`
|
|
2924
|
+
);
|
|
2925
|
+
lines.push("");
|
|
2926
|
+
lines.push("### Quality Checklist");
|
|
2927
|
+
lines.push("- [ ] 15-20 keywords with complete score data");
|
|
2928
|
+
lines.push("- [ ] All 3 tiers represented (not just longtail)");
|
|
2929
|
+
lines.push("- [ ] Keywords validated against actual review language");
|
|
2930
|
+
lines.push("- [ ] No semantic duplicates");
|
|
2931
|
+
lines.push("- [ ] Locale-appropriate (not direct translations)");
|
|
2932
|
+
if (fileAction) {
|
|
2933
|
+
lines.push(`File: ${fileAction} at ${outputPath}`);
|
|
2934
|
+
} else {
|
|
2825
2935
|
lines.push(
|
|
2826
|
-
`
|
|
2936
|
+
`Tip: set writeTemplate=true to create the JSON skeleton at ${outputPath}`
|
|
2827
2937
|
);
|
|
2828
2938
|
}
|
|
2939
|
+
lines.push("");
|
|
2940
|
+
lines.push("Suggested JSON shape:");
|
|
2941
|
+
lines.push("```json");
|
|
2942
|
+
lines.push(templatePreview);
|
|
2943
|
+
lines.push("```");
|
|
2829
2944
|
return {
|
|
2830
|
-
content: [
|
|
2945
|
+
content: [
|
|
2946
|
+
{
|
|
2947
|
+
type: "text",
|
|
2948
|
+
text: lines.join("\n")
|
|
2949
|
+
}
|
|
2950
|
+
]
|
|
2831
2951
|
};
|
|
2832
2952
|
}
|
|
2833
2953
|
|
|
@@ -2880,6 +3000,14 @@ var tools = [
|
|
|
2880
3000
|
zodSchema: keywordResearchInputSchema,
|
|
2881
3001
|
handler: handleKeywordResearch,
|
|
2882
3002
|
category: "ASO Research"
|
|
3003
|
+
},
|
|
3004
|
+
{
|
|
3005
|
+
name: searchAppTool.name,
|
|
3006
|
+
description: searchAppTool.description,
|
|
3007
|
+
inputSchema: searchAppTool.inputSchema,
|
|
3008
|
+
zodSchema: searchAppInputSchema,
|
|
3009
|
+
handler: handleSearchApp,
|
|
3010
|
+
category: "App Management"
|
|
2883
3011
|
}
|
|
2884
3012
|
];
|
|
2885
3013
|
function getToolDefinitions() {
|
|
@@ -2889,7 +3017,8 @@ function getToolDefinitions() {
|
|
|
2889
3017
|
improvePublicTool,
|
|
2890
3018
|
initProjectTool,
|
|
2891
3019
|
createBlogHtmlTool,
|
|
2892
|
-
keywordResearchTool
|
|
3020
|
+
keywordResearchTool,
|
|
3021
|
+
searchAppTool
|
|
2893
3022
|
];
|
|
2894
3023
|
}
|
|
2895
3024
|
function getToolHandler(name) {
|