pabal-web-mcp 1.3.6 → 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 -297
- 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(
|
|
2709
|
+
return raw.map((k) => k.trim()).filter((k) => k.length > 0);
|
|
2455
2710
|
}
|
|
2456
|
-
return raw.split(",").map((k) => k.trim()).filter(
|
|
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;
|
|
2466
|
-
}
|
|
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,212 +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] || "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
|
|
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
|
|
2697
2773
|
});
|
|
2698
|
-
raw.keywordScores.push(res);
|
|
2699
2774
|
}
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
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();
|
|
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");
|
|
2723
2778
|
const researchDir = path11.join(
|
|
2724
2779
|
getKeywordResearchDir(),
|
|
2725
2780
|
"products",
|
|
@@ -2727,52 +2782,172 @@ async function handleKeywordResearch(input) {
|
|
|
2727
2782
|
"locales",
|
|
2728
2783
|
locale
|
|
2729
2784
|
);
|
|
2730
|
-
|
|
2731
|
-
const fileName =
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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({
|
|
2735
2817
|
slug,
|
|
2736
2818
|
locale,
|
|
2737
2819
|
platform,
|
|
2738
2820
|
country: resolvedCountry,
|
|
2739
|
-
seedKeywords:
|
|
2740
|
-
competitorApps:
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
},
|
|
2758
|
-
data: { raw, summary }
|
|
2759
|
-
};
|
|
2760
|
-
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
|
+
);
|
|
2761
2839
|
const lines = [];
|
|
2762
|
-
lines.push(`#
|
|
2763
|
-
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
|
+
);
|
|
2764
2862
|
lines.push(
|
|
2765
|
-
`
|
|
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)"}`
|
|
2766
2864
|
);
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
)
|
|
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");
|
|
2916
|
+
lines.push(
|
|
2917
|
+
`14) Categorize into tiers: Tier1 (3-5 high-traffic core), Tier2 (5-7 feature-match), Tier3 (5-8 longtail)`
|
|
2918
|
+
);
|
|
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 {
|
|
2770
2935
|
lines.push(
|
|
2771
|
-
`
|
|
2936
|
+
`Tip: set writeTemplate=true to create the JSON skeleton at ${outputPath}`
|
|
2772
2937
|
);
|
|
2773
2938
|
}
|
|
2939
|
+
lines.push("");
|
|
2940
|
+
lines.push("Suggested JSON shape:");
|
|
2941
|
+
lines.push("```json");
|
|
2942
|
+
lines.push(templatePreview);
|
|
2943
|
+
lines.push("```");
|
|
2774
2944
|
return {
|
|
2775
|
-
content: [
|
|
2945
|
+
content: [
|
|
2946
|
+
{
|
|
2947
|
+
type: "text",
|
|
2948
|
+
text: lines.join("\n")
|
|
2949
|
+
}
|
|
2950
|
+
]
|
|
2776
2951
|
};
|
|
2777
2952
|
}
|
|
2778
2953
|
|
|
@@ -2825,6 +3000,14 @@ var tools = [
|
|
|
2825
3000
|
zodSchema: keywordResearchInputSchema,
|
|
2826
3001
|
handler: handleKeywordResearch,
|
|
2827
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"
|
|
2828
3011
|
}
|
|
2829
3012
|
];
|
|
2830
3013
|
function getToolDefinitions() {
|
|
@@ -2834,7 +3017,8 @@ function getToolDefinitions() {
|
|
|
2834
3017
|
improvePublicTool,
|
|
2835
3018
|
initProjectTool,
|
|
2836
3019
|
createBlogHtmlTool,
|
|
2837
|
-
keywordResearchTool
|
|
3020
|
+
keywordResearchTool,
|
|
3021
|
+
searchAppTool
|
|
2838
3022
|
];
|
|
2839
3023
|
}
|
|
2840
3024
|
function getToolHandler(name) {
|