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.
- package/README.md +24 -7
- package/build/shared/src/exam-result-store.js +2 -1
- package/build/shared/src/pulsemcp-admin-client/lib/get-moz-backlinks.js +39 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-moz-metrics.js +36 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-moz-stored-metrics.js +39 -0
- package/build/shared/src/pulsemcp-admin-client/lib/upload-image.js +1 -1
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +26 -0
- package/build/shared/src/server.js +13 -0
- package/build/shared/src/tools/get-exam-result.js +9 -2
- package/build/shared/src/tools/get-moz-backlinks.js +102 -0
- package/build/shared/src/tools/get-moz-metrics.js +95 -0
- package/build/shared/src/tools/get-moz-stored-metrics.js +119 -0
- package/build/shared/src/tools/run-exam-for-mirror.js +7 -5
- package/build/shared/src/tools.js +13 -0
- package/package.json +1 -1
- package/shared/exam-result-store.js +2 -1
- package/shared/pulsemcp-admin-client/lib/get-moz-backlinks.d.ts +7 -0
- package/shared/pulsemcp-admin-client/lib/get-moz-backlinks.js +39 -0
- package/shared/pulsemcp-admin-client/lib/get-moz-metrics.d.ts +6 -0
- package/shared/pulsemcp-admin-client/lib/get-moz-metrics.js +36 -0
- package/shared/pulsemcp-admin-client/lib/get-moz-stored-metrics.d.ts +8 -0
- package/shared/pulsemcp-admin-client/lib/get-moz-stored-metrics.js +39 -0
- package/shared/pulsemcp-admin-client/lib/upload-image.js +1 -1
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +26 -0
- package/shared/server.d.ts +31 -1
- package/shared/server.js +13 -0
- package/shared/tools/get-exam-result.js +9 -2
- package/shared/tools/get-moz-backlinks.d.ts +41 -0
- package/shared/tools/get-moz-backlinks.js +102 -0
- package/shared/tools/get-moz-metrics.d.ts +35 -0
- package/shared/tools/get-moz-metrics.js +95 -0
- package/shared/tools/get-moz-stored-metrics.d.ts +45 -0
- package/shared/tools/get-moz-stored-metrics.js +119 -0
- package/shared/tools/run-exam-for-mirror.js +7 -5
- package/shared/tools.d.ts +4 -1
- package/shared/tools.js +13 -0
- 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` |
|
|
131
|
-
| `proctor_readonly` |
|
|
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
|
-
|
|
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' });
|
|
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) =>
|
|
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
|
|
137
|
-
content += ` Passed: ${line.passed
|
|
138
|
-
content += ` Failed: ${line.failed
|
|
139
|
-
content += ` Skipped: ${line.skipped
|
|
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;
|