pabal-web-mcp 1.3.5 → 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.
@@ -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("Locale code (e.g., en-US, ko-KR). Used for storage under .aso/keywordResearch/products/[slug]/locales/."),
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([]).describe("Seed keywords to start from."),
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([]).describe("Known competitor apps to probe."),
2447
- filename: z6.string().trim().optional().describe("Override output filename. Defaults to keyword-research-[platform]-[country].json"),
2448
- writeTemplate: z6.boolean().default(false).describe("If true, write a JSON template at the output path."),
2449
- researchData: z6.string().trim().optional().describe(
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,328 +2445,10 @@ var jsonSchema6 = zodToJsonSchema6(keywordResearchInputSchema, {
2460
2445
  var inputSchema6 = jsonSchema6.definitions?.KeywordResearchInput || jsonSchema6;
2461
2446
  var keywordResearchTool = {
2462
2447
  name: TOOL_NAME,
2463
- description: `Prep + persist keyword research ahead of improve-public using mcp-appstore outputs.
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). Minimum checklist before saving: search_app results present, keywordSuggestions.* NOT empty, keywordScores >= 10 items, recommendedKeywords 10\u201315 items."
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
- if (!raw) return [];
2539
- if (Array.isArray(raw)) {
2540
- return raw.map((k) => k.trim()).filter((k) => k.length > 0);
2541
- }
2542
- return raw.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
2543
- }
2544
- async function handleKeywordResearch(input) {
2545
- const {
2546
- slug,
2547
- locale,
2548
- platform = "ios",
2549
- country,
2550
- seedKeywords = [],
2551
- competitorApps = [],
2552
- filename,
2553
- writeTemplate = false,
2554
- researchData,
2555
- researchDataPath
2556
- } = input;
2557
- const { config, locales } = loadProductLocales(slug);
2558
- const primaryLocale = resolvePrimaryLocale(config, locales);
2559
- const primaryLocaleData = locales[primaryLocale];
2560
- const { app: registeredApp, path: registeredPath } = findRegisteredApp(slug);
2561
- const { supportedLocales, path: supportedPath } = getSupportedLocalesForSlug(slug, platform);
2562
- const autoSeeds = [];
2563
- const autoCompetitors = [];
2564
- if (primaryLocaleData?.aso?.title) {
2565
- autoSeeds.push(primaryLocaleData.aso.title);
2566
- }
2567
- const parsedKeywords = normalizeKeywords(primaryLocaleData?.aso?.keywords);
2568
- autoSeeds.push(...parsedKeywords.slice(0, 5));
2569
- if (config?.name) autoSeeds.push(config.name);
2570
- if (config?.tagline) autoSeeds.push(config.tagline);
2571
- if (!config?.name && registeredApp?.name) autoSeeds.push(registeredApp.name);
2572
- if (!primaryLocaleData?.aso?.title) {
2573
- if (platform === "ios" && registeredApp?.appStore?.name) {
2574
- autoSeeds.push(registeredApp.appStore.name);
2575
- }
2576
- if (platform === "android" && registeredApp?.googlePlay?.name) {
2577
- autoSeeds.push(registeredApp.googlePlay.name);
2578
- }
2579
- }
2580
- if (platform === "ios") {
2581
- if (config?.appStoreAppId) {
2582
- autoCompetitors.push({ appId: String(config.appStoreAppId), platform });
2583
- } else if (config?.bundleId) {
2584
- autoCompetitors.push({ appId: config.bundleId, platform });
2585
- } else if (registeredApp?.appStore?.appId) {
2586
- autoCompetitors.push({
2587
- appId: String(registeredApp.appStore.appId),
2588
- platform
2589
- });
2590
- } else if (registeredApp?.appStore?.bundleId) {
2591
- autoCompetitors.push({
2592
- appId: registeredApp.appStore.bundleId,
2593
- platform
2594
- });
2595
- }
2596
- } else if (platform === "android" && config?.packageName) {
2597
- autoCompetitors.push({ appId: config.packageName, platform });
2598
- } else if (platform === "android" && registeredApp?.googlePlay?.packageName) {
2599
- autoCompetitors.push({
2600
- appId: registeredApp.googlePlay.packageName,
2601
- platform
2602
- });
2603
- }
2604
- const resolvedSeeds = seedKeywords.length > 0 ? seedKeywords : Array.from(new Set(autoSeeds));
2605
- const resolvedCompetitors = competitorApps.length > 0 ? competitorApps : autoCompetitors;
2606
- const resolvedCountry = country || (locale?.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
2607
- const researchDir = path11.join(
2608
- getKeywordResearchDir(),
2609
- "products",
2610
- slug,
2611
- "locales",
2612
- locale
2613
- );
2614
- const defaultFileName = `keyword-research-${platform}-${resolvedCountry}.json`;
2615
- const fileName = filename || defaultFileName;
2616
- let outputPath = path11.join(researchDir, fileName);
2617
- let fileAction;
2618
- const parseJsonWithContext = (text) => {
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({
2658
- slug,
2659
- locale,
2660
- platform,
2661
- country: resolvedCountry,
2662
- seedKeywords: resolvedSeeds,
2663
- competitorApps: resolvedCompetitors
2664
- }),
2665
- null,
2666
- 2
2667
- );
2668
- const lines = [];
2669
- lines.push(`# Keyword research plan (${slug})`);
2670
- lines.push(`Locale: ${locale} | Platform: ${platform} | Country: ${resolvedCountry}`);
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
- );
2702
- lines.push(
2703
- `3) Discover apps: search_app(term=seed, num=10-20, platform=${platform}, country=${resolvedCountry}); get_similar_apps(appId=top competitor, num=10).`
2704
- );
2705
- lines.push(
2706
- `4) Expand keywords (num=20-30 each): suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search + suggest_keywords_by_apps(apps=[top competitors]).`
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
- lines.push(
2724
- `10) Do not finalize until: search_app has results, keywordSuggestions.* have entries, keywordScores >=10 items, recommendedKeywords 10\u201315 items. If missing, rerun the relevant calls.`
2725
- );
2726
- if (fileAction) {
2727
- lines.push(`File: ${fileAction} at ${outputPath}`);
2728
- } else {
2729
- lines.push(
2730
- `Tip: set writeTemplate=true to create the JSON skeleton at ${outputPath}`
2731
- );
2732
- }
2733
- lines.push("");
2734
- lines.push("Suggested JSON shape:");
2735
- lines.push("```json");
2736
- lines.push(templatePreview);
2737
- lines.push("```");
2738
- return {
2739
- content: [
2740
- {
2741
- type: "text",
2742
- text: lines.join("\n")
2743
- }
2744
- ]
2745
- };
2746
- }
2747
-
2748
- // src/tools/keyword-research-runner.ts
2749
- import fs12 from "fs";
2750
- import path12 from "path";
2751
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2752
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2753
- import { z as z7 } from "zod";
2754
- import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
2755
- var TOOL_NAME2 = "keyword-research-runner";
2756
- var keywordResearchRunnerInputSchema = z7.object({
2757
- slug: z7.string().trim().describe("Product slug"),
2758
- locale: z7.string().trim().describe("Locale code (e.g., en-US, ko-KR)"),
2759
- platform: z7.enum(["ios", "android"]).default("ios"),
2760
- country: z7.string().length(2).optional().describe("Store country (default derived from locale region or 'us')"),
2761
- seedKeywords: z7.array(z7.string().trim()).default([]),
2762
- competitorApps: z7.array(
2763
- z7.object({
2764
- appId: z7.string().trim(),
2765
- platform: z7.enum(["ios", "android"])
2766
- })
2767
- ).default([]),
2768
- serverCwd: z7.string().trim().optional().describe("Path to mcp-appstore cwd (default: external-tools/mcp-appstore)"),
2769
- serverCommand: z7.array(z7.string()).optional().describe(
2770
- "Command + args to start mcp-appstore (default: ['node','server.js'])"
2771
- ),
2772
- numSuggestions: z7.number().int().positive().max(50).default(20).describe("Number of keyword suggestions per strategy (default: 20)")
2773
- });
2774
- var jsonSchema7 = zodToJsonSchema7(keywordResearchRunnerInputSchema, {
2775
- name: "KeywordResearchRunnerInput",
2776
- $refStrategy: "none"
2777
- });
2778
- var inputSchema7 = jsonSchema7.definitions?.KeywordResearchRunnerInput || jsonSchema7;
2779
- var keywordResearchRunnerTool = {
2780
- name: TOOL_NAME2,
2781
- description: "Automatically call mcp-appstore tools to collect 10\u201315 keywords per locale and save to .aso/keywordResearch/...",
2782
- inputSchema: inputSchema7
2783
- };
2784
- function normalizeKeywords2(raw) {
2785
2452
  if (!raw) return [];
2786
2453
  if (Array.isArray(raw)) {
2787
2454
  return raw.map((k) => k.trim()).filter(Boolean);
@@ -2799,9 +2466,9 @@ async function callToolJSON(client, name, args) {
2799
2466
  }
2800
2467
  }
2801
2468
  function ensureDir(dir) {
2802
- fs12.mkdirSync(dir, { recursive: true });
2469
+ fs11.mkdirSync(dir, { recursive: true });
2803
2470
  }
2804
- async function handleKeywordResearchRunner(input) {
2471
+ async function handleKeywordResearch(input) {
2805
2472
  const {
2806
2473
  slug,
2807
2474
  locale,
@@ -2815,12 +2482,12 @@ async function handleKeywordResearchRunner(input) {
2815
2482
  } = input;
2816
2483
  const { config, locales } = loadProductLocales(slug);
2817
2484
  const primaryLocale = resolvePrimaryLocale(config, locales);
2485
+ const primaryLocaleData = locales[primaryLocale];
2818
2486
  const { app: registeredApp } = findRegisteredApp(slug);
2819
2487
  const resolvedCountry = country || (locale.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
2820
2488
  const autoSeeds = [];
2821
- const primaryLocaleData = locales[primaryLocale];
2822
2489
  if (primaryLocaleData?.aso?.title) autoSeeds.push(primaryLocaleData.aso.title);
2823
- const parsedKeywords = normalizeKeywords2(primaryLocaleData?.aso?.keywords);
2490
+ const parsedKeywords = normalizeKeywords(primaryLocaleData?.aso?.keywords);
2824
2491
  autoSeeds.push(...parsedKeywords.slice(0, 5));
2825
2492
  if (config?.name) autoSeeds.push(config.name);
2826
2493
  if (config?.tagline) autoSeeds.push(config.tagline);
@@ -2837,9 +2504,15 @@ async function handleKeywordResearchRunner(input) {
2837
2504
  } else if (config?.bundleId) {
2838
2505
  autoCompetitors.push({ appId: config.bundleId, platform });
2839
2506
  } else if (registeredApp?.appStore?.appId) {
2840
- autoCompetitors.push({ appId: String(registeredApp.appStore.appId), platform });
2507
+ autoCompetitors.push({
2508
+ appId: String(registeredApp.appStore.appId),
2509
+ platform
2510
+ });
2841
2511
  } else if (registeredApp?.appStore?.bundleId) {
2842
- autoCompetitors.push({ appId: registeredApp.appStore.bundleId, platform });
2512
+ autoCompetitors.push({
2513
+ appId: registeredApp.appStore.bundleId,
2514
+ platform
2515
+ });
2843
2516
  }
2844
2517
  } else {
2845
2518
  if (config?.packageName) {
@@ -2859,12 +2532,12 @@ async function handleKeywordResearchRunner(input) {
2859
2532
  ])
2860
2533
  ).values()
2861
2534
  ).slice(0, 5);
2862
- const cwd = serverCwd || path12.resolve(process.cwd(), "external-tools", "mcp-appstore");
2535
+ const cwd = serverCwd || path11.resolve(process.cwd(), "external-tools", "mcp-appstore");
2863
2536
  const cmd = serverCommand?.[0] || "node";
2864
2537
  const args = serverCommand?.length ? serverCommand.slice(1) : ["server.js"];
2865
2538
  const transport = new StdioClientTransport({ command: cmd, args, cwd });
2866
2539
  const client = new Client(
2867
- { name: "keyword-research-runner", version: "1.0.0" },
2540
+ { name: "keyword-research", version: "1.0.0" },
2868
2541
  { capabilities: { tools: {} } }
2869
2542
  );
2870
2543
  await client.connect(transport);
@@ -2891,7 +2564,12 @@ async function handleKeywordResearchRunner(input) {
2891
2564
  num: 20,
2892
2565
  country: resolvedCountry
2893
2566
  });
2894
- raw.searchApp.push({ query: term, platform, country: resolvedCountry, results: res?.results ?? res });
2567
+ raw.searchApp.push({
2568
+ query: term,
2569
+ platform,
2570
+ country: resolvedCountry,
2571
+ results: res?.results ?? res
2572
+ });
2895
2573
  }
2896
2574
  for (const comp of competitors.slice(0, 5)) {
2897
2575
  const res = await callToolJSON(client, "get_app_details", {
@@ -2916,32 +2594,62 @@ async function handleKeywordResearchRunner(input) {
2916
2594
  const suggestCalls = [
2917
2595
  [
2918
2596
  "suggest_keywords_by_seeds",
2919
- { keywords: seeds, platform, num: numSuggestions, country: resolvedCountry },
2597
+ {
2598
+ keywords: seeds,
2599
+ platform,
2600
+ num: numSuggestions,
2601
+ country: resolvedCountry
2602
+ },
2920
2603
  "bySeeds"
2921
2604
  ],
2922
2605
  [
2923
2606
  "suggest_keywords_by_category",
2924
- firstComp ? { appId: firstComp, platform, num: numSuggestions, country: resolvedCountry } : null,
2607
+ firstComp ? {
2608
+ appId: firstComp,
2609
+ platform,
2610
+ num: numSuggestions,
2611
+ country: resolvedCountry
2612
+ } : null,
2925
2613
  "byCategory"
2926
2614
  ],
2927
2615
  [
2928
2616
  "suggest_keywords_by_similarity",
2929
- firstComp ? { appId: firstComp, platform, num: numSuggestions, country: resolvedCountry } : null,
2617
+ firstComp ? {
2618
+ appId: firstComp,
2619
+ platform,
2620
+ num: numSuggestions,
2621
+ country: resolvedCountry
2622
+ } : null,
2930
2623
  "bySimilarity"
2931
2624
  ],
2932
2625
  [
2933
2626
  "suggest_keywords_by_competition",
2934
- firstComp ? { appId: firstComp, platform, num: numSuggestions, country: resolvedCountry } : null,
2627
+ firstComp ? {
2628
+ appId: firstComp,
2629
+ platform,
2630
+ num: numSuggestions,
2631
+ country: resolvedCountry
2632
+ } : null,
2935
2633
  "byCompetition"
2936
2634
  ],
2937
2635
  [
2938
2636
  "suggest_keywords_by_search",
2939
- { keywords: seeds.slice(0, 5), platform, num: numSuggestions, country: resolvedCountry },
2637
+ {
2638
+ keywords: seeds.slice(0, 5),
2639
+ platform,
2640
+ num: numSuggestions,
2641
+ country: resolvedCountry
2642
+ },
2940
2643
  "bySearchHints"
2941
2644
  ],
2942
2645
  [
2943
2646
  "suggest_keywords_by_apps",
2944
- appsList.length ? { apps: appsList, platform, num: numSuggestions, country: resolvedCountry } : null,
2647
+ appsList.length ? {
2648
+ apps: appsList,
2649
+ platform,
2650
+ num: numSuggestions,
2651
+ country: resolvedCountry
2652
+ } : null,
2945
2653
  "byApps"
2946
2654
  ]
2947
2655
  ];
@@ -2974,8 +2682,7 @@ async function handleKeywordResearchRunner(input) {
2974
2682
  arr.forEach((v) => {
2975
2683
  if (typeof v === "string") candidateSet.add(v);
2976
2684
  if (v?.keyword) candidateSet.add(String(v.keyword));
2977
- if (v?.suggestions)
2978
- addCandidates(v.suggestions);
2685
+ if (v?.suggestions) addCandidates(v.suggestions);
2979
2686
  });
2980
2687
  }
2981
2688
  };
@@ -2992,7 +2699,9 @@ async function handleKeywordResearchRunner(input) {
2992
2699
  }
2993
2700
  const scored = raw.keywordScores.map((s) => {
2994
2701
  const trafficScore = typeof s?.scores?.traffic?.score === "number" ? s.scores.traffic.score : parseFloat(s?.traffic?.score ?? s?.scores?.traffic?.score ?? "0") || 0;
2995
- const difficultyScore = typeof s?.scores?.difficulty?.score === "number" ? s.scores.difficulty.score : parseFloat(s?.difficulty?.score ?? s?.scores?.difficulty?.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;
2996
2705
  return {
2997
2706
  keyword: s.keyword || s?.scores?.keyword,
2998
2707
  trafficScore,
@@ -3010,7 +2719,8 @@ async function handleKeywordResearchRunner(input) {
3010
2719
  rationale: "Sorted by traffic score (desc). Blend of core/high-traffic and mid/longtail technical terms.",
3011
2720
  nextActions: "Feed top 10\u201315 into improve-public Stage 1. If any sections are empty (suggestions/reviews), add more competitors/seeds and rerun."
3012
2721
  };
3013
- const researchDir = path12.join(
2722
+ await client.close();
2723
+ const researchDir = path11.join(
3014
2724
  getKeywordResearchDir(),
3015
2725
  "products",
3016
2726
  slug,
@@ -3019,7 +2729,7 @@ async function handleKeywordResearchRunner(input) {
3019
2729
  );
3020
2730
  ensureDir(researchDir);
3021
2731
  const fileName = `keyword-research-${platform}-${resolvedCountry}.json`;
3022
- const outputPath = path12.join(researchDir, fileName);
2732
+ const outputPath = path11.join(researchDir, fileName);
3023
2733
  const payload = {
3024
2734
  meta: {
3025
2735
  slug,
@@ -3043,21 +2753,24 @@ async function handleKeywordResearchRunner(input) {
3043
2753
  "For any recommended keyword without scores, rerun get_keyword_scores to fill traffic/difficulty.",
3044
2754
  "Keep rationale/nextActions in English by default unless you intentionally localize them."
3045
2755
  ],
3046
- note: "Generated by keyword-research-runner"
2756
+ note: "Generated automatically by keyword-research"
3047
2757
  },
3048
2758
  data: { raw, summary }
3049
2759
  };
3050
- fs12.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
3051
- await client.close();
2760
+ fs11.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
3052
2761
  const lines = [];
3053
- lines.push(`# keyword-research-runner`);
2762
+ lines.push(`# keyword-research`);
3054
2763
  lines.push(`Saved to ${outputPath}`);
3055
2764
  lines.push(
3056
2765
  `Candidates scored: ${scored.length}, Recommended: ${summary.recommendedKeywords.length}`
3057
2766
  );
3058
- lines.push(
3059
- `Warnings: rerun if keywordSuggestions are empty or recommended < 10.`
3060
- );
2767
+ if (scored.length < 10 || summary.recommendedKeywords.length < 10 || Object.values(raw.keywordSuggestions).some(
2768
+ (arr) => Array.isArray(arr) && arr.length === 0
2769
+ )) {
2770
+ lines.push(
2771
+ `Warning: results look sparse. Add more competitors/seeds and rerun.`
2772
+ );
2773
+ }
3061
2774
  return {
3062
2775
  content: [{ type: "text", text: lines.join("\n") }]
3063
2776
  };
@@ -3112,13 +2825,6 @@ var tools = [
3112
2825
  zodSchema: keywordResearchInputSchema,
3113
2826
  handler: handleKeywordResearch,
3114
2827
  category: "ASO Research"
3115
- },
3116
- {
3117
- name: keywordResearchRunnerTool.name,
3118
- description: keywordResearchRunnerTool.description,
3119
- inputSchema: keywordResearchRunnerTool.inputSchema,
3120
- handler: handleKeywordResearchRunner,
3121
- category: "ASO Research"
3122
2828
  }
3123
2829
  ];
3124
2830
  function getToolDefinitions() {
@@ -3128,7 +2834,6 @@ function getToolDefinitions() {
3128
2834
  improvePublicTool,
3129
2835
  initProjectTool,
3130
2836
  createBlogHtmlTool,
3131
- keywordResearchRunnerTool,
3132
2837
  keywordResearchTool
3133
2838
  ];
3134
2839
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-web-mcp",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",