pabal-web-mcp 1.3.0 → 1.3.2

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.
@@ -1241,6 +1241,8 @@ ${researchSections.join("\n")}
1241
1241
  prompt += `Apply the selected keywords to ALL fields:
1242
1242
  `;
1243
1243
  prompt += `- \`aso.title\` (\u226430): **"App Name: Primary Keyword"** format (app name in English, keyword in target language, keyword starts with uppercase after the colon)
1244
+ `;
1245
+ prompt += ` - **Do NOT translate/rename the app name**; keep the original English app name across all locales.
1244
1246
  `;
1245
1247
  prompt += `- \`aso.subtitle\` (\u226430): Complementary keywords
1246
1248
  `;
@@ -1445,6 +1447,8 @@ ${researchSections.join(
1445
1447
  prompt += ` - Example: "Aurora EOS: \uC624\uB85C\uB77C \uC608\uBCF4" (Korean), "Aurora EOS: \u30AA\u30FC\u30ED\u30E9\u4E88\u5831" (Japanese)
1446
1448
  `;
1447
1449
  prompt += ` - The keyword after the colon must start with an uppercase letter
1450
+ `;
1451
+ prompt += ` - **Do NOT translate/rename the app name**; keep the original English app name across all locales.
1448
1452
  `;
1449
1453
  prompt += `4. Swap keywords in sentences while keeping:
1450
1454
  `;
@@ -2372,10 +2376,59 @@ Writing style reference for ${locale}: Found ${posts.length} existing post(s) us
2372
2376
  }
2373
2377
 
2374
2378
  // src/tools/keyword-research.ts
2375
- import fs10 from "fs";
2376
- import path10 from "path";
2379
+ import fs11 from "fs";
2380
+ import path11 from "path";
2377
2381
  import { z as z6 } from "zod";
2378
2382
  import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
2383
+
2384
+ // src/utils/registered-apps.util.ts
2385
+ import fs10 from "fs";
2386
+ import os from "os";
2387
+ import path10 from "path";
2388
+ var DEFAULT_REGISTERED_APPS_PATH = path10.join(
2389
+ os.homedir(),
2390
+ ".config",
2391
+ "pabal-mcp",
2392
+ "registered-apps.json"
2393
+ );
2394
+ function safeReadJson(filePath) {
2395
+ if (!fs10.existsSync(filePath)) return null;
2396
+ try {
2397
+ const raw = fs10.readFileSync(filePath, "utf-8");
2398
+ const parsed = JSON.parse(raw);
2399
+ if (!parsed?.apps || !Array.isArray(parsed.apps)) {
2400
+ return null;
2401
+ }
2402
+ return parsed;
2403
+ } catch {
2404
+ return null;
2405
+ }
2406
+ }
2407
+ function loadRegisteredApps(filePath = DEFAULT_REGISTERED_APPS_PATH) {
2408
+ const data = safeReadJson(filePath);
2409
+ return { apps: data?.apps || [], path: filePath };
2410
+ }
2411
+ function findRegisteredApp(slug, filePath) {
2412
+ const { apps, path: usedPath } = loadRegisteredApps(filePath);
2413
+ const app = apps.find((a) => a.slug === slug);
2414
+ return { app, path: usedPath };
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
+ }
2430
+
2431
+ // src/tools/keyword-research.ts
2379
2432
  var TOOL_NAME = "keyword-research";
2380
2433
  var keywordResearchInputSchema = z6.object({
2381
2434
  slug: z6.string().trim().describe("Product slug"),
@@ -2395,6 +2448,9 @@ var keywordResearchInputSchema = z6.object({
2395
2448
  writeTemplate: z6.boolean().default(false).describe("If true, write a JSON template at the output path."),
2396
2449
  researchData: z6.string().trim().optional().describe(
2397
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)."
2398
2454
  )
2399
2455
  });
2400
2456
  var jsonSchema6 = zodToJsonSchema6(keywordResearchInputSchema, {
@@ -2431,26 +2487,31 @@ function buildTemplate({
2431
2487
  },
2432
2488
  plan: {
2433
2489
  steps: [
2434
- "Start mcp-appstore server (npm start in external-tools/mcp-appstore).",
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 competitors.",
2435
2492
  "Discover competitors: search_app(term=seed keyword), get_similar_apps(appId=known competitor).",
2436
- "Collect candidates: suggest_keywords_by_seeds, suggest_keywords_by_category, suggest_keywords_by_similarity, suggest_keywords_by_competition.",
2493
+ "Collect candidates: suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search + suggest_keywords_by_apps(apps=[top competitors]).",
2437
2494
  "Score shortlist: get_keyword_scores for 15\u201330 candidates per platform/country.",
2438
- "Context check: analyze_reviews on top apps for language/tone cues."
2495
+ "Context check: analyze_reviews and fetch_reviews on top apps for language/tone cues."
2439
2496
  ],
2440
2497
  note: "Run per platform/country. Save raw tool outputs plus curated top keywords."
2441
2498
  },
2442
2499
  data: {
2443
2500
  raw: {
2444
2501
  searchApp: [],
2502
+ getAppDetails: [],
2503
+ similarApps: [],
2445
2504
  keywordSuggestions: {
2446
2505
  bySeeds: [],
2447
2506
  byCategory: [],
2448
2507
  bySimilarity: [],
2449
2508
  byCompetition: [],
2450
- bySearchHints: []
2509
+ bySearchHints: [],
2510
+ byApps: []
2451
2511
  },
2452
2512
  keywordScores: [],
2453
- reviewsAnalysis: []
2513
+ reviewsAnalysis: [],
2514
+ reviewsRaw: []
2454
2515
  },
2455
2516
  summary: {
2456
2517
  recommendedKeywords: [],
@@ -2465,9 +2526,9 @@ function saveJsonFile({
2465
2526
  fileName,
2466
2527
  payload
2467
2528
  }) {
2468
- fs10.mkdirSync(researchDir, { recursive: true });
2469
- const outputPath = path10.join(researchDir, fileName);
2470
- fs10.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
2529
+ fs11.mkdirSync(researchDir, { recursive: true });
2530
+ const outputPath = path11.join(researchDir, fileName);
2531
+ fs11.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
2471
2532
  return outputPath;
2472
2533
  }
2473
2534
  function normalizeKeywords(raw) {
@@ -2487,11 +2548,14 @@ async function handleKeywordResearch(input) {
2487
2548
  competitorApps = [],
2488
2549
  filename,
2489
2550
  writeTemplate = false,
2490
- researchData
2551
+ researchData,
2552
+ researchDataPath
2491
2553
  } = input;
2492
2554
  const { config, locales } = loadProductLocales(slug);
2493
2555
  const primaryLocale = resolvePrimaryLocale(config, locales);
2494
2556
  const primaryLocaleData = locales[primaryLocale];
2557
+ const { app: registeredApp, path: registeredPath } = findRegisteredApp(slug);
2558
+ const { supportedLocales, path: supportedPath } = getSupportedLocalesForSlug(slug, platform);
2495
2559
  const autoSeeds = [];
2496
2560
  const autoCompetitors = [];
2497
2561
  if (primaryLocaleData?.aso?.title) {
@@ -2501,19 +2565,43 @@ async function handleKeywordResearch(input) {
2501
2565
  autoSeeds.push(...parsedKeywords.slice(0, 5));
2502
2566
  if (config?.name) autoSeeds.push(config.name);
2503
2567
  if (config?.tagline) autoSeeds.push(config.tagline);
2568
+ if (!config?.name && registeredApp?.name) autoSeeds.push(registeredApp.name);
2569
+ if (!primaryLocaleData?.aso?.title) {
2570
+ if (platform === "ios" && registeredApp?.appStore?.name) {
2571
+ autoSeeds.push(registeredApp.appStore.name);
2572
+ }
2573
+ if (platform === "android" && registeredApp?.googlePlay?.name) {
2574
+ autoSeeds.push(registeredApp.googlePlay.name);
2575
+ }
2576
+ }
2504
2577
  if (platform === "ios") {
2505
2578
  if (config?.appStoreAppId) {
2506
2579
  autoCompetitors.push({ appId: String(config.appStoreAppId), platform });
2507
2580
  } else if (config?.bundleId) {
2508
2581
  autoCompetitors.push({ appId: config.bundleId, platform });
2582
+ } else if (registeredApp?.appStore?.appId) {
2583
+ autoCompetitors.push({
2584
+ appId: String(registeredApp.appStore.appId),
2585
+ platform
2586
+ });
2587
+ } else if (registeredApp?.appStore?.bundleId) {
2588
+ autoCompetitors.push({
2589
+ appId: registeredApp.appStore.bundleId,
2590
+ platform
2591
+ });
2509
2592
  }
2510
2593
  } else if (platform === "android" && config?.packageName) {
2511
2594
  autoCompetitors.push({ appId: config.packageName, platform });
2595
+ } else if (platform === "android" && registeredApp?.googlePlay?.packageName) {
2596
+ autoCompetitors.push({
2597
+ appId: registeredApp.googlePlay.packageName,
2598
+ platform
2599
+ });
2512
2600
  }
2513
2601
  const resolvedSeeds = seedKeywords.length > 0 ? seedKeywords : Array.from(new Set(autoSeeds));
2514
2602
  const resolvedCompetitors = competitorApps.length > 0 ? competitorApps : autoCompetitors;
2515
2603
  const resolvedCountry = country || (locale?.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
2516
- const researchDir = path10.join(
2604
+ const researchDir = path11.join(
2517
2605
  getKeywordResearchDir(),
2518
2606
  "products",
2519
2607
  slug,
@@ -2522,18 +2610,36 @@ async function handleKeywordResearch(input) {
2522
2610
  );
2523
2611
  const defaultFileName = `keyword-research-${platform}-${resolvedCountry}.json`;
2524
2612
  const fileName = filename || defaultFileName;
2525
- let outputPath = path10.join(researchDir, fileName);
2613
+ let outputPath = path11.join(researchDir, fileName);
2526
2614
  let fileAction;
2527
- if (writeTemplate || researchData) {
2528
- const payload = researchData ? (() => {
2529
- try {
2530
- return JSON.parse(researchData);
2531
- } catch (err) {
2615
+ const parseJsonWithContext = (text) => {
2616
+ try {
2617
+ return JSON.parse(text);
2618
+ } catch (err) {
2619
+ const message = err instanceof Error ? err.message : String(err);
2620
+ const match = /position (\d+)/i.exec(message) || /column (\d+)/i.exec(message) || /char (\d+)/i.exec(message);
2621
+ if (match) {
2622
+ const pos = Number(match[1]);
2623
+ const start = Math.max(0, pos - 40);
2624
+ const end = Math.min(text.length, pos + 40);
2625
+ const context = text.slice(start, end);
2532
2626
  throw new Error(
2533
- `Failed to parse researchData JSON: ${err instanceof Error ? err.message : String(err)}`
2627
+ `Failed to parse researchData JSON: ${message}
2628
+ Context around ${pos}: ${context}`
2534
2629
  );
2535
2630
  }
2536
- })() : buildTemplate({
2631
+ throw new Error(`Failed to parse researchData JSON: ${message}`);
2632
+ }
2633
+ };
2634
+ const loadResearchDataFromPath = (p) => {
2635
+ if (!fs11.existsSync(p)) {
2636
+ throw new Error(`researchDataPath not found: ${p}`);
2637
+ }
2638
+ const raw = fs11.readFileSync(p, "utf-8");
2639
+ return parseJsonWithContext(raw);
2640
+ };
2641
+ if (writeTemplate || researchData) {
2642
+ const payload = researchData ? parseJsonWithContext(researchData) : researchDataPath ? loadResearchDataFromPath(researchDataPath) : buildTemplate({
2537
2643
  slug,
2538
2644
  locale,
2539
2645
  platform,
@@ -2560,6 +2666,22 @@ async function handleKeywordResearch(input) {
2560
2666
  lines.push(`# Keyword research plan (${slug})`);
2561
2667
  lines.push(`Locale: ${locale} | Platform: ${platform} | Country: ${resolvedCountry}`);
2562
2668
  lines.push(`Primary locale detected: ${primaryLocale}`);
2669
+ if (supportedLocales.length > 0) {
2670
+ lines.push(
2671
+ `Registered supported locales (${platform}): ${supportedLocales.join(
2672
+ ", "
2673
+ )} (source: ${supportedPath})`
2674
+ );
2675
+ if (!supportedLocales.includes(locale)) {
2676
+ lines.push(
2677
+ `WARNING: locale ${locale} not in registered supported locales. Confirm this locale or update registered-apps.json.`
2678
+ );
2679
+ }
2680
+ } else {
2681
+ lines.push(
2682
+ `Registered supported locales not found for ${platform} (checked: ${supportedPath}).`
2683
+ );
2684
+ }
2563
2685
  lines.push(
2564
2686
  `Seeds: ${resolvedSeeds.length > 0 ? resolvedSeeds.join(", ") : "(none set; add seedKeywords or ensure ASO keywords/title exist)"}`
2565
2687
  );
@@ -2572,19 +2694,22 @@ async function handleKeywordResearch(input) {
2572
2694
  `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.`
2573
2695
  );
2574
2696
  lines.push(
2575
- `2) Discover apps: search_app(term=seed, platform=${platform}, country=${country}); get_similar_apps(appId=known competitor).`
2697
+ `2) Confirm IDs/locales: get_app_details(appId from config/registered-apps) to lock locale/country and competitor list.`
2698
+ );
2699
+ lines.push(
2700
+ `3) Discover apps: search_app(term=seed, platform=${platform}, country=${resolvedCountry}); get_similar_apps(appId=known competitor).`
2576
2701
  );
2577
2702
  lines.push(
2578
- `3) Expand keywords: suggest_keywords_by_seeds, suggest_keywords_by_category, suggest_keywords_by_similarity, suggest_keywords_by_competition, suggest_keywords_by_search.`
2703
+ `4) Expand keywords: suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search + suggest_keywords_by_apps(apps=[top competitors]).`
2579
2704
  );
2580
2705
  lines.push(
2581
- `4) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
2706
+ `5) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
2582
2707
  );
2583
2708
  lines.push(
2584
- `5) Context check: analyze_reviews on top apps to harvest native phrasing; keep snippets for improve-public.`
2709
+ `6) Context check: analyze_reviews and fetch_reviews on top apps to harvest native phrasing; keep snippets for improve-public.`
2585
2710
  );
2586
2711
  lines.push(
2587
- `6) Save all raw responses + your final top 10\u201315 keywords to: ${outputPath} (structure mirrors .aso/pullData/.aso/pushData under products/<slug>/locales/<locale>)`
2712
+ `7) Save all raw responses + your final top 10\u201315 keywords to: ${outputPath} (structure mirrors .aso/pullData/.aso/pushData under products/<slug>/locales/<locale>)`
2588
2713
  );
2589
2714
  if (fileAction) {
2590
2715
  lines.push(`File: ${fileAction} at ${outputPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pabal-web-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",