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.
- package/dist/bin/mcp-server.js +150 -25
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -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
|
|
2376
|
-
import
|
|
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 (
|
|
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
|
|
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
|
-
|
|
2469
|
-
const outputPath =
|
|
2470
|
-
|
|
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 =
|
|
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 =
|
|
2613
|
+
let outputPath = path11.join(researchDir, fileName);
|
|
2526
2614
|
let fileAction;
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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: ${
|
|
2627
|
+
`Failed to parse researchData JSON: ${message}
|
|
2628
|
+
Context around ${pos}: ${context}`
|
|
2534
2629
|
);
|
|
2535
2630
|
}
|
|
2536
|
-
|
|
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)
|
|
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
|
-
`
|
|
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
|
-
`
|
|
2706
|
+
`5) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
|
|
2582
2707
|
);
|
|
2583
2708
|
lines.push(
|
|
2584
|
-
`
|
|
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
|
-
`
|
|
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}`);
|