pagesight 0.16.0 → 0.17.0
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/package.json +1 -1
- package/src/tools/audit.ts +10 -1
- package/src/tools/search.ts +276 -10
- package/src/tools/setup.ts +58 -19
package/package.json
CHANGED
package/src/tools/audit.ts
CHANGED
|
@@ -326,7 +326,16 @@ export function registerAuditTool(server: McpServer): void {
|
|
|
326
326
|
if (pagespeedResult.status === "fulfilled") {
|
|
327
327
|
addPagespeedFindings(pagespeedResult.value, findings);
|
|
328
328
|
} else {
|
|
329
|
-
|
|
329
|
+
const psiErr = String(pagespeedResult.reason);
|
|
330
|
+
const statusMatch = psiErr.match(/\((\d{3})\)/);
|
|
331
|
+
const status = statusMatch ? Number(statusMatch[1]) : 0;
|
|
332
|
+
if (status === 403) {
|
|
333
|
+
errors.push("PageSpeed: SKIPPED (API key not authorized — enable PageSpeed Insights API in Google Cloud)");
|
|
334
|
+
} else if (status === 429) {
|
|
335
|
+
errors.push("PageSpeed: SKIPPED (rate limited — try again later or set GOOGLE_API_KEY)");
|
|
336
|
+
} else {
|
|
337
|
+
errors.push(`PageSpeed: ${psiErr.replace(/:\s*\{[\s\S]*$/, "")}`);
|
|
338
|
+
}
|
|
330
339
|
}
|
|
331
340
|
|
|
332
341
|
// Process robots
|
package/src/tools/search.ts
CHANGED
|
@@ -105,6 +105,59 @@ function humanizeState(state: string): string {
|
|
|
105
105
|
return map[state] ?? state;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// ── Coverage helpers ──
|
|
109
|
+
|
|
110
|
+
const COVERAGE_FILTERS = [
|
|
111
|
+
"not_indexed",
|
|
112
|
+
"server_error",
|
|
113
|
+
"redirect",
|
|
114
|
+
"soft_404",
|
|
115
|
+
"blocked",
|
|
116
|
+
"duplicate",
|
|
117
|
+
"discovered",
|
|
118
|
+
"crawled_not_indexed",
|
|
119
|
+
] as const;
|
|
120
|
+
|
|
121
|
+
type CoverageFilter = (typeof COVERAGE_FILTERS)[number];
|
|
122
|
+
|
|
123
|
+
function matchesCoverageFilter(result: InspectionSummary, filter: CoverageFilter): boolean {
|
|
124
|
+
if (result.error) return false;
|
|
125
|
+
const cs = result.coverageState.toLowerCase();
|
|
126
|
+
const pf = result.pageFetchState.toLowerCase();
|
|
127
|
+
switch (filter) {
|
|
128
|
+
case "not_indexed":
|
|
129
|
+
return result.verdict !== "PASS";
|
|
130
|
+
case "server_error":
|
|
131
|
+
return cs.includes("server error") || pf === "server_error";
|
|
132
|
+
case "redirect":
|
|
133
|
+
return cs.includes("redirect") || pf.includes("redirect");
|
|
134
|
+
case "soft_404":
|
|
135
|
+
return cs.includes("soft 404") || pf === "soft_404";
|
|
136
|
+
case "blocked":
|
|
137
|
+
return cs.includes("blocked");
|
|
138
|
+
case "duplicate":
|
|
139
|
+
return cs.includes("duplicate");
|
|
140
|
+
case "discovered":
|
|
141
|
+
return cs.includes("discovered");
|
|
142
|
+
case "crawled_not_indexed":
|
|
143
|
+
return cs.includes("crawled - currently not indexed");
|
|
144
|
+
default:
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function filterNameForState(state: string): string | null {
|
|
150
|
+
const s = state.toLowerCase();
|
|
151
|
+
if (s.includes("server error")) return "server_error";
|
|
152
|
+
if (s.includes("blocked")) return "blocked";
|
|
153
|
+
if (s.includes("redirect")) return "redirect";
|
|
154
|
+
if (s.includes("soft 404")) return "soft_404";
|
|
155
|
+
if (s.includes("duplicate")) return "duplicate";
|
|
156
|
+
if (s.includes("discovered - currently not indexed")) return "discovered";
|
|
157
|
+
if (s.includes("crawled - currently not indexed")) return "crawled_not_indexed";
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
108
161
|
function formatSampleResults(
|
|
109
162
|
siteUrl: string,
|
|
110
163
|
sitemapUrl: string,
|
|
@@ -187,6 +240,90 @@ function formatSampleResults(
|
|
|
187
240
|
return lines.join("\n");
|
|
188
241
|
}
|
|
189
242
|
|
|
243
|
+
// ── Coverage formatter ──
|
|
244
|
+
|
|
245
|
+
function formatCoverage(siteUrl: string, sitemapUrl: string, totalUrls: number, results: InspectionSummary[]): string {
|
|
246
|
+
const inspected = results.filter((r) => !r.error);
|
|
247
|
+
const errors = results.filter((r) => r.error);
|
|
248
|
+
const indexed = inspected.filter((r) => r.verdict === "PASS");
|
|
249
|
+
const notIndexed = inspected.filter((r) => r.verdict !== "PASS");
|
|
250
|
+
|
|
251
|
+
const lines: string[] = [
|
|
252
|
+
`=== Index Coverage: ${siteUrl} ===`,
|
|
253
|
+
`Sitemap: ${sitemapUrl} (${totalUrls.toLocaleString()} URLs)`,
|
|
254
|
+
`Inspected: ${results.length}`,
|
|
255
|
+
"",
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const total = inspected.length;
|
|
259
|
+
if (total === 0) {
|
|
260
|
+
lines.push("No URLs could be inspected.");
|
|
261
|
+
if (errors.length > 0) {
|
|
262
|
+
lines.push("");
|
|
263
|
+
for (const e of errors) lines.push(` ${e.url}: ${e.error}`);
|
|
264
|
+
}
|
|
265
|
+
return lines.join("\n");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Group not-indexed by coverageState
|
|
269
|
+
const issueGroups: Record<string, InspectionSummary[]> = {};
|
|
270
|
+
for (const r of notIndexed) {
|
|
271
|
+
const key = r.coverageState;
|
|
272
|
+
if (!issueGroups[key]) issueGroups[key] = [];
|
|
273
|
+
issueGroups[key].push(r);
|
|
274
|
+
}
|
|
275
|
+
const sortedIssues = Object.entries(issueGroups).sort((a, b) => b[1].length - a[1].length);
|
|
276
|
+
|
|
277
|
+
// Breakdown
|
|
278
|
+
lines.push("--- Breakdown ---", "");
|
|
279
|
+
lines.push(`Indexed: ${indexed.length}/${total} (${Math.round((indexed.length / total) * 100)}%)`);
|
|
280
|
+
lines.push(`Not indexed: ${notIndexed.length}/${total} (${Math.round((notIndexed.length / total) * 100)}%)`);
|
|
281
|
+
for (const [state, urls] of sortedIssues) {
|
|
282
|
+
lines.push(` ${state}: ${urls.length}`);
|
|
283
|
+
}
|
|
284
|
+
if (errors.length > 0) {
|
|
285
|
+
lines.push(`Inspection errors: ${errors.length}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Extrapolated estimates
|
|
289
|
+
if (totalUrls > total) {
|
|
290
|
+
lines.push("", "--- Estimated Totals ---", "");
|
|
291
|
+
const indexedEst = Math.round((indexed.length / total) * totalUrls);
|
|
292
|
+
const notIndexedEst = Math.round((notIndexed.length / total) * totalUrls);
|
|
293
|
+
lines.push(`~${indexedEst.toLocaleString()} indexed (of ${totalUrls.toLocaleString()})`);
|
|
294
|
+
lines.push(`~${notIndexedEst.toLocaleString()} not indexed`);
|
|
295
|
+
for (const [state, urls] of sortedIssues) {
|
|
296
|
+
const est = Math.round((urls.length / total) * totalUrls);
|
|
297
|
+
lines.push(` ~${est.toLocaleString()} ${state}`);
|
|
298
|
+
}
|
|
299
|
+
lines.push("", `Estimates based on ${total}-URL sample — actual numbers may vary.`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Example URLs per issue
|
|
303
|
+
if (sortedIssues.length > 0) {
|
|
304
|
+
lines.push("", "--- URLs by Issue ---", "");
|
|
305
|
+
for (const [state, urls] of sortedIssues) {
|
|
306
|
+
lines.push(`${state}:`);
|
|
307
|
+
for (const u of urls.slice(0, 3)) {
|
|
308
|
+
lines.push(` ${u.url}`);
|
|
309
|
+
if (u.lastCrawlTime) lines.push(` Last crawled: ${u.lastCrawlTime}`);
|
|
310
|
+
if (u.pageFetchState !== "SUCCESSFUL") lines.push(` Fetch: ${humanizeState(u.pageFetchState)}`);
|
|
311
|
+
}
|
|
312
|
+
if (urls.length > 3) lines.push(` ... and ${urls.length - 3} more in sample`);
|
|
313
|
+
lines.push("");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Suggest drill-down
|
|
318
|
+
const filterSuggestions = sortedIssues.map(([state]) => filterNameForState(state)).filter(Boolean);
|
|
319
|
+
if (filterSuggestions.length > 0) {
|
|
320
|
+
lines.push(`Drill deeper: search(action='sample', filter='...') to investigate specific issues.`);
|
|
321
|
+
lines.push(` Available filters: ${filterSuggestions.join(", ")}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
190
327
|
// ── Sitemaps formatters ──
|
|
191
328
|
|
|
192
329
|
function formatSites(sites: GscSite[]): string {
|
|
@@ -559,20 +696,48 @@ function formatGapAnalysis(
|
|
|
559
696
|
export function registerSearchTool(server: McpServer): void {
|
|
560
697
|
server.tool(
|
|
561
698
|
"search",
|
|
562
|
-
"Query Google Search Console. Inspect URL indexing
|
|
699
|
+
"Query Google Search Console. Inspect URL indexing, get coverage breakdown by issue type, sample-inspect with filters, list properties and sitemaps, analyze search traffic, or find keyword gaps.",
|
|
563
700
|
{
|
|
564
701
|
action: z
|
|
565
|
-
.enum([
|
|
702
|
+
.enum([
|
|
703
|
+
"inspect",
|
|
704
|
+
"sample",
|
|
705
|
+
"coverage",
|
|
706
|
+
"sitemaps",
|
|
707
|
+
"analytics",
|
|
708
|
+
"gaps",
|
|
709
|
+
"list_sites",
|
|
710
|
+
"get_site",
|
|
711
|
+
"get_sitemap",
|
|
712
|
+
])
|
|
566
713
|
.optional()
|
|
567
714
|
.describe("Action to perform. Auto-detected from params when unambiguous."),
|
|
568
715
|
site_url: z.string().optional().describe("GSC property (e.g., 'sc-domain:example.com')."),
|
|
569
716
|
url: z.string().url().optional().describe("URL to inspect in Google's index."),
|
|
570
|
-
sitemap_url: z.string().url().optional().describe("Sitemap URL for sample inspection or get_sitemap."),
|
|
571
|
-
sample_size: z
|
|
717
|
+
sitemap_url: z.string().url().optional().describe("Sitemap URL for sample/coverage inspection or get_sitemap."),
|
|
718
|
+
sample_size: z
|
|
719
|
+
.number()
|
|
720
|
+
.min(1)
|
|
721
|
+
.max(50)
|
|
722
|
+
.optional()
|
|
723
|
+
.describe("URLs to inspect (1-50). Default: 5 for sample, 20 for coverage."),
|
|
572
724
|
sample_strategy: z
|
|
573
725
|
.enum(["random", "first", "spread"])
|
|
574
726
|
.optional()
|
|
575
|
-
.describe("Sampling strategy. Default: 'random'."),
|
|
727
|
+
.describe("Sampling strategy. Default: 'spread' for coverage, 'random' for sample."),
|
|
728
|
+
filter: z
|
|
729
|
+
.enum([
|
|
730
|
+
"not_indexed",
|
|
731
|
+
"server_error",
|
|
732
|
+
"redirect",
|
|
733
|
+
"soft_404",
|
|
734
|
+
"blocked",
|
|
735
|
+
"duplicate",
|
|
736
|
+
"discovered",
|
|
737
|
+
"crawled_not_indexed",
|
|
738
|
+
])
|
|
739
|
+
.optional()
|
|
740
|
+
.describe("Filter sample results by coverage issue type. Inspects more URLs internally to find matches."),
|
|
576
741
|
start_date: z.string().optional().describe("Start date (YYYY-MM-DD) for analytics. Default: 28 days ago."),
|
|
577
742
|
end_date: z.string().optional().describe("End date (YYYY-MM-DD) for analytics. Default: 3 days ago."),
|
|
578
743
|
dimensions: z
|
|
@@ -609,6 +774,7 @@ export function registerSearchTool(server: McpServer): void {
|
|
|
609
774
|
sitemap_url,
|
|
610
775
|
sample_size,
|
|
611
776
|
sample_strategy,
|
|
777
|
+
filter,
|
|
612
778
|
start_date,
|
|
613
779
|
end_date,
|
|
614
780
|
dimensions,
|
|
@@ -628,7 +794,7 @@ export function registerSearchTool(server: McpServer): void {
|
|
|
628
794
|
params.action ??
|
|
629
795
|
(url && site_url && !hasAnalyticsParams
|
|
630
796
|
? "inspect"
|
|
631
|
-
: sample_size
|
|
797
|
+
: sample_size || filter
|
|
632
798
|
? "sample"
|
|
633
799
|
: hasAnalyticsParams
|
|
634
800
|
? "analytics"
|
|
@@ -683,7 +849,6 @@ export function registerSearchTool(server: McpServer): void {
|
|
|
683
849
|
if (sitemaps.length === 0) {
|
|
684
850
|
return textResult(`No sitemaps found for ${site_url} in GSC. Provide a sitemap_url directly.`);
|
|
685
851
|
}
|
|
686
|
-
// Pick the first non-index sitemap, or the first one
|
|
687
852
|
const nonIndex = sitemaps.find((s) => !s.isSitemapsIndex);
|
|
688
853
|
resolvedSitemapUrl = (nonIndex ?? sitemaps[0]).path;
|
|
689
854
|
}
|
|
@@ -691,7 +856,6 @@ export function registerSearchTool(server: McpServer): void {
|
|
|
691
856
|
// Fetch and parse sitemap
|
|
692
857
|
let parsed = await fetchSitemap(resolvedSitemapUrl);
|
|
693
858
|
|
|
694
|
-
// If it's a sitemap index, fetch the first child
|
|
695
859
|
if (parsed.isSitemapIndex && parsed.childSitemaps.length > 0) {
|
|
696
860
|
const childUrl = parsed.childSitemaps[0];
|
|
697
861
|
parsed = await fetchSitemap(childUrl);
|
|
@@ -702,10 +866,72 @@ export function registerSearchTool(server: McpServer): void {
|
|
|
702
866
|
return textResult(`Sitemap ${resolvedSitemapUrl} contains no URLs.`);
|
|
703
867
|
}
|
|
704
868
|
|
|
705
|
-
|
|
869
|
+
if (filter) {
|
|
870
|
+
// Filtered sample: inspect more URLs, return only matches
|
|
871
|
+
const scanLimit = Math.min(count * 5, 50, parsed.urls.length);
|
|
872
|
+
const pool = sampleUrls(parsed.urls, scanLimit, strategy);
|
|
873
|
+
const matched: InspectionSummary[] = [];
|
|
874
|
+
let scanned = 0;
|
|
875
|
+
|
|
876
|
+
for (const u of pool) {
|
|
877
|
+
const result = await inspectSingle(u, site_url);
|
|
878
|
+
scanned++;
|
|
879
|
+
if (matchesCoverageFilter(result, filter)) {
|
|
880
|
+
matched.push(result);
|
|
881
|
+
if (matched.length >= count) break;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (matched.length === 0) {
|
|
886
|
+
return textResult(
|
|
887
|
+
[
|
|
888
|
+
`=== Filtered Sample: ${site_url} ===`,
|
|
889
|
+
`Sitemap: ${resolvedSitemapUrl} (${parsed.urls.length.toLocaleString()} URLs)`,
|
|
890
|
+
`Filter: ${filter}`,
|
|
891
|
+
`Scanned: ${scanned} | Matched: 0`,
|
|
892
|
+
"",
|
|
893
|
+
`No URLs matching "${filter}" found in ${scanned} inspected URLs.`,
|
|
894
|
+
"Try a larger sample_size or a different filter.",
|
|
895
|
+
].join("\n"),
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const lines: string[] = [
|
|
900
|
+
`=== Filtered Sample: ${site_url} ===`,
|
|
901
|
+
`Sitemap: ${resolvedSitemapUrl} (${parsed.urls.length.toLocaleString()} URLs)`,
|
|
902
|
+
`Filter: ${filter}`,
|
|
903
|
+
`Scanned: ${scanned} | Matched: ${matched.length}`,
|
|
904
|
+
"",
|
|
905
|
+
"--- Matching URLs ---",
|
|
906
|
+
"",
|
|
907
|
+
];
|
|
908
|
+
|
|
909
|
+
for (let i = 0; i < matched.length; i++) {
|
|
910
|
+
const r = matched[i];
|
|
911
|
+
lines.push(`${i + 1}. ${r.url}`);
|
|
912
|
+
lines.push(` Coverage: ${r.coverageState}`);
|
|
913
|
+
lines.push(` Page fetch: ${humanizeState(r.pageFetchState)}`);
|
|
914
|
+
if (r.robotsTxtState !== "ALLOWED") lines.push(` Robots.txt: ${humanizeState(r.robotsTxtState)}`);
|
|
915
|
+
if (r.indexingState !== "INDEXING_ALLOWED") lines.push(` Indexing: ${humanizeState(r.indexingState)}`);
|
|
916
|
+
if (r.lastCrawlTime) lines.push(` Last crawled: ${r.lastCrawlTime}`);
|
|
917
|
+
if (r.googleCanonical && r.googleCanonical !== r.url) {
|
|
918
|
+
lines.push(` Google canonical: ${r.googleCanonical}`);
|
|
919
|
+
}
|
|
920
|
+
lines.push("");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (matched.length < count && scanned >= scanLimit) {
|
|
924
|
+
lines.push(
|
|
925
|
+
`Found ${matched.length}/${count} requested — scanned ${scanned} URLs (limit ${scanLimit}). Increase sample_size for a wider scan.`,
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return textResult(lines.join("\n"));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Unfiltered sample (original behavior)
|
|
706
933
|
const sampled = sampleUrls(parsed.urls, count, strategy);
|
|
707
934
|
|
|
708
|
-
// Inspect each URL sequentially (API rate limits)
|
|
709
935
|
const results: InspectionSummary[] = [];
|
|
710
936
|
for (const u of sampled) {
|
|
711
937
|
results.push(await inspectSingle(u, site_url));
|
|
@@ -714,6 +940,46 @@ export function registerSearchTool(server: McpServer): void {
|
|
|
714
940
|
return textResult(formatSampleResults(site_url, resolvedSitemapUrl, parsed.urls.length, results));
|
|
715
941
|
}
|
|
716
942
|
|
|
943
|
+
// ── coverage ──
|
|
944
|
+
if (resolvedAction === "coverage") {
|
|
945
|
+
if (!site_url) return textResult("Error: site_url is required for coverage.");
|
|
946
|
+
|
|
947
|
+
const count = sample_size ?? 20;
|
|
948
|
+
const strategy = sample_strategy ?? "spread";
|
|
949
|
+
|
|
950
|
+
// Discover sitemap URL if not provided
|
|
951
|
+
let resolvedSitemapUrl = sitemap_url;
|
|
952
|
+
if (!resolvedSitemapUrl) {
|
|
953
|
+
const sitemaps = await listSitemaps(site_url);
|
|
954
|
+
if (sitemaps.length === 0) {
|
|
955
|
+
return textResult(`No sitemaps found for ${site_url} in GSC. Provide a sitemap_url directly.`);
|
|
956
|
+
}
|
|
957
|
+
const nonIndex = sitemaps.find((s) => !s.isSitemapsIndex);
|
|
958
|
+
resolvedSitemapUrl = (nonIndex ?? sitemaps[0]).path;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
let parsed = await fetchSitemap(resolvedSitemapUrl);
|
|
962
|
+
|
|
963
|
+
if (parsed.isSitemapIndex && parsed.childSitemaps.length > 0) {
|
|
964
|
+
const childUrl = parsed.childSitemaps[0];
|
|
965
|
+
parsed = await fetchSitemap(childUrl);
|
|
966
|
+
resolvedSitemapUrl = `${resolvedSitemapUrl} → ${childUrl}`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (parsed.urls.length === 0) {
|
|
970
|
+
return textResult(`Sitemap ${resolvedSitemapUrl} contains no URLs.`);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const sampled = sampleUrls(parsed.urls, count, strategy);
|
|
974
|
+
|
|
975
|
+
const results: InspectionSummary[] = [];
|
|
976
|
+
for (const u of sampled) {
|
|
977
|
+
results.push(await inspectSingle(u, site_url));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return textResult(formatCoverage(site_url, resolvedSitemapUrl, parsed.urls.length, results));
|
|
981
|
+
}
|
|
982
|
+
|
|
717
983
|
// ── list_sites ──
|
|
718
984
|
if (resolvedAction === "list_sites") {
|
|
719
985
|
const sites = await listSites();
|
package/src/tools/setup.ts
CHANGED
|
@@ -17,7 +17,33 @@ export function registerSetupTool(server: McpServer): void {
|
|
|
17
17
|
async ({ action, client_id, client_secret, code }) => {
|
|
18
18
|
if (action === "status") {
|
|
19
19
|
const method = getAuthMethod();
|
|
20
|
-
if (method
|
|
20
|
+
if (method !== "none") {
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: `=== Pagesight Auth Status ===\n\nStatus: CONFIGURED\nMethod: ${method}`,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for partial OAuth config
|
|
32
|
+
const hasClientId = !!process.env.GSC_CLIENT_ID;
|
|
33
|
+
const hasClientSecret = !!process.env.GSC_CLIENT_SECRET;
|
|
34
|
+
const hasPartial = hasClientId || hasClientSecret;
|
|
35
|
+
|
|
36
|
+
if (hasPartial) {
|
|
37
|
+
const present = [hasClientId && "GSC_CLIENT_ID", hasClientSecret && "GSC_CLIENT_SECRET"]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(", ");
|
|
40
|
+
const missing = [
|
|
41
|
+
!hasClientId && "GSC_CLIENT_ID",
|
|
42
|
+
!hasClientSecret && "GSC_CLIENT_SECRET",
|
|
43
|
+
"GSC_REFRESH_TOKEN",
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(", ");
|
|
21
47
|
return {
|
|
22
48
|
content: [
|
|
23
49
|
{
|
|
@@ -25,34 +51,47 @@ export function registerSetupTool(server: McpServer): void {
|
|
|
25
51
|
text: [
|
|
26
52
|
"=== Pagesight Auth Status ===",
|
|
27
53
|
"",
|
|
28
|
-
"Status:
|
|
54
|
+
"Status: PARTIAL",
|
|
55
|
+
` Present: ${present}`,
|
|
56
|
+
` Missing: ${missing}`,
|
|
29
57
|
"",
|
|
30
|
-
"
|
|
31
|
-
"",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
" 3. Create OAuth 2.0 credentials (Desktop app)",
|
|
36
|
-
" 4. Call: setup(action='get_auth_url', client_id='YOUR_ID')",
|
|
37
|
-
" 5. Visit the URL, authorize, copy the code",
|
|
38
|
-
" 6. Call: setup(action='exchange_code', client_id='YOUR_ID', client_secret='YOUR_SECRET', code='THE_CODE')",
|
|
39
|
-
" 7. Set env vars: GSC_CLIENT_ID, GSC_CLIENT_SECRET, GSC_REFRESH_TOKEN",
|
|
40
|
-
"",
|
|
41
|
-
"Option 2: Service Account",
|
|
42
|
-
" 1. Create a service account in Google Cloud",
|
|
43
|
-
" 2. Download the JSON key file",
|
|
44
|
-
" 3. Add the service account email as a user in Search Console",
|
|
45
|
-
" 4. Set env var: GSC_SERVICE_ACCOUNT_KEY=/path/to/key.json",
|
|
58
|
+
"Complete OAuth setup:",
|
|
59
|
+
" 1. Call: setup(action='get_auth_url', client_id='YOUR_ID')",
|
|
60
|
+
" 2. Visit the URL, authorize, copy the code",
|
|
61
|
+
" 3. Call: setup(action='exchange_code', client_id='YOUR_ID', client_secret='YOUR_SECRET', code='THE_CODE')",
|
|
62
|
+
" 4. Set GSC_REFRESH_TOKEN from the response",
|
|
46
63
|
].join("\n"),
|
|
47
64
|
},
|
|
48
65
|
],
|
|
49
66
|
};
|
|
50
67
|
}
|
|
68
|
+
|
|
51
69
|
return {
|
|
52
70
|
content: [
|
|
53
71
|
{
|
|
54
72
|
type: "text",
|
|
55
|
-
text:
|
|
73
|
+
text: [
|
|
74
|
+
"=== Pagesight Auth Status ===",
|
|
75
|
+
"",
|
|
76
|
+
"Status: NOT CONFIGURED",
|
|
77
|
+
"",
|
|
78
|
+
"To use Pagesight, configure one of:",
|
|
79
|
+
"",
|
|
80
|
+
"Option 1: OAuth 2.0 (recommended for personal use)",
|
|
81
|
+
" 1. Create a Google Cloud project",
|
|
82
|
+
" 2. Enable 'Google Search Console API'",
|
|
83
|
+
" 3. Create OAuth 2.0 credentials (Desktop app)",
|
|
84
|
+
" 4. Call: setup(action='get_auth_url', client_id='YOUR_ID')",
|
|
85
|
+
" 5. Visit the URL, authorize, copy the code",
|
|
86
|
+
" 6. Call: setup(action='exchange_code', client_id='YOUR_ID', client_secret='YOUR_SECRET', code='THE_CODE')",
|
|
87
|
+
" 7. Set env vars: GSC_CLIENT_ID, GSC_CLIENT_SECRET, GSC_REFRESH_TOKEN",
|
|
88
|
+
"",
|
|
89
|
+
"Option 2: Service Account",
|
|
90
|
+
" 1. Create a service account in Google Cloud",
|
|
91
|
+
" 2. Download the JSON key file",
|
|
92
|
+
" 3. Add the service account email as a user in Search Console",
|
|
93
|
+
" 4. Set env var: GSC_SERVICE_ACCOUNT_KEY=/path/to/key.json",
|
|
94
|
+
].join("\n"),
|
|
56
95
|
},
|
|
57
96
|
],
|
|
58
97
|
};
|