pabal-web-mcp 1.3.4 → 1.3.5
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 +330 -1
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -2497,7 +2497,7 @@ function buildTemplate({
|
|
|
2497
2497
|
"For any recommended keyword without scores, rerun get_keyword_scores to fill traffic/difficulty.",
|
|
2498
2498
|
"Keep rationale/nextActions in English by default unless you intentionally localize them."
|
|
2499
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)."
|
|
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
2501
|
},
|
|
2502
2502
|
data: {
|
|
2503
2503
|
raw: {
|
|
@@ -2720,6 +2720,9 @@ Context around ${pos}: ${context}`
|
|
|
2720
2720
|
lines.push(
|
|
2721
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
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
|
+
);
|
|
2723
2726
|
if (fileAction) {
|
|
2724
2727
|
lines.push(`File: ${fileAction} at ${outputPath}`);
|
|
2725
2728
|
} else {
|
|
@@ -2742,6 +2745,324 @@ Context around ${pos}: ${context}`
|
|
|
2742
2745
|
};
|
|
2743
2746
|
}
|
|
2744
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
|
+
if (!raw) return [];
|
|
2786
|
+
if (Array.isArray(raw)) {
|
|
2787
|
+
return raw.map((k) => k.trim()).filter(Boolean);
|
|
2788
|
+
}
|
|
2789
|
+
return raw.split(",").map((k) => k.trim()).filter(Boolean);
|
|
2790
|
+
}
|
|
2791
|
+
async function callToolJSON(client, name, args) {
|
|
2792
|
+
const result = await client.callTool({ name, arguments: args });
|
|
2793
|
+
const text = result.content?.[0]?.text;
|
|
2794
|
+
if (!text) return null;
|
|
2795
|
+
try {
|
|
2796
|
+
return JSON.parse(text);
|
|
2797
|
+
} catch {
|
|
2798
|
+
return text;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
function ensureDir(dir) {
|
|
2802
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
2803
|
+
}
|
|
2804
|
+
async function handleKeywordResearchRunner(input) {
|
|
2805
|
+
const {
|
|
2806
|
+
slug,
|
|
2807
|
+
locale,
|
|
2808
|
+
platform = "ios",
|
|
2809
|
+
country,
|
|
2810
|
+
seedKeywords = [],
|
|
2811
|
+
competitorApps = [],
|
|
2812
|
+
serverCwd,
|
|
2813
|
+
serverCommand,
|
|
2814
|
+
numSuggestions = 20
|
|
2815
|
+
} = input;
|
|
2816
|
+
const { config, locales } = loadProductLocales(slug);
|
|
2817
|
+
const primaryLocale = resolvePrimaryLocale(config, locales);
|
|
2818
|
+
const { app: registeredApp } = findRegisteredApp(slug);
|
|
2819
|
+
const resolvedCountry = country || (locale.includes("-") ? locale.split("-")[1].toLowerCase() : "us");
|
|
2820
|
+
const autoSeeds = [];
|
|
2821
|
+
const primaryLocaleData = locales[primaryLocale];
|
|
2822
|
+
if (primaryLocaleData?.aso?.title) autoSeeds.push(primaryLocaleData.aso.title);
|
|
2823
|
+
const parsedKeywords = normalizeKeywords2(primaryLocaleData?.aso?.keywords);
|
|
2824
|
+
autoSeeds.push(...parsedKeywords.slice(0, 5));
|
|
2825
|
+
if (config?.name) autoSeeds.push(config.name);
|
|
2826
|
+
if (config?.tagline) autoSeeds.push(config.tagline);
|
|
2827
|
+
if (registeredApp?.name) autoSeeds.push(registeredApp.name);
|
|
2828
|
+
if (platform === "ios" && registeredApp?.appStore?.name)
|
|
2829
|
+
autoSeeds.push(registeredApp.appStore.name);
|
|
2830
|
+
if (platform === "android" && registeredApp?.googlePlay?.name)
|
|
2831
|
+
autoSeeds.push(registeredApp.googlePlay.name);
|
|
2832
|
+
const seeds = Array.from(/* @__PURE__ */ new Set([...seedKeywords, ...autoSeeds])).slice(0, 10);
|
|
2833
|
+
const autoCompetitors = [];
|
|
2834
|
+
if (platform === "ios") {
|
|
2835
|
+
if (config?.appStoreAppId) {
|
|
2836
|
+
autoCompetitors.push({ appId: String(config.appStoreAppId), platform });
|
|
2837
|
+
} else if (config?.bundleId) {
|
|
2838
|
+
autoCompetitors.push({ appId: config.bundleId, platform });
|
|
2839
|
+
} else if (registeredApp?.appStore?.appId) {
|
|
2840
|
+
autoCompetitors.push({ appId: String(registeredApp.appStore.appId), platform });
|
|
2841
|
+
} else if (registeredApp?.appStore?.bundleId) {
|
|
2842
|
+
autoCompetitors.push({ appId: registeredApp.appStore.bundleId, platform });
|
|
2843
|
+
}
|
|
2844
|
+
} else {
|
|
2845
|
+
if (config?.packageName) {
|
|
2846
|
+
autoCompetitors.push({ appId: config.packageName, platform });
|
|
2847
|
+
} else if (registeredApp?.googlePlay?.packageName) {
|
|
2848
|
+
autoCompetitors.push({
|
|
2849
|
+
appId: registeredApp.googlePlay.packageName,
|
|
2850
|
+
platform
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
const competitors = Array.from(
|
|
2855
|
+
new Map(
|
|
2856
|
+
[...competitorApps, ...autoCompetitors].map((c) => [
|
|
2857
|
+
`${c.platform}:${c.appId}`,
|
|
2858
|
+
c
|
|
2859
|
+
])
|
|
2860
|
+
).values()
|
|
2861
|
+
).slice(0, 5);
|
|
2862
|
+
const cwd = serverCwd || path12.resolve(process.cwd(), "external-tools", "mcp-appstore");
|
|
2863
|
+
const cmd = serverCommand?.[0] || "node";
|
|
2864
|
+
const args = serverCommand?.length ? serverCommand.slice(1) : ["server.js"];
|
|
2865
|
+
const transport = new StdioClientTransport({ command: cmd, args, cwd });
|
|
2866
|
+
const client = new Client(
|
|
2867
|
+
{ name: "keyword-research-runner", version: "1.0.0" },
|
|
2868
|
+
{ capabilities: { tools: {} } }
|
|
2869
|
+
);
|
|
2870
|
+
await client.connect(transport);
|
|
2871
|
+
const raw = {
|
|
2872
|
+
searchApp: [],
|
|
2873
|
+
getAppDetails: [],
|
|
2874
|
+
similarApps: [],
|
|
2875
|
+
keywordSuggestions: {
|
|
2876
|
+
bySeeds: [],
|
|
2877
|
+
byCategory: [],
|
|
2878
|
+
bySimilarity: [],
|
|
2879
|
+
byCompetition: [],
|
|
2880
|
+
bySearchHints: [],
|
|
2881
|
+
byApps: []
|
|
2882
|
+
},
|
|
2883
|
+
keywordScores: [],
|
|
2884
|
+
reviewsAnalysis: [],
|
|
2885
|
+
reviewsRaw: []
|
|
2886
|
+
};
|
|
2887
|
+
for (const term of seeds.slice(0, 3)) {
|
|
2888
|
+
const res = await callToolJSON(client, "search_app", {
|
|
2889
|
+
term,
|
|
2890
|
+
platform,
|
|
2891
|
+
num: 20,
|
|
2892
|
+
country: resolvedCountry
|
|
2893
|
+
});
|
|
2894
|
+
raw.searchApp.push({ query: term, platform, country: resolvedCountry, results: res?.results ?? res });
|
|
2895
|
+
}
|
|
2896
|
+
for (const comp of competitors.slice(0, 5)) {
|
|
2897
|
+
const res = await callToolJSON(client, "get_app_details", {
|
|
2898
|
+
appId: comp.appId,
|
|
2899
|
+
platform: comp.platform,
|
|
2900
|
+
country: resolvedCountry
|
|
2901
|
+
});
|
|
2902
|
+
raw.getAppDetails.push(res);
|
|
2903
|
+
}
|
|
2904
|
+
if (competitors.length > 0) {
|
|
2905
|
+
const first = competitors[0];
|
|
2906
|
+
const res = await callToolJSON(client, "get_similar_apps", {
|
|
2907
|
+
appId: first.appId,
|
|
2908
|
+
platform,
|
|
2909
|
+
num: 10,
|
|
2910
|
+
country: resolvedCountry
|
|
2911
|
+
});
|
|
2912
|
+
raw.similarApps = res?.similarApps || res || [];
|
|
2913
|
+
}
|
|
2914
|
+
const firstComp = competitors[0]?.appId;
|
|
2915
|
+
const appsList = competitors.map((c) => c.appId);
|
|
2916
|
+
const suggestCalls = [
|
|
2917
|
+
[
|
|
2918
|
+
"suggest_keywords_by_seeds",
|
|
2919
|
+
{ keywords: seeds, platform, num: numSuggestions, country: resolvedCountry },
|
|
2920
|
+
"bySeeds"
|
|
2921
|
+
],
|
|
2922
|
+
[
|
|
2923
|
+
"suggest_keywords_by_category",
|
|
2924
|
+
firstComp ? { appId: firstComp, platform, num: numSuggestions, country: resolvedCountry } : null,
|
|
2925
|
+
"byCategory"
|
|
2926
|
+
],
|
|
2927
|
+
[
|
|
2928
|
+
"suggest_keywords_by_similarity",
|
|
2929
|
+
firstComp ? { appId: firstComp, platform, num: numSuggestions, country: resolvedCountry } : null,
|
|
2930
|
+
"bySimilarity"
|
|
2931
|
+
],
|
|
2932
|
+
[
|
|
2933
|
+
"suggest_keywords_by_competition",
|
|
2934
|
+
firstComp ? { appId: firstComp, platform, num: numSuggestions, country: resolvedCountry } : null,
|
|
2935
|
+
"byCompetition"
|
|
2936
|
+
],
|
|
2937
|
+
[
|
|
2938
|
+
"suggest_keywords_by_search",
|
|
2939
|
+
{ keywords: seeds.slice(0, 5), platform, num: numSuggestions, country: resolvedCountry },
|
|
2940
|
+
"bySearchHints"
|
|
2941
|
+
],
|
|
2942
|
+
[
|
|
2943
|
+
"suggest_keywords_by_apps",
|
|
2944
|
+
appsList.length ? { apps: appsList, platform, num: numSuggestions, country: resolvedCountry } : null,
|
|
2945
|
+
"byApps"
|
|
2946
|
+
]
|
|
2947
|
+
];
|
|
2948
|
+
for (const [tool, args2, key] of suggestCalls) {
|
|
2949
|
+
if (!args2) continue;
|
|
2950
|
+
const res = await callToolJSON(client, tool, args2);
|
|
2951
|
+
const suggestions = res?.suggestions || res || [];
|
|
2952
|
+
raw.keywordSuggestions[key] = suggestions;
|
|
2953
|
+
}
|
|
2954
|
+
for (const comp of competitors.slice(0, 2)) {
|
|
2955
|
+
const analysis = await callToolJSON(client, "analyze_reviews", {
|
|
2956
|
+
appId: comp.appId,
|
|
2957
|
+
platform: comp.platform,
|
|
2958
|
+
country: resolvedCountry,
|
|
2959
|
+
num: 150
|
|
2960
|
+
});
|
|
2961
|
+
raw.reviewsAnalysis.push(analysis);
|
|
2962
|
+
const rawReviews = await callToolJSON(client, "fetch_reviews", {
|
|
2963
|
+
appId: comp.appId,
|
|
2964
|
+
platform: comp.platform,
|
|
2965
|
+
country: resolvedCountry,
|
|
2966
|
+
num: 80
|
|
2967
|
+
});
|
|
2968
|
+
raw.reviewsRaw.push(rawReviews);
|
|
2969
|
+
}
|
|
2970
|
+
const candidateSet = /* @__PURE__ */ new Set();
|
|
2971
|
+
const addCandidates = (arr) => {
|
|
2972
|
+
if (!arr) return;
|
|
2973
|
+
if (Array.isArray(arr)) {
|
|
2974
|
+
arr.forEach((v) => {
|
|
2975
|
+
if (typeof v === "string") candidateSet.add(v);
|
|
2976
|
+
if (v?.keyword) candidateSet.add(String(v.keyword));
|
|
2977
|
+
if (v?.suggestions)
|
|
2978
|
+
addCandidates(v.suggestions);
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
seeds.forEach((s) => candidateSet.add(s));
|
|
2983
|
+
Object.values(raw.keywordSuggestions).forEach((arr) => addCandidates(arr));
|
|
2984
|
+
const candidates = Array.from(candidateSet).slice(0, 30);
|
|
2985
|
+
for (const kw of candidates) {
|
|
2986
|
+
const res = await callToolJSON(client, "get_keyword_scores", {
|
|
2987
|
+
keyword: kw,
|
|
2988
|
+
platform,
|
|
2989
|
+
country: resolvedCountry
|
|
2990
|
+
});
|
|
2991
|
+
raw.keywordScores.push(res);
|
|
2992
|
+
}
|
|
2993
|
+
const scored = raw.keywordScores.map((s) => {
|
|
2994
|
+
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;
|
|
2996
|
+
return {
|
|
2997
|
+
keyword: s.keyword || s?.scores?.keyword,
|
|
2998
|
+
trafficScore,
|
|
2999
|
+
difficultyScore,
|
|
3000
|
+
raw: s
|
|
3001
|
+
};
|
|
3002
|
+
}).filter((s) => s.keyword);
|
|
3003
|
+
const recommended = scored.sort((a, b) => b.trafficScore - a.trafficScore).slice(0, 15);
|
|
3004
|
+
const summary = {
|
|
3005
|
+
recommendedKeywords: recommended.map((r) => ({
|
|
3006
|
+
keyword: r.keyword,
|
|
3007
|
+
trafficScore: r.trafficScore,
|
|
3008
|
+
difficultyScore: r.difficultyScore
|
|
3009
|
+
})),
|
|
3010
|
+
rationale: "Sorted by traffic score (desc). Blend of core/high-traffic and mid/longtail technical terms.",
|
|
3011
|
+
nextActions: "Feed top 10\u201315 into improve-public Stage 1. If any sections are empty (suggestions/reviews), add more competitors/seeds and rerun."
|
|
3012
|
+
};
|
|
3013
|
+
const researchDir = path12.join(
|
|
3014
|
+
getKeywordResearchDir(),
|
|
3015
|
+
"products",
|
|
3016
|
+
slug,
|
|
3017
|
+
"locales",
|
|
3018
|
+
locale
|
|
3019
|
+
);
|
|
3020
|
+
ensureDir(researchDir);
|
|
3021
|
+
const fileName = `keyword-research-${platform}-${resolvedCountry}.json`;
|
|
3022
|
+
const outputPath = path12.join(researchDir, fileName);
|
|
3023
|
+
const payload = {
|
|
3024
|
+
meta: {
|
|
3025
|
+
slug,
|
|
3026
|
+
locale,
|
|
3027
|
+
platform,
|
|
3028
|
+
country: resolvedCountry,
|
|
3029
|
+
seedKeywords: seeds,
|
|
3030
|
+
competitorApps: competitors,
|
|
3031
|
+
source: "mcp-appstore",
|
|
3032
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3033
|
+
},
|
|
3034
|
+
plan: {
|
|
3035
|
+
steps: [
|
|
3036
|
+
"Start mcp-appstore server (node server.js in external-tools/mcp-appstore).",
|
|
3037
|
+
"Confirm app IDs/locales: get_app_details(appId from config/registered-apps) to lock country/lang and 3\u20135 competitors.",
|
|
3038
|
+
"Discover competitors: search_app(term=seed keyword, num=10\u201320), get_similar_apps(appId=top competitor, num=10).",
|
|
3039
|
+
"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]).",
|
|
3040
|
+
"Score shortlist: get_keyword_scores for 15\u201330 candidates per platform/country.",
|
|
3041
|
+
"Context check: analyze_reviews (num=100\u2013200) and fetch_reviews (num=50\u2013100) on top apps for language/tone cues.",
|
|
3042
|
+
"If keywordSuggestions/similar/reviews are sparse, rerun calls (add more competitors/seeds) until you have 10\u201315 strong keywords.",
|
|
3043
|
+
"For any recommended keyword without scores, rerun get_keyword_scores to fill traffic/difficulty.",
|
|
3044
|
+
"Keep rationale/nextActions in English by default unless you intentionally localize them."
|
|
3045
|
+
],
|
|
3046
|
+
note: "Generated by keyword-research-runner"
|
|
3047
|
+
},
|
|
3048
|
+
data: { raw, summary }
|
|
3049
|
+
};
|
|
3050
|
+
fs12.writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
3051
|
+
await client.close();
|
|
3052
|
+
const lines = [];
|
|
3053
|
+
lines.push(`# keyword-research-runner`);
|
|
3054
|
+
lines.push(`Saved to ${outputPath}`);
|
|
3055
|
+
lines.push(
|
|
3056
|
+
`Candidates scored: ${scored.length}, Recommended: ${summary.recommendedKeywords.length}`
|
|
3057
|
+
);
|
|
3058
|
+
lines.push(
|
|
3059
|
+
`Warnings: rerun if keywordSuggestions are empty or recommended < 10.`
|
|
3060
|
+
);
|
|
3061
|
+
return {
|
|
3062
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
|
|
2745
3066
|
// src/tools/index.ts
|
|
2746
3067
|
var tools = [
|
|
2747
3068
|
{
|
|
@@ -2791,6 +3112,13 @@ var tools = [
|
|
|
2791
3112
|
zodSchema: keywordResearchInputSchema,
|
|
2792
3113
|
handler: handleKeywordResearch,
|
|
2793
3114
|
category: "ASO Research"
|
|
3115
|
+
},
|
|
3116
|
+
{
|
|
3117
|
+
name: keywordResearchRunnerTool.name,
|
|
3118
|
+
description: keywordResearchRunnerTool.description,
|
|
3119
|
+
inputSchema: keywordResearchRunnerTool.inputSchema,
|
|
3120
|
+
handler: handleKeywordResearchRunner,
|
|
3121
|
+
category: "ASO Research"
|
|
2794
3122
|
}
|
|
2795
3123
|
];
|
|
2796
3124
|
function getToolDefinitions() {
|
|
@@ -2800,6 +3128,7 @@ function getToolDefinitions() {
|
|
|
2800
3128
|
improvePublicTool,
|
|
2801
3129
|
initProjectTool,
|
|
2802
3130
|
createBlogHtmlTool,
|
|
3131
|
+
keywordResearchRunnerTool,
|
|
2803
3132
|
keywordResearchTool
|
|
2804
3133
|
];
|
|
2805
3134
|
}
|