pulsemcp-cms-admin-mcp-server 0.9.10 → 0.9.12

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.
Files changed (37) hide show
  1. package/README.md +24 -7
  2. package/build/shared/src/exam-result-store.js +2 -1
  3. package/build/shared/src/pulsemcp-admin-client/lib/get-moz-backlinks.js +39 -0
  4. package/build/shared/src/pulsemcp-admin-client/lib/get-moz-metrics.js +36 -0
  5. package/build/shared/src/pulsemcp-admin-client/lib/get-moz-stored-metrics.js +39 -0
  6. package/build/shared/src/pulsemcp-admin-client/lib/upload-image.js +1 -1
  7. package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +26 -0
  8. package/build/shared/src/server.js +13 -0
  9. package/build/shared/src/tools/get-exam-result.js +9 -2
  10. package/build/shared/src/tools/get-moz-backlinks.js +102 -0
  11. package/build/shared/src/tools/get-moz-metrics.js +95 -0
  12. package/build/shared/src/tools/get-moz-stored-metrics.js +119 -0
  13. package/build/shared/src/tools/run-exam-for-mirror.js +7 -5
  14. package/build/shared/src/tools.js +13 -0
  15. package/package.json +1 -1
  16. package/shared/exam-result-store.js +2 -1
  17. package/shared/pulsemcp-admin-client/lib/get-moz-backlinks.d.ts +7 -0
  18. package/shared/pulsemcp-admin-client/lib/get-moz-backlinks.js +39 -0
  19. package/shared/pulsemcp-admin-client/lib/get-moz-metrics.d.ts +6 -0
  20. package/shared/pulsemcp-admin-client/lib/get-moz-metrics.js +36 -0
  21. package/shared/pulsemcp-admin-client/lib/get-moz-stored-metrics.d.ts +8 -0
  22. package/shared/pulsemcp-admin-client/lib/get-moz-stored-metrics.js +39 -0
  23. package/shared/pulsemcp-admin-client/lib/upload-image.js +1 -1
  24. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +26 -0
  25. package/shared/server.d.ts +31 -1
  26. package/shared/server.js +13 -0
  27. package/shared/tools/get-exam-result.js +9 -2
  28. package/shared/tools/get-moz-backlinks.d.ts +41 -0
  29. package/shared/tools/get-moz-backlinks.js +102 -0
  30. package/shared/tools/get-moz-metrics.d.ts +35 -0
  31. package/shared/tools/get-moz-metrics.js +95 -0
  32. package/shared/tools/get-moz-stored-metrics.d.ts +45 -0
  33. package/shared/tools/get-moz-stored-metrics.js +119 -0
  34. package/shared/tools/run-exam-for-mirror.js +7 -5
  35. package/shared/tools.d.ts +4 -1
  36. package/shared/tools.js +13 -0
  37. package/shared/types.d.ts +45 -0
package/README.md CHANGED
@@ -95,6 +95,14 @@ This server is built and tested on macOS with Claude Desktop. It should work wit
95
95
  | `run_exam_for_mirror` | proctor | write | Run proctor exams against unofficial mirrors via Fly Machines. Returns truncated summary with `result_id`. |
96
96
  | `get_exam_result` | proctor | read | Retrieve full untruncated exam results by `result_id`, with optional section/mirror filtering. |
97
97
  | `save_results_for_mirror` | proctor | write | Save proctor exam results via `result_id` from `run_exam_for_mirror`. |
98
+ | `list_proctor_runs` | proctor | read | List proctor runs with filtering by name, recommended status, and tenant IDs. |
99
+ | `get_proctor_metadata` | proctor | read | Get available proctor runtimes and exam types. |
100
+ | `list_discovered_urls` | discovered_urls | read | List discovered URLs with status filtering and pagination. |
101
+ | `mark_discovered_url_processed` | discovered_urls | write | Mark a discovered URL as processed with a result status. |
102
+ | `get_discovered_url_stats` | discovered_urls | read | Get summary statistics for discovered URLs pipeline. |
103
+ | `get_moz_metrics` | moz | read | Fetch live URL metrics from the MOZ API (page authority, domain authority, spam score, link counts). |
104
+ | `get_moz_backlinks` | moz | read | Fetch live backlink data from the MOZ API (source pages, anchor text, domain authority). |
105
+ | `get_moz_stored_metrics` | moz | read | List stored/historical MOZ data for a server's canonicals with pagination. |
98
106
 
99
107
  # Tool Groups
100
108
 
@@ -127,8 +135,12 @@ This server organizes tools into groups that can be selectively enabled or disab
127
135
  | `redirects_readonly` | 2 | URL redirects read-only (list, get) |
128
136
  | `good_jobs` | 10 | Full GoodJob background job management (read + write) |
129
137
  | `good_jobs_readonly` | 5 | GoodJob read-only (list, get, stats, processes, cron) |
130
- | `proctor` | 3 | Proctor exam execution, result retrieval, and result storage (read + write) |
131
- | `proctor_readonly` | 1 | Proctor results read-only (`get_exam_result`) |
138
+ | `proctor` | 5 | Proctor exam execution, result retrieval, and result storage (read + write) |
139
+ | `proctor_readonly` | 3 | Proctor results read-only (`get_exam_result`, `list_proctor_runs`, `get_proctor_metadata`) |
140
+ | `discovered_urls` | 3 | Discovered URL management (read + write) |
141
+ | `discovered_urls_readonly` | 2 | Discovered URL tools read-only (list, stats) |
142
+ | `moz` | 3 | MOZ SEO metrics — live URL metrics, backlinks, and stored historical data (all read-only) |
143
+ | `moz_readonly` | 3 | MOZ tools read-only (alias — all MOZ tools are read-only) |
132
144
 
133
145
  ### Tools by Group
134
146
 
@@ -161,14 +173,19 @@ This server organizes tools into groups that can be selectively enabled or disab
161
173
  - Read-only: `list_good_jobs`, `get_good_job`, `list_good_job_cron_schedules`, `list_good_job_processes`, `get_good_job_queue_statistics`
162
174
  - Write: `retry_good_job`, `discard_good_job`, `reschedule_good_job`, `force_trigger_good_job_cron`, `cleanup_good_jobs`
163
175
  - **proctor** / **proctor_readonly**:
164
- - Read-only: `get_exam_result`
176
+ - Read-only: `get_exam_result`, `list_proctor_runs`, `get_proctor_metadata`
165
177
  - Write: `run_exam_for_mirror`, `save_results_for_mirror`
178
+ - **discovered_urls** / **discovered_urls_readonly**:
179
+ - Read-only: `list_discovered_urls`, `get_discovered_url_stats`
180
+ - Write: `mark_discovered_url_processed`
181
+ - **moz** / **moz_readonly**:
182
+ - Read-only: `get_moz_metrics`, `get_moz_backlinks`, `get_moz_stored_metrics`
166
183
 
167
184
  ## Environment Variables
168
185
 
169
- | Variable | Description | Default |
170
- | ------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
171
- | `TOOL_GROUPS` | Comma-separated list of enabled tool groups | `newsletter,server_directory,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers,redirects,good_jobs,proctor` (all base groups) |
186
+ | Variable | Description | Default |
187
+ | ------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
188
+ | `TOOL_GROUPS` | Comma-separated list of enabled tool groups | `newsletter,server_directory,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers,redirects,good_jobs,proctor,discovered_urls,moz` (all base groups) |
172
189
 
173
190
  ## Examples
174
191
 
@@ -193,7 +210,7 @@ TOOL_GROUPS=server_directory_readonly
193
210
  Enable all groups with read-only access:
194
211
 
195
212
  ```bash
196
- TOOL_GROUPS=newsletter_readonly,server_directory_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly,redirects_readonly,good_jobs_readonly,proctor_readonly
213
+ TOOL_GROUPS=newsletter_readonly,server_directory_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly,redirects_readonly,good_jobs_readonly,proctor_readonly,discovered_urls_readonly,moz_readonly
197
214
  ```
198
215
 
199
216
  Note: `proctor_readonly` includes only `get_exam_result` for retrieving stored results. `notifications` has no readonly variant since sending emails is always a write operation.
@@ -21,7 +21,8 @@ export function extractExamId(line) {
21
21
  */
22
22
  export function extractStatus(line) {
23
23
  const data = line.data;
24
- return data?.status || line.status || 'unknown';
24
+ const result = data?.result;
25
+ return (result?.status || data?.status || line.status || 'unknown');
25
26
  }
26
27
  /**
27
28
  * Maximum number of results to keep on disk. Oldest results are evicted
@@ -0,0 +1,39 @@
1
+ export async function getMozBacklinks(apiKey, baseUrl, params) {
2
+ const apiUrl = new URL('/api/moz/backlinks', baseUrl);
3
+ apiUrl.searchParams.append('url', params.url);
4
+ if (params.scope) {
5
+ apiUrl.searchParams.append('scope', params.scope);
6
+ }
7
+ if (params.limit !== undefined) {
8
+ apiUrl.searchParams.append('limit', params.limit.toString());
9
+ }
10
+ const response = await fetch(apiUrl.toString(), {
11
+ method: 'GET',
12
+ headers: {
13
+ 'X-API-Key': apiKey,
14
+ Accept: 'application/json',
15
+ },
16
+ });
17
+ if (!response.ok) {
18
+ if (response.status === 401) {
19
+ throw new Error('Invalid API key');
20
+ }
21
+ if (response.status === 403) {
22
+ throw new Error('User lacks admin privileges');
23
+ }
24
+ if (response.status === 400) {
25
+ const errorData = (await response.json());
26
+ throw new Error(errorData.error || 'Bad request');
27
+ }
28
+ if (response.status === 429) {
29
+ throw new Error('MOZ API rate limit exceeded. Please try again later.');
30
+ }
31
+ if (response.status === 502) {
32
+ const errorData = (await response.json());
33
+ throw new Error(errorData.error || 'MOZ API error');
34
+ }
35
+ throw new Error(`Failed to fetch MOZ backlinks: ${response.status} ${response.statusText}`);
36
+ }
37
+ const data = (await response.json());
38
+ return data.data;
39
+ }
@@ -0,0 +1,36 @@
1
+ export async function getMozMetrics(apiKey, baseUrl, params) {
2
+ const apiUrl = new URL('/api/moz/metrics', baseUrl);
3
+ apiUrl.searchParams.append('url', params.url);
4
+ if (params.scope) {
5
+ apiUrl.searchParams.append('scope', params.scope);
6
+ }
7
+ const response = await fetch(apiUrl.toString(), {
8
+ method: 'GET',
9
+ headers: {
10
+ 'X-API-Key': apiKey,
11
+ Accept: 'application/json',
12
+ },
13
+ });
14
+ if (!response.ok) {
15
+ if (response.status === 401) {
16
+ throw new Error('Invalid API key');
17
+ }
18
+ if (response.status === 403) {
19
+ throw new Error('User lacks admin privileges');
20
+ }
21
+ if (response.status === 400) {
22
+ const errorData = (await response.json());
23
+ throw new Error(errorData.error || 'Bad request');
24
+ }
25
+ if (response.status === 429) {
26
+ throw new Error('MOZ API rate limit exceeded. Please try again later.');
27
+ }
28
+ if (response.status === 502) {
29
+ const errorData = (await response.json());
30
+ throw new Error(errorData.error || 'MOZ API error');
31
+ }
32
+ throw new Error(`Failed to fetch MOZ metrics: ${response.status} ${response.statusText}`);
33
+ }
34
+ const data = (await response.json());
35
+ return data.data;
36
+ }
@@ -0,0 +1,39 @@
1
+ export async function getMozStoredMetrics(apiKey, baseUrl, params) {
2
+ const apiUrl = new URL('/api/moz/stored_metrics', baseUrl);
3
+ apiUrl.searchParams.append('server_id', params.server_id);
4
+ if (params.canonical_id !== undefined) {
5
+ apiUrl.searchParams.append('canonical_id', params.canonical_id.toString());
6
+ }
7
+ if (params.limit !== undefined) {
8
+ apiUrl.searchParams.append('limit', params.limit.toString());
9
+ }
10
+ if (params.offset !== undefined) {
11
+ apiUrl.searchParams.append('offset', params.offset.toString());
12
+ }
13
+ const response = await fetch(apiUrl.toString(), {
14
+ method: 'GET',
15
+ headers: {
16
+ 'X-API-Key': apiKey,
17
+ Accept: 'application/json',
18
+ },
19
+ });
20
+ if (!response.ok) {
21
+ if (response.status === 401) {
22
+ throw new Error('Invalid API key');
23
+ }
24
+ if (response.status === 403) {
25
+ throw new Error('User lacks admin privileges');
26
+ }
27
+ if (response.status === 400) {
28
+ const errorData = (await response.json());
29
+ throw new Error(errorData.error || 'Bad request');
30
+ }
31
+ if (response.status === 404) {
32
+ const errorData = (await response.json());
33
+ throw new Error(errorData.error || 'Server not found');
34
+ }
35
+ throw new Error(`Failed to fetch MOZ stored metrics: ${response.status} ${response.statusText}`);
36
+ }
37
+ const data = (await response.json());
38
+ return data;
39
+ }
@@ -3,7 +3,7 @@ export async function uploadImage(apiKey, baseUrl, postSlug, fileName, fileData)
3
3
  // Create form data for multipart upload
4
4
  const formData = new FormData();
5
5
  // Create a blob from the buffer
6
- const blob = new Blob([fileData], { type: 'image/png' }); // Default to PNG, adjust as needed
6
+ const blob = new Blob([new Uint8Array(fileData)], { type: 'image/png' });
7
7
  // Add file to form data
8
8
  formData.append('file', blob, fileName);
9
9
  // Add folder and filepath as documented in the API
@@ -997,5 +997,31 @@ export function createMockPulseMCPAdminClient(mockData) {
997
997
  errored_today: 0,
998
998
  };
999
999
  },
1000
+ async getMozMetrics() {
1001
+ return {
1002
+ metrics: { page_authority: 50, domain_authority: 60 },
1003
+ raw_response: {},
1004
+ processed_at: new Date().toISOString(),
1005
+ };
1006
+ },
1007
+ async getMozBacklinks() {
1008
+ return {
1009
+ backlinks: [],
1010
+ raw_response: {},
1011
+ processed_at: new Date().toISOString(),
1012
+ };
1013
+ },
1014
+ async getMozStoredMetrics() {
1015
+ return {
1016
+ data: [],
1017
+ meta: {
1018
+ current_page: 1,
1019
+ total_pages: 0,
1020
+ total_count: 0,
1021
+ has_next: false,
1022
+ limit: 30,
1023
+ },
1024
+ };
1025
+ },
1000
1026
  };
1001
1027
  }
@@ -68,6 +68,9 @@ import { getProctorMetadata } from './pulsemcp-admin-client/lib/get-proctor-meta
68
68
  import { getDiscoveredUrls } from './pulsemcp-admin-client/lib/get-discovered-urls.js';
69
69
  import { markDiscoveredUrlProcessed } from './pulsemcp-admin-client/lib/mark-discovered-url-processed.js';
70
70
  import { getDiscoveredUrlStats } from './pulsemcp-admin-client/lib/get-discovered-url-stats.js';
71
+ import { getMozMetrics } from './pulsemcp-admin-client/lib/get-moz-metrics.js';
72
+ import { getMozBacklinks } from './pulsemcp-admin-client/lib/get-moz-backlinks.js';
73
+ import { getMozStoredMetrics } from './pulsemcp-admin-client/lib/get-moz-stored-metrics.js';
71
74
  // PulseMCP Admin API client implementation
72
75
  export class PulseMCPAdminClient {
73
76
  apiKey;
@@ -284,6 +287,16 @@ export class PulseMCPAdminClient {
284
287
  async getDiscoveredUrlStats() {
285
288
  return getDiscoveredUrlStats(this.apiKey, this.baseUrl);
286
289
  }
290
+ // MOZ REST API methods
291
+ async getMozMetrics(params) {
292
+ return getMozMetrics(this.apiKey, this.baseUrl, params);
293
+ }
294
+ async getMozBacklinks(params) {
295
+ return getMozBacklinks(this.apiKey, this.baseUrl, params);
296
+ }
297
+ async getMozStoredMetrics(params) {
298
+ return getMozStoredMetrics(this.apiKey, this.baseUrl, params);
299
+ }
287
300
  }
288
301
  export function createMCPServer(options) {
289
302
  const server = new Server({
@@ -67,9 +67,16 @@ Typical usage:
67
67
  const targetType = typeMap[validatedArgs.section];
68
68
  lines = lines.filter((line) => line.type === targetType);
69
69
  }
70
- // Filter by mirror_id
70
+ // Filter by mirror_id — check both top-level and data.mirror_id
71
+ // since the backend nests mirror_id inside the data payload.
71
72
  if (validatedArgs.mirror_id !== undefined) {
72
- lines = lines.filter((line) => line.type !== 'exam_result' || line.mirror_id === validatedArgs.mirror_id);
73
+ lines = lines.filter((line) => {
74
+ if (line.type !== 'exam_result')
75
+ return true;
76
+ const data = line.data;
77
+ const mirrorId = line.mirror_id ?? data?.mirror_id;
78
+ return mirrorId === validatedArgs.mirror_id;
79
+ });
73
80
  }
74
81
  let content = `**Exam Result Details**\n\n`;
75
82
  content += `Result ID: ${stored.result_id}\n`;
@@ -0,0 +1,102 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ url: 'The URL to fetch backlinks for. Must be a valid HTTP/HTTPS URL.',
4
+ scope: 'Scope of the backlinks lookup: "url" (exact URL, default), "domain" (entire root domain), or "subdomain" (specific subdomain)',
5
+ limit: 'Number of backlinks to return, 1-50. Default: 1',
6
+ };
7
+ const GetMozBacklinksSchema = z.object({
8
+ url: z.string().describe(PARAM_DESCRIPTIONS.url),
9
+ scope: z.enum(['url', 'domain', 'subdomain']).optional().describe(PARAM_DESCRIPTIONS.scope),
10
+ limit: z.number().min(1).max(50).optional().describe(PARAM_DESCRIPTIONS.limit),
11
+ });
12
+ export function getMozBacklinks(_server, clientFactory) {
13
+ return {
14
+ name: 'get_moz_backlinks',
15
+ description: `Fetch live backlink data from the MOZ API. Returns source pages, anchor text, and domain authority for backlinks pointing to a given URL.
16
+
17
+ Example response:
18
+ {
19
+ "backlinks": [
20
+ {
21
+ "source_page": "https://other.com/blog",
22
+ "anchor_text": "example link",
23
+ "domain_authority": 72
24
+ }
25
+ ],
26
+ "raw_response": { ... },
27
+ "processed_at": "2026-03-15T12:00:00Z"
28
+ }
29
+
30
+ Use cases:
31
+ - Discover which sites link to a given MCP server URL
32
+ - Analyze the quality of backlinks (by domain authority)
33
+ - Research link profiles for SEO analysis`,
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ url: { type: 'string', description: PARAM_DESCRIPTIONS.url },
38
+ scope: {
39
+ type: 'string',
40
+ enum: ['url', 'domain', 'subdomain'],
41
+ description: PARAM_DESCRIPTIONS.scope,
42
+ },
43
+ limit: {
44
+ type: 'number',
45
+ minimum: 1,
46
+ maximum: 50,
47
+ description: PARAM_DESCRIPTIONS.limit,
48
+ },
49
+ },
50
+ required: ['url'],
51
+ },
52
+ handler: async (args) => {
53
+ const validatedArgs = GetMozBacklinksSchema.parse(args);
54
+ const client = clientFactory();
55
+ try {
56
+ const response = await client.getMozBacklinks({
57
+ url: validatedArgs.url,
58
+ scope: validatedArgs.scope,
59
+ limit: validatedArgs.limit,
60
+ });
61
+ let content = `**MOZ Backlinks for ${validatedArgs.url}**`;
62
+ if (validatedArgs.scope) {
63
+ content += ` (scope: ${validatedArgs.scope})`;
64
+ }
65
+ content += '\n\n';
66
+ if (response.backlinks.length === 0) {
67
+ content += 'No backlinks found.\n';
68
+ }
69
+ else {
70
+ content += `Found ${response.backlinks.length} backlink(s):\n\n`;
71
+ for (const [index, backlink] of response.backlinks.entries()) {
72
+ content += `${index + 1}. **${backlink.source_page || 'Unknown source'}**\n`;
73
+ if (backlink.anchor_text)
74
+ content += ` Anchor text: "${backlink.anchor_text}"\n`;
75
+ if (backlink.domain_authority !== undefined)
76
+ content += ` Domain Authority: ${backlink.domain_authority}\n`;
77
+ // Include any additional fields
78
+ const knownKeys = ['source_page', 'anchor_text', 'domain_authority'];
79
+ const extraKeys = Object.keys(backlink).filter((k) => !knownKeys.includes(k));
80
+ for (const key of extraKeys) {
81
+ content += ` ${key}: ${JSON.stringify(backlink[key])}\n`;
82
+ }
83
+ content += '\n';
84
+ }
85
+ }
86
+ content += `**Processed at:** ${response.processed_at}`;
87
+ return { content: [{ type: 'text', text: content.trim() }] };
88
+ }
89
+ catch (error) {
90
+ return {
91
+ content: [
92
+ {
93
+ type: 'text',
94
+ text: `Error fetching MOZ backlinks: ${error instanceof Error ? error.message : String(error)}`,
95
+ },
96
+ ],
97
+ isError: true,
98
+ };
99
+ }
100
+ },
101
+ };
102
+ }
@@ -0,0 +1,95 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ url: 'The URL to fetch MOZ metrics for. Must be a valid HTTP/HTTPS URL.',
4
+ scope: 'Scope of the metrics lookup: "url" (exact URL, default), "domain" (entire root domain), or "subdomain" (specific subdomain)',
5
+ };
6
+ const GetMozMetricsSchema = z.object({
7
+ url: z.string().describe(PARAM_DESCRIPTIONS.url),
8
+ scope: z.enum(['url', 'domain', 'subdomain']).optional().describe(PARAM_DESCRIPTIONS.scope),
9
+ });
10
+ export function getMozMetrics(_server, clientFactory) {
11
+ return {
12
+ name: 'get_moz_metrics',
13
+ description: `Fetch live URL metrics from the MOZ API. Returns page authority, domain authority, spam score, and link counts for a given URL.
14
+
15
+ Example response:
16
+ {
17
+ "metrics": {
18
+ "page_authority": 88,
19
+ "domain_authority": 95,
20
+ "spam_score": 1,
21
+ "root_domains_to_page": 441855
22
+ },
23
+ "raw_response": { ... },
24
+ "processed_at": "2026-03-15T12:00:00Z"
25
+ }
26
+
27
+ Use cases:
28
+ - Check the authority and spam score of a URL
29
+ - Compare domain authority across different MCP server websites
30
+ - Evaluate the SEO strength of a page or domain`,
31
+ inputSchema: {
32
+ type: 'object',
33
+ properties: {
34
+ url: { type: 'string', description: PARAM_DESCRIPTIONS.url },
35
+ scope: {
36
+ type: 'string',
37
+ enum: ['url', 'domain', 'subdomain'],
38
+ description: PARAM_DESCRIPTIONS.scope,
39
+ },
40
+ },
41
+ required: ['url'],
42
+ },
43
+ handler: async (args) => {
44
+ const validatedArgs = GetMozMetricsSchema.parse(args);
45
+ const client = clientFactory();
46
+ try {
47
+ const response = await client.getMozMetrics({
48
+ url: validatedArgs.url,
49
+ scope: validatedArgs.scope,
50
+ });
51
+ let content = `**MOZ Metrics for ${validatedArgs.url}**`;
52
+ if (validatedArgs.scope) {
53
+ content += ` (scope: ${validatedArgs.scope})`;
54
+ }
55
+ content += '\n\n';
56
+ const m = response.metrics;
57
+ if (m.page_authority !== undefined)
58
+ content += `**Page Authority:** ${m.page_authority}\n`;
59
+ if (m.domain_authority !== undefined)
60
+ content += `**Domain Authority:** ${m.domain_authority}\n`;
61
+ if (m.spam_score !== undefined)
62
+ content += `**Spam Score:** ${m.spam_score}\n`;
63
+ if (m.root_domains_to_page !== undefined)
64
+ content += `**Root Domains to Page:** ${m.root_domains_to_page}\n`;
65
+ content += `\n**Processed at:** ${response.processed_at}\n`;
66
+ // Include any additional metrics
67
+ const knownKeys = [
68
+ 'page_authority',
69
+ 'domain_authority',
70
+ 'spam_score',
71
+ 'root_domains_to_page',
72
+ ];
73
+ const extraKeys = Object.keys(m).filter((k) => !knownKeys.includes(k));
74
+ if (extraKeys.length > 0) {
75
+ content += '\n**Additional metrics:**\n';
76
+ for (const key of extraKeys) {
77
+ content += `- ${key}: ${JSON.stringify(m[key])}\n`;
78
+ }
79
+ }
80
+ return { content: [{ type: 'text', text: content.trim() }] };
81
+ }
82
+ catch (error) {
83
+ return {
84
+ content: [
85
+ {
86
+ type: 'text',
87
+ text: `Error fetching MOZ metrics: ${error instanceof Error ? error.message : String(error)}`,
88
+ },
89
+ ],
90
+ isError: true,
91
+ };
92
+ }
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,119 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ server_id: 'MCP server ID (numeric) or slug to fetch stored MOZ data for',
4
+ canonical_id: 'Optional canonical ID to filter results to a specific canonical URL',
5
+ limit: 'Results per page, range 1-100. Default: 30',
6
+ offset: 'Pagination offset. Default: 0',
7
+ };
8
+ const GetMozStoredMetricsSchema = z.object({
9
+ server_id: z.string().describe(PARAM_DESCRIPTIONS.server_id),
10
+ canonical_id: z.number().optional().describe(PARAM_DESCRIPTIONS.canonical_id),
11
+ limit: z.number().min(1).max(100).optional().describe(PARAM_DESCRIPTIONS.limit),
12
+ offset: z.number().min(0).optional().describe(PARAM_DESCRIPTIONS.offset),
13
+ });
14
+ export function getMozStoredMetrics(_server, clientFactory) {
15
+ return {
16
+ name: 'get_moz_stored_metrics',
17
+ description: `List stored/historical MOZ data for a server's canonicals. Returns MOZ metrics that were previously collected and stored, ordered by timestamp descending (newest first).
18
+
19
+ Example response:
20
+ {
21
+ "data": [
22
+ {
23
+ "id": 123,
24
+ "canonical_id": 456,
25
+ "canonical_url": "https://example.com/mcp",
26
+ "scope": "url",
27
+ "timestamp": "2026-03-01T00:00:00Z",
28
+ "triggered_by": "weekly_collection",
29
+ "page_authority": 42,
30
+ "root_domains_to_page": 100,
31
+ "site_metrics": { ... },
32
+ "created_at": "2026-03-01T00:05:00Z"
33
+ }
34
+ ],
35
+ "meta": {
36
+ "current_page": 1,
37
+ "total_pages": 3,
38
+ "total_count": 75,
39
+ "has_next": true,
40
+ "limit": 30
41
+ }
42
+ }
43
+
44
+ Use cases:
45
+ - View historical MOZ metrics for an MCP server over time
46
+ - Track page authority changes for a server's canonical URLs
47
+ - Compare metrics across different canonicals for the same server`,
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ server_id: { type: 'string', description: PARAM_DESCRIPTIONS.server_id },
52
+ canonical_id: { type: 'number', description: PARAM_DESCRIPTIONS.canonical_id },
53
+ limit: {
54
+ type: 'number',
55
+ minimum: 1,
56
+ maximum: 100,
57
+ description: PARAM_DESCRIPTIONS.limit,
58
+ },
59
+ offset: { type: 'number', minimum: 0, description: PARAM_DESCRIPTIONS.offset },
60
+ },
61
+ required: ['server_id'],
62
+ },
63
+ handler: async (args) => {
64
+ const validatedArgs = GetMozStoredMetricsSchema.parse(args);
65
+ const client = clientFactory();
66
+ try {
67
+ const response = await client.getMozStoredMetrics({
68
+ server_id: validatedArgs.server_id,
69
+ canonical_id: validatedArgs.canonical_id,
70
+ limit: validatedArgs.limit,
71
+ offset: validatedArgs.offset,
72
+ });
73
+ let content = `**Stored MOZ Metrics for server "${validatedArgs.server_id}"**`;
74
+ if (validatedArgs.canonical_id !== undefined) {
75
+ content += ` (canonical ID: ${validatedArgs.canonical_id})`;
76
+ }
77
+ content += '\n\n';
78
+ const { meta } = response;
79
+ content += `Page ${meta.current_page} of ${meta.total_pages} (${meta.total_count} total records)\n\n`;
80
+ if (response.data.length === 0) {
81
+ content += 'No stored MOZ data found.\n';
82
+ }
83
+ else {
84
+ for (const [index, record] of response.data.entries()) {
85
+ content += `${index + 1}. **${record.canonical_url}** (ID: ${record.id})\n`;
86
+ content += ` Canonical ID: ${record.canonical_id}`;
87
+ if (record.scope)
88
+ content += ` | Scope: ${record.scope}`;
89
+ content += '\n';
90
+ content += ` Timestamp: ${record.timestamp} | Triggered by: ${record.triggered_by}\n`;
91
+ if (record.page_authority !== undefined)
92
+ content += ` Page Authority: ${record.page_authority}\n`;
93
+ if (record.root_domains_to_page !== undefined)
94
+ content += ` Root Domains to Page: ${record.root_domains_to_page}\n`;
95
+ if (record.site_metrics && Object.keys(record.site_metrics).length > 0) {
96
+ content += ` Site Metrics: ${JSON.stringify(record.site_metrics)}\n`;
97
+ }
98
+ content += '\n';
99
+ }
100
+ }
101
+ if (meta.has_next) {
102
+ content += `_More results available. Use offset=${meta.current_page * meta.limit} to get the next page._`;
103
+ }
104
+ return { content: [{ type: 'text', text: content.trim() }] };
105
+ }
106
+ catch (error) {
107
+ return {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: `Error fetching stored MOZ metrics: ${error instanceof Error ? error.message : String(error)}`,
112
+ },
113
+ ],
114
+ isError: true,
115
+ };
116
+ }
117
+ },
118
+ };
119
+ }
@@ -131,13 +131,15 @@ Use cases:
131
131
  }
132
132
  break;
133
133
  }
134
- case 'summary':
134
+ case 'summary': {
135
+ const summaryData = line.data;
135
136
  content += `\n**Summary**\n`;
136
- content += ` Total: ${line.total || 0}\n`;
137
- content += ` Passed: ${line.passed || 0}\n`;
138
- content += ` Failed: ${line.failed || 0}\n`;
139
- content += ` Skipped: ${line.skipped || 0}\n`;
137
+ content += ` Total: ${summaryData?.total_exams ?? line.total ?? 0}\n`;
138
+ content += ` Passed: ${summaryData?.successful ?? line.passed ?? 0}\n`;
139
+ content += ` Failed: ${summaryData?.failed ?? line.failed ?? 0}\n`;
140
+ content += ` Skipped: ${summaryData?.skipped ?? line.skipped ?? 0}\n`;
140
141
  break;
142
+ }
141
143
  case 'error':
142
144
  content += `\n**Error**: ${line.message || line.error || JSON.stringify(line)}\n`;
143
145
  break;