pabal-web-mcp 1.3.3 → 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 +343 -9
- package/package.json +1 -1
package/dist/bin/mcp-server.js
CHANGED
|
@@ -2488,14 +2488,16 @@ function buildTemplate({
|
|
|
2488
2488
|
plan: {
|
|
2489
2489
|
steps: [
|
|
2490
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.",
|
|
2492
|
-
"Discover competitors: search_app(term=seed keyword), get_similar_apps(appId=
|
|
2493
|
-
"Collect candidates: suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search
|
|
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
2494
|
"Score shortlist: get_keyword_scores for 15\u201330 candidates per platform/country.",
|
|
2495
|
-
"Context check: analyze_reviews and fetch_reviews 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."
|
|
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."
|
|
2497
2499
|
],
|
|
2498
|
-
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."
|
|
2499
2501
|
},
|
|
2500
2502
|
data: {
|
|
2501
2503
|
raw: {
|
|
@@ -2698,16 +2700,16 @@ Context around ${pos}: ${context}`
|
|
|
2698
2700
|
`2) Confirm IDs/locales: get_app_details(appId from config/registered-apps) to lock locale/country and competitor list.`
|
|
2699
2701
|
);
|
|
2700
2702
|
lines.push(
|
|
2701
|
-
`3) Discover apps: search_app(term=seed, platform=${platform}, country=${resolvedCountry}); get_similar_apps(appId=
|
|
2703
|
+
`3) Discover apps: search_app(term=seed, num=10-20, platform=${platform}, country=${resolvedCountry}); get_similar_apps(appId=top competitor, num=10).`
|
|
2702
2704
|
);
|
|
2703
2705
|
lines.push(
|
|
2704
|
-
`4) Expand keywords: suggest_keywords_by_seeds/by_category/by_similarity/by_competition/by_search + suggest_keywords_by_apps(apps=[top competitors]).`
|
|
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]).`
|
|
2705
2707
|
);
|
|
2706
2708
|
lines.push(
|
|
2707
2709
|
`5) Score shortlist: get_keyword_scores for 15\u201330 candidates (note: scores are heuristic per README).`
|
|
2708
2710
|
);
|
|
2709
2711
|
lines.push(
|
|
2710
|
-
`6) Context check: analyze_reviews and fetch_reviews on top apps to harvest native phrasing; keep snippets for improve-public.`
|
|
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.`
|
|
2711
2713
|
);
|
|
2712
2714
|
lines.push(
|
|
2713
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>)`
|
|
@@ -2715,6 +2717,12 @@ Context around ${pos}: ${context}`
|
|
|
2715
2717
|
lines.push(
|
|
2716
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.`
|
|
2717
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
|
+
);
|
|
2718
2726
|
if (fileAction) {
|
|
2719
2727
|
lines.push(`File: ${fileAction} at ${outputPath}`);
|
|
2720
2728
|
} else {
|
|
@@ -2737,6 +2745,324 @@ Context around ${pos}: ${context}`
|
|
|
2737
2745
|
};
|
|
2738
2746
|
}
|
|
2739
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
|
+
|
|
2740
3066
|
// src/tools/index.ts
|
|
2741
3067
|
var tools = [
|
|
2742
3068
|
{
|
|
@@ -2786,6 +3112,13 @@ var tools = [
|
|
|
2786
3112
|
zodSchema: keywordResearchInputSchema,
|
|
2787
3113
|
handler: handleKeywordResearch,
|
|
2788
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"
|
|
2789
3122
|
}
|
|
2790
3123
|
];
|
|
2791
3124
|
function getToolDefinitions() {
|
|
@@ -2795,6 +3128,7 @@ function getToolDefinitions() {
|
|
|
2795
3128
|
improvePublicTool,
|
|
2796
3129
|
initProjectTool,
|
|
2797
3130
|
createBlogHtmlTool,
|
|
3131
|
+
keywordResearchRunnerTool,
|
|
2798
3132
|
keywordResearchTool
|
|
2799
3133
|
];
|
|
2800
3134
|
}
|