pabal-web-mcp 1.3.0 → 1.3.1

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"),
@@ -2431,26 +2484,31 @@ function buildTemplate({
2431
2484
  },
2432
2485
  plan: {
2433
2486
  steps: [
2434
- "Start mcp-appstore server (npm start in external-tools/mcp-appstore).",
2487
+ "Start mcp-appstore server (node server.js in external-tools/mcp-appstore).",
2488
+ "Confirm app IDs/locales: get_app_details(appId from config/registered-apps) to lock country/lang and competitors.",
2435
2489
  "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.",
2490
+ "Collect candidates: suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search + suggest_keywords_by_apps(apps=[top competitors]).",
2437
2491
  "Score shortlist: get_keyword_scores for 15\u201330 candidates per platform/country.",
2438
- "Context check: analyze_reviews on top apps for language/tone cues."
2492
+ "Context check: analyze_reviews and fetch_reviews on top apps for language/tone cues."
2439
2493
  ],
2440
2494
  note: "Run per platform/country. Save raw tool outputs plus curated top keywords."
2441
2495
  },
2442
2496
  data: {
2443
2497
  raw: {
2444
2498
  searchApp: [],
2499
+ getAppDetails: [],
2500
+ similarApps: [],
2445
2501
  keywordSuggestions: {
2446
2502
  bySeeds: [],
2447
2503
  byCategory: [],
2448
2504
  bySimilarity: [],
2449
2505
  byCompetition: [],
2450
- bySearchHints: []
2506
+ bySearchHints: [],
2507
+ byApps: []
2451
2508
  },
2452
2509
  keywordScores: [],
2453
- reviewsAnalysis: []
2510
+ reviewsAnalysis: [],
2511
+ reviewsRaw: []
2454
2512
  },
2455
2513
  summary: {
2456
2514
  recommendedKeywords: [],
@@ -2465,9 +2523,9 @@ function saveJsonFile({
2465
2523
  fileName,
2466
2524
  payload
2467
2525
  }) {
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");
2526
+ fs11.mkdirSync(researchDir, { recursive: true });
2527
+ const outputPath = path11.join(researchDir, fileName);
2528
+ fs11.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
2471
2529
  return outputPath;
2472
2530
  }
2473
2531
  function normalizeKeywords(raw) {
@@ -2492,6 +2550,8 @@ async function handleKeywordResearch(input) {
2492
2550
  const { config, locales } = loadProductLocales(slug);
2493
2551
  const primaryLocale = resolvePrimaryLocale(config, locales);
2494
2552
  const primaryLocaleData = locales[primaryLocale];
2553
+ const { app: registeredApp, path: registeredPath } = findRegisteredApp(slug);
2554
+ const { supportedLocales, path: supportedPath } = getSupportedLocalesForSlug(slug, platform);
2495
2555
  const autoSeeds = [];
2496
2556
  const autoCompetitors = [];
2497
2557
  if (primaryLocaleData?.aso?.title) {
@@ -2501,19 +2561,43 @@ async function handleKeywordResearch(input) {
2501
2561
  autoSeeds.push(...parsedKeywords.slice(0, 5));
2502
2562
  if (config?.name) autoSeeds.push(config.name);
2503
2563
  if (config?.tagline) autoSeeds.push(config.tagline);
2564
+ if (!config?.name && registeredApp?.name) autoSeeds.push(registeredApp.name);
2565
+ if (!primaryLocaleData?.aso?.title) {
2566
+ if (platform === "ios" && registeredApp?.appStore?.name) {
2567
+ autoSeeds.push(registeredApp.appStore.name);
2568
+ }
2569
+ if (platform === "android" && registeredApp?.googlePlay?.name) {
2570
+ autoSeeds.push(registeredApp.googlePlay.name);
2571
+ }
2572
+ }
2504
2573
  if (platform === "ios") {
2505
2574
  if (config?.appStoreAppId) {
2506
2575
  autoCompetitors.push({ appId: String(config.appStoreAppId), platform });
2507
2576
  } else if (config?.bundleId) {
2508
2577
  autoCompetitors.push({ appId: config.bundleId, platform });
2578
+ } else if (registeredApp?.appStore?.appId) {
2579
+ autoCompetitors.push({
2580
+ appId: String(registeredApp.appStore.appId),
2581
+ platform
2582
+ });
2583
+ } else if (registeredApp?.appStore?.bundleId) {
2584
+ autoCompetitors.push({
2585
+ appId: registeredApp.appStore.bundleId,
2586
+ platform
2587
+ });
2509
2588
  }
2510
2589
  } else if (platform === "android" && config?.packageName) {
2511
2590
  autoCompetitors.push({ appId: config.packageName, platform });
2591
+ } else if (platform === "android" && registeredApp?.googlePlay?.packageName) {
2592
+ autoCompetitors.push({
2593
+ appId: registeredApp.googlePlay.packageName,
2594
+ platform
2595
+ });
2512
2596
  }
2513
2597
  const resolvedSeeds = seedKeywords.length > 0 ? seedKeywords : Array.from(new Set(autoSeeds));
2514
2598
  const resolvedCompetitors = competitorApps.length > 0 ? competitorApps : autoCompetitors;
2515
2599
  const resolvedCountry = country || (locale?.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
2516
- const researchDir = path10.join(
2600
+ const researchDir = path11.join(
2517
2601
  getKeywordResearchDir(),
2518
2602
  "products",
2519
2603
  slug,
@@ -2522,7 +2606,7 @@ async function handleKeywordResearch(input) {
2522
2606
  );
2523
2607
  const defaultFileName = `keyword-research-${platform}-${resolvedCountry}.json`;
2524
2608
  const fileName = filename || defaultFileName;
2525
- let outputPath = path10.join(researchDir, fileName);
2609
+ let outputPath = path11.join(researchDir, fileName);
2526
2610
  let fileAction;
2527
2611
  if (writeTemplate || researchData) {
2528
2612
  const payload = researchData ? (() => {
@@ -2560,6 +2644,22 @@ async function handleKeywordResearch(input) {
2560
2644
  lines.push(`# Keyword research plan (${slug})`);
2561
2645
  lines.push(`Locale: ${locale} | Platform: ${platform} | Country: ${resolvedCountry}`);
2562
2646
  lines.push(`Primary locale detected: ${primaryLocale}`);
2647
+ if (supportedLocales.length > 0) {
2648
+ lines.push(
2649
+ `Registered supported locales (${platform}): ${supportedLocales.join(
2650
+ ", "
2651
+ )} (source: ${supportedPath})`
2652
+ );
2653
+ if (!supportedLocales.includes(locale)) {
2654
+ lines.push(
2655
+ `WARNING: locale ${locale} not in registered supported locales. Confirm this locale or update registered-apps.json.`
2656
+ );
2657
+ }
2658
+ } else {
2659
+ lines.push(
2660
+ `Registered supported locales not found for ${platform} (checked: ${supportedPath}).`
2661
+ );
2662
+ }
2563
2663
  lines.push(
2564
2664
  `Seeds: ${resolvedSeeds.length > 0 ? resolvedSeeds.join(", ") : "(none set; add seedKeywords or ensure ASO keywords/title exist)"}`
2565
2665
  );
@@ -2572,19 +2672,22 @@ async function handleKeywordResearch(input) {
2572
2672
  `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
2673
  );
2574
2674
  lines.push(
2575
- `2) Discover apps: search_app(term=seed, platform=${platform}, country=${country}); get_similar_apps(appId=known competitor).`
2675
+ `2) Confirm IDs/locales: get_app_details(appId from config/registered-apps) to lock locale/country and competitor list.`
2676
+ );
2677
+ lines.push(
2678
+ `3) Discover apps: search_app(term=seed, platform=${platform}, country=${resolvedCountry}); get_similar_apps(appId=known competitor).`
2576
2679
  );
2577
2680
  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.`
2681
+ `4) Expand keywords: suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search + suggest_keywords_by_apps(apps=[top competitors]).`
2579
2682
  );
2580
2683
  lines.push(
2581
- `4) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
2684
+ `5) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
2582
2685
  );
2583
2686
  lines.push(
2584
- `5) Context check: analyze_reviews on top apps to harvest native phrasing; keep snippets for improve-public.`
2687
+ `6) Context check: analyze_reviews and fetch_reviews on top apps to harvest native phrasing; keep snippets for improve-public.`
2585
2688
  );
2586
2689
  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>)`
2690
+ `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
2691
  );
2589
2692
  if (fileAction) {
2590
2693
  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.1",
4
4
  "type": "module",
5
5
  "description": "MCP server for ASO data management with shared types and utilities",
6
6
  "author": "skyu",