pulsemcp-cms-admin-mcp-server 0.8.0 → 0.9.2

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 (34) hide show
  1. package/README.md +1 -1
  2. package/build/shared/src/pulsemcp-admin-client/lib/create-mcp-implementation.js +3 -0
  3. package/build/shared/src/pulsemcp-admin-client/lib/get-proctor-runs.js +68 -0
  4. package/build/shared/src/pulsemcp-admin-client/lib/save-mcp-implementation.js +3 -0
  5. package/build/shared/src/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +1 -0
  6. package/build/shared/src/pulsemcp-admin-client/lib/update-unified-mcp-server.js +2 -0
  7. package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +6 -0
  8. package/build/shared/src/server.js +4 -0
  9. package/build/shared/src/tools/get-mcp-server.js +5 -2
  10. package/build/shared/src/tools/list-proctor-runs.js +171 -0
  11. package/build/shared/src/tools/save-mcp-implementation.js +15 -3
  12. package/build/shared/src/tools/update-mcp-server.js +16 -4
  13. package/build/shared/src/tools.js +3 -1
  14. package/package.json +1 -1
  15. package/shared/pulsemcp-admin-client/lib/create-mcp-implementation.js +3 -0
  16. package/shared/pulsemcp-admin-client/lib/get-proctor-runs.d.ts +11 -0
  17. package/shared/pulsemcp-admin-client/lib/get-proctor-runs.js +68 -0
  18. package/shared/pulsemcp-admin-client/lib/save-mcp-implementation.js +3 -0
  19. package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.d.ts +2 -0
  20. package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +1 -0
  21. package/shared/pulsemcp-admin-client/lib/update-unified-mcp-server.js +2 -0
  22. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +6 -0
  23. package/shared/server.d.ts +3 -1
  24. package/shared/server.js +4 -0
  25. package/shared/tools/get-mcp-server.js +5 -2
  26. package/shared/tools/list-proctor-runs.d.ts +58 -0
  27. package/shared/tools/list-proctor-runs.js +171 -0
  28. package/shared/tools/save-mcp-implementation.d.ts +5 -1
  29. package/shared/tools/save-mcp-implementation.js +15 -3
  30. package/shared/tools/update-mcp-server.d.ts +4 -0
  31. package/shared/tools/update-mcp-server.js +16 -4
  32. package/shared/tools.d.ts +2 -2
  33. package/shared/tools.js +3 -1
  34. package/shared/types.d.ts +44 -1
package/README.md CHANGED
@@ -218,7 +218,7 @@ TOOL_GROUPS=newsletter,server_directory_readonly
218
218
  - Enable or disable specific tool groups by setting `TOOL_GROUPS` environment variable
219
219
  - Use `_readonly` suffixes to restrict groups to read-only operations (e.g., `server_directory_readonly`)
220
220
  - Use the `remote` array parameter in `save_mcp_implementation` to configure remote endpoints for MCP servers (transport, host_platform, authentication_method, etc.)
221
- - Use the `canonical` array parameter in `save_mcp_implementation` to set canonical URLs with scope (domain, subdomain, subfolder, or url)
221
+ - Use the `canonical` array parameter in `save_mcp_implementation` to set canonical URLs with scope (domain, subdomain, or url)
222
222
  - Remote endpoints allow specifying how MCP servers can be accessed (direct URL, setup URL, authentication method, cost, etc.)
223
223
  - When updating existing remotes, include the remote `id` (number from `get_draft_mcp_implementations`) in the remote object
224
224
  - Use `list_mcp_servers` to browse MCP servers with filtering by status (draft/live/archived) and classification (official/community)
@@ -64,6 +64,9 @@ export async function createMCPImplementation(apiKey, baseUrl, params) {
64
64
  if (params.recommended !== undefined) {
65
65
  formData.append('mcp_implementation[recommended]', params.recommended.toString());
66
66
  }
67
+ if (params.verified_no_remote_canonicals !== undefined) {
68
+ formData.append('mcp_implementation[verified_no_remote_canonicals]', params.verified_no_remote_canonicals.toString());
69
+ }
67
70
  // Date overrides
68
71
  if (params.created_on_override !== undefined) {
69
72
  formData.append('mcp_implementation[created_on_override]', params.created_on_override);
@@ -0,0 +1,68 @@
1
+ export async function getProctorRuns(apiKey, baseUrl, params) {
2
+ const url = new URL('/api/proctor_runs', baseUrl);
3
+ if (params?.q) {
4
+ url.searchParams.append('q', params.q);
5
+ }
6
+ if (params?.recommended) {
7
+ url.searchParams.append('recommended', '1');
8
+ }
9
+ if (params?.tenant_ids) {
10
+ url.searchParams.append('tenant_ids', params.tenant_ids);
11
+ }
12
+ if (params?.sort) {
13
+ url.searchParams.append('sort', params.sort);
14
+ }
15
+ if (params?.direction) {
16
+ url.searchParams.append('direction', params.direction);
17
+ }
18
+ if (params?.limit) {
19
+ url.searchParams.append('limit', params.limit.toString());
20
+ }
21
+ if (params?.offset !== undefined && params.offset > 0) {
22
+ url.searchParams.append('offset', params.offset.toString());
23
+ }
24
+ const response = await fetch(url.toString(), {
25
+ method: 'GET',
26
+ headers: {
27
+ 'X-API-Key': apiKey,
28
+ Accept: 'application/json',
29
+ },
30
+ });
31
+ if (!response.ok) {
32
+ if (response.status === 401) {
33
+ throw new Error('Invalid API key');
34
+ }
35
+ if (response.status === 403) {
36
+ throw new Error('User lacks admin privileges');
37
+ }
38
+ throw new Error(`Failed to fetch proctor runs: ${response.status} ${response.statusText}`);
39
+ }
40
+ const data = (await response.json());
41
+ return {
42
+ runs: data.data.map((run) => ({
43
+ id: run.id,
44
+ slug: run.slug,
45
+ name: run.name,
46
+ recommended: run.recommended,
47
+ mirrors_count: run.mirrors_count,
48
+ tenant_count: run.tenant_count,
49
+ latest_version: run.latest_version,
50
+ latest_mirror_id: run.latest_mirror_id,
51
+ latest_mirror_name: run.latest_mirror_name,
52
+ latest_tested: run.latest_tested,
53
+ last_auth_check_days: run.last_auth_check_days,
54
+ last_tools_list_days: run.last_tools_list_days,
55
+ auth_types: run.auth_types,
56
+ num_tools: run.num_tools,
57
+ packages: run.packages,
58
+ remotes: run.remotes,
59
+ })),
60
+ pagination: {
61
+ current_page: data.meta.current_page,
62
+ total_pages: data.meta.total_pages,
63
+ total_count: data.meta.total_count,
64
+ has_next: data.meta.has_next,
65
+ limit: data.meta.limit,
66
+ },
67
+ };
68
+ }
@@ -74,6 +74,9 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
74
74
  if (params.recommended !== undefined) {
75
75
  formData.append('mcp_implementation[recommended]', params.recommended.toString());
76
76
  }
77
+ if (params.verified_no_remote_canonicals !== undefined) {
78
+ formData.append('mcp_implementation[verified_no_remote_canonicals]', params.verified_no_remote_canonicals.toString());
79
+ }
77
80
  // Date overrides
78
81
  if (params.created_on_override !== undefined) {
79
82
  formData.append('mcp_implementation[created_on_override]', params.created_on_override);
@@ -47,6 +47,7 @@ export function mapToUnifiedServer(impl) {
47
47
  package_name: impl.package_name,
48
48
  // Flags
49
49
  recommended: impl.recommended,
50
+ verified_no_remote_canonicals: mcpServer.verified_no_remote_canonicals,
50
51
  // Canonical URLs
51
52
  canonical_urls: impl.canonical,
52
53
  // Remote endpoints
@@ -49,6 +49,8 @@ export async function updateUnifiedMCPServer(apiKey, baseUrl, implementationId,
49
49
  // Flags
50
50
  if (params.recommended !== undefined)
51
51
  implParams.recommended = params.recommended;
52
+ if (params.verified_no_remote_canonicals !== undefined)
53
+ implParams.verified_no_remote_canonicals = params.verified_no_remote_canonicals;
52
54
  // Date overrides
53
55
  if (params.created_on_override !== undefined)
54
56
  implParams.created_on_override = params.created_on_override;
@@ -958,6 +958,12 @@ export function createMockPulseMCPAdminClient(mockData) {
958
958
  errors: [],
959
959
  };
960
960
  },
961
+ async getProctorRuns() {
962
+ return {
963
+ runs: [],
964
+ pagination: { current_page: 1, total_pages: 1, total_count: 0, has_next: false, limit: 30 },
965
+ };
966
+ },
961
967
  // Discovered URL methods
962
968
  async getDiscoveredUrls() {
963
969
  return {
@@ -261,6 +261,10 @@ export class PulseMCPAdminClient {
261
261
  const { saveResultsForMirror } = await import('./pulsemcp-admin-client/lib/save-results-for-mirror.js');
262
262
  return saveResultsForMirror(this.apiKey, this.baseUrl, params);
263
263
  }
264
+ async getProctorRuns(params) {
265
+ const { getProctorRuns } = await import('./pulsemcp-admin-client/lib/get-proctor-runs.js');
266
+ return getProctorRuns(this.apiKey, this.baseUrl, params);
267
+ }
264
268
  // Discovered URL REST API methods
265
269
  async getDiscoveredUrls(params) {
266
270
  const { getDiscoveredUrls } = await import('./pulsemcp-admin-client/lib/get-discovered-urls.js');
@@ -14,7 +14,7 @@ The response includes:
14
14
  - **Basic info**: name, descriptions, status, classification, implementation language
15
15
  - **Provider**: organization/person details
16
16
  - **Source code location**: GitHub repository info with stars and last update
17
- - **Canonical URLs**: authoritative URLs with scope (domain, subdomain, subfolder, url)
17
+ - **Canonical URLs**: authoritative URLs with scope (domain, subdomain, url)
18
18
  - **Remote endpoints**: all deployment endpoints with transport, platform, auth, and cost info
19
19
  - **Tags**: categorization tags
20
20
  - **Download statistics**: npm download estimates
@@ -48,7 +48,7 @@ Example response:
48
48
  "github_stars": 5000
49
49
  },
50
50
  "canonical_urls": [
51
- { "url": "https://github.com/modelcontextprotocol/servers", "scope": "subfolder" }
51
+ { "url": "https://github.com/modelcontextprotocol/servers", "scope": "domain" }
52
52
  ],
53
53
  "remotes": [
54
54
  {
@@ -102,6 +102,9 @@ Example response:
102
102
  if (server.recommended !== undefined) {
103
103
  content += `**Recommended:** ${server.recommended ? 'Yes' : 'No'}\n`;
104
104
  }
105
+ if (server.verified_no_remote_canonicals !== undefined) {
106
+ content += `**Verified No Remote Canonicals:** ${server.verified_no_remote_canonicals ? 'Yes' : 'No'}\n`;
107
+ }
105
108
  if (server.short_description) {
106
109
  content += `\n**Short Description:**\n${server.short_description}\n`;
107
110
  }
@@ -0,0 +1,171 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ q: 'Search by server slug, implementation name, or mirror name',
4
+ recommended: 'When true, filter to recommended servers only. When false or omitted, shows all servers',
5
+ tenant_ids: 'Comma-separated tenant IDs to filter by (OR logic)',
6
+ sort: 'Column to sort by: slug, name, mirrors, recommended, tenants, latest_tested, last_auth_check, last_tools_list',
7
+ direction: 'Sort direction: asc or desc. Default: asc',
8
+ limit: 'Results per page, range 1-100. Default: 30',
9
+ offset: 'Pagination offset. Default: 0',
10
+ };
11
+ const ListProctorRunsSchema = z.object({
12
+ q: z.string().optional().describe(PARAM_DESCRIPTIONS.q),
13
+ recommended: z.boolean().optional().describe(PARAM_DESCRIPTIONS.recommended),
14
+ tenant_ids: z.string().optional().describe(PARAM_DESCRIPTIONS.tenant_ids),
15
+ sort: z
16
+ .enum([
17
+ 'slug',
18
+ 'name',
19
+ 'mirrors',
20
+ 'recommended',
21
+ 'tenants',
22
+ 'latest_tested',
23
+ 'last_auth_check',
24
+ 'last_tools_list',
25
+ ])
26
+ .optional()
27
+ .describe(PARAM_DESCRIPTIONS.sort),
28
+ direction: z.enum(['asc', 'desc']).optional().describe(PARAM_DESCRIPTIONS.direction),
29
+ limit: z.number().min(1).max(100).optional().describe(PARAM_DESCRIPTIONS.limit),
30
+ offset: z.number().min(0).optional().describe(PARAM_DESCRIPTIONS.offset),
31
+ });
32
+ export function listProctorRuns(_server, clientFactory) {
33
+ return {
34
+ name: 'list_proctor_runs',
35
+ description: `List proctor run summaries for MCP servers. Shows testing status across all servers including auth-check and tools-list exam results. Useful for identifying untested servers or servers that need re-testing.
36
+
37
+ Default sort order: untested servers first, then stalest auth check, then stalest tools list, then alphabetical by slug.
38
+
39
+ Example response:
40
+ {
41
+ "runs": [
42
+ {
43
+ "id": 123,
44
+ "slug": "some-server",
45
+ "name": "Some Server",
46
+ "recommended": true,
47
+ "mirrors_count": 2,
48
+ "latest_tested": true,
49
+ "last_auth_check_days": 2,
50
+ "last_tools_list_days": 3,
51
+ "auth_types": ["oauth2"],
52
+ "num_tools": 5,
53
+ "packages": ["npm"],
54
+ "remotes": ["streamable-http"]
55
+ }
56
+ ],
57
+ "pagination": { "current_page": 1, "total_pages": 2, "total_count": 45, "has_next": true }
58
+ }
59
+
60
+ Use cases:
61
+ - Find servers that haven't been tested yet (untested appear first by default)
62
+ - Check which servers have stale proctor results
63
+ - Filter to recommended servers to prioritize testing
64
+ - Search for a specific server's testing status`,
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ q: { type: 'string', description: PARAM_DESCRIPTIONS.q },
69
+ recommended: { type: 'boolean', description: PARAM_DESCRIPTIONS.recommended },
70
+ tenant_ids: { type: 'string', description: PARAM_DESCRIPTIONS.tenant_ids },
71
+ sort: {
72
+ type: 'string',
73
+ enum: [
74
+ 'slug',
75
+ 'name',
76
+ 'mirrors',
77
+ 'recommended',
78
+ 'tenants',
79
+ 'latest_tested',
80
+ 'last_auth_check',
81
+ 'last_tools_list',
82
+ ],
83
+ description: PARAM_DESCRIPTIONS.sort,
84
+ },
85
+ direction: {
86
+ type: 'string',
87
+ enum: ['asc', 'desc'],
88
+ description: PARAM_DESCRIPTIONS.direction,
89
+ },
90
+ limit: { type: 'number', minimum: 1, maximum: 100, description: PARAM_DESCRIPTIONS.limit },
91
+ offset: { type: 'number', minimum: 0, description: PARAM_DESCRIPTIONS.offset },
92
+ },
93
+ },
94
+ handler: async (args) => {
95
+ const validatedArgs = ListProctorRunsSchema.parse(args);
96
+ const client = clientFactory();
97
+ try {
98
+ const response = await client.getProctorRuns({
99
+ q: validatedArgs.q,
100
+ recommended: validatedArgs.recommended,
101
+ tenant_ids: validatedArgs.tenant_ids,
102
+ sort: validatedArgs.sort,
103
+ direction: validatedArgs.direction,
104
+ limit: validatedArgs.limit,
105
+ offset: validatedArgs.offset,
106
+ });
107
+ let content = `Found ${response.runs.length} proctor run summaries`;
108
+ if (response.pagination) {
109
+ content += ` (page ${response.pagination.current_page} of ${response.pagination.total_pages}, total: ${response.pagination.total_count})`;
110
+ }
111
+ content += ':\n\n';
112
+ for (const [index, run] of response.runs.entries()) {
113
+ content += `${index + 1}. **${run.slug}**`;
114
+ if (run.name) {
115
+ content += ` — ${run.name}`;
116
+ }
117
+ content += ` (ID: ${run.id})\n`;
118
+ if (run.recommended) {
119
+ content += ` Recommended: yes\n`;
120
+ }
121
+ content += ` Mirrors: ${run.mirrors_count}, Tenants: ${run.tenant_count}\n`;
122
+ if (run.latest_version) {
123
+ content += ` Latest Version: ${run.latest_version}`;
124
+ if (run.latest_mirror_name) {
125
+ content += ` (mirror: ${run.latest_mirror_name}, ID: ${run.latest_mirror_id})`;
126
+ }
127
+ content += '\n';
128
+ }
129
+ content += ` Latest Tested: ${run.latest_tested ? 'yes' : 'no'}\n`;
130
+ if (run.last_auth_check_days !== null) {
131
+ content += ` Last Auth Check: ${run.last_auth_check_days} day(s) ago\n`;
132
+ }
133
+ else {
134
+ content += ` Last Auth Check: never\n`;
135
+ }
136
+ if (run.last_tools_list_days !== null) {
137
+ content += ` Last Tools List: ${run.last_tools_list_days} day(s) ago\n`;
138
+ }
139
+ else {
140
+ content += ` Last Tools List: never\n`;
141
+ }
142
+ if (run.auth_types.length > 0) {
143
+ content += ` Auth Types: ${run.auth_types.join(', ')}\n`;
144
+ }
145
+ if (run.num_tools !== null) {
146
+ content += ` Num Tools: ${run.num_tools}\n`;
147
+ }
148
+ if (run.packages.length > 0) {
149
+ content += ` Packages: ${run.packages.join(', ')}\n`;
150
+ }
151
+ if (run.remotes.length > 0) {
152
+ content += ` Remotes: ${run.remotes.join(', ')}\n`;
153
+ }
154
+ content += '\n';
155
+ }
156
+ return { content: [{ type: 'text', text: content.trim() }] };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ content: [
161
+ {
162
+ type: 'text',
163
+ text: `Error fetching proctor runs: ${error instanceof Error ? error.message : String(error)}`,
164
+ },
165
+ ],
166
+ isError: true,
167
+ };
168
+ }
169
+ },
170
+ };
171
+ }
@@ -26,7 +26,9 @@ const PARAM_DESCRIPTIONS = {
26
26
  // Remote endpoints
27
27
  remote: 'Array of remote endpoint configurations for MCP servers. Each remote can have: id (existing remote ID or blank for new), url_direct, url_setup, transport (e.g., "sse"), host_platform (e.g., "smithery"), host_infrastructure (e.g., "cloudflare"), authentication_method (e.g., "open"), cost (e.g., "free"), status (defaults to "live"), display_name, and internal_notes.',
28
28
  // Canonical URLs
29
- canonical: 'Array of canonical URL configurations. Each entry must have: url (the canonical URL), scope (one of "domain", "subdomain", "subfolder", or "url"), and optional note for additional context.',
29
+ canonical: 'Array of canonical URL configurations. Each entry must have: url (the canonical URL), scope (one of "domain", "subdomain", or "url"), and optional note for additional context.',
30
+ // Flags
31
+ verified_no_remote_canonicals: 'Mark that this server has been verified to have no remote canonical URLs (true = verified no remote canonicals exist, false = reset/canonicals found)',
30
32
  // Other fields
31
33
  internal_notes: 'Admin-only notes. Not displayed publicly. Used for tracking submission sources, reviewer comments, etc.',
32
34
  };
@@ -83,11 +85,16 @@ const SaveMCPImplementationSchema = z.object({
83
85
  canonical: z
84
86
  .array(z.object({
85
87
  url: z.string(),
86
- scope: z.enum(['domain', 'subdomain', 'subfolder', 'url']),
88
+ scope: z.enum(['domain', 'subdomain', 'url']),
87
89
  note: z.string().optional(),
88
90
  }))
89
91
  .optional()
90
92
  .describe(PARAM_DESCRIPTIONS.canonical),
93
+ // Flags
94
+ verified_no_remote_canonicals: z
95
+ .boolean()
96
+ .optional()
97
+ .describe(PARAM_DESCRIPTIONS.verified_no_remote_canonicals),
91
98
  // Other fields
92
99
  internal_notes: z.string().optional().describe(PARAM_DESCRIPTIONS.internal_notes),
93
100
  });
@@ -294,13 +301,18 @@ Use cases:
294
301
  type: 'object',
295
302
  properties: {
296
303
  url: { type: 'string' },
297
- scope: { type: 'string', enum: ['domain', 'subdomain', 'subfolder', 'url'] },
304
+ scope: { type: 'string', enum: ['domain', 'subdomain', 'url'] },
298
305
  note: { type: 'string' },
299
306
  },
300
307
  required: ['url', 'scope'],
301
308
  },
302
309
  description: PARAM_DESCRIPTIONS.canonical,
303
310
  },
311
+ // Flags
312
+ verified_no_remote_canonicals: {
313
+ type: 'boolean',
314
+ description: PARAM_DESCRIPTIONS.verified_no_remote_canonicals,
315
+ },
304
316
  // Other fields
305
317
  internal_notes: {
306
318
  type: 'string',
@@ -16,6 +16,7 @@ const PARAM_DESCRIPTIONS = {
16
16
  package_registry: 'Package registry: npm, pypi, cargo, etc.',
17
17
  package_name: 'Package name on the registry (e.g., "@modelcontextprotocol/server-filesystem")',
18
18
  recommended: 'Mark this server as recommended by PulseMCP',
19
+ verified_no_remote_canonicals: 'Mark that this server has been verified to have no remote canonical URLs (true = verified no remote canonicals exist, false = reset/canonicals found)',
19
20
  created_on_override: 'Override the automatically derived created date (ISO date string, e.g., "2025-01-15")',
20
21
  tags: 'Tags for the server. Replaces all existing tags when provided. Use tag slugs.',
21
22
  canonical_urls: 'Authoritative URLs for the server. Replaces all existing canonical URLs when provided.',
@@ -25,8 +26,8 @@ const PARAM_DESCRIPTIONS = {
25
26
  const CanonicalUrlSchema = z.object({
26
27
  url: z.string().describe('The canonical URL'),
27
28
  scope: z
28
- .enum(['domain', 'subdomain', 'subfolder', 'url'])
29
- .describe('Scope of the canonical: domain, subdomain, subfolder, or url (exact match)'),
29
+ .enum(['domain', 'subdomain', 'url'])
30
+ .describe('Scope of the canonical: domain, subdomain, or url (exact match)'),
30
31
  note: z.string().optional().describe('Optional note about this canonical URL'),
31
32
  });
32
33
  const RemoteEndpointSchema = z.object({
@@ -82,6 +83,10 @@ const UpdateMCPServerSchema = z.object({
82
83
  package_registry: z.string().optional().describe(PARAM_DESCRIPTIONS.package_registry),
83
84
  package_name: z.string().optional().describe(PARAM_DESCRIPTIONS.package_name),
84
85
  recommended: z.boolean().optional().describe(PARAM_DESCRIPTIONS.recommended),
86
+ verified_no_remote_canonicals: z
87
+ .boolean()
88
+ .optional()
89
+ .describe(PARAM_DESCRIPTIONS.verified_no_remote_canonicals),
85
90
  created_on_override: z.string().optional().describe(PARAM_DESCRIPTIONS.created_on_override),
86
91
  tags: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.tags),
87
92
  canonical_urls: z
@@ -126,7 +131,7 @@ Providing canonical_urls replaces ALL existing canonical URLs:
126
131
  {
127
132
  "implementation_id": 456,
128
133
  "canonical_urls": [
129
- { "url": "https://github.com/org/repo", "scope": "subfolder" },
134
+ { "url": "https://github.com/org/repo", "scope": "domain" },
130
135
  { "url": "https://npmjs.com/package/name", "scope": "url" }
131
136
  ]
132
137
  }
@@ -220,6 +225,10 @@ Create new provider:
220
225
  package_registry: { type: 'string', description: PARAM_DESCRIPTIONS.package_registry },
221
226
  package_name: { type: 'string', description: PARAM_DESCRIPTIONS.package_name },
222
227
  recommended: { type: 'boolean', description: PARAM_DESCRIPTIONS.recommended },
228
+ verified_no_remote_canonicals: {
229
+ type: 'boolean',
230
+ description: PARAM_DESCRIPTIONS.verified_no_remote_canonicals,
231
+ },
223
232
  created_on_override: {
224
233
  type: 'string',
225
234
  description: PARAM_DESCRIPTIONS.created_on_override,
@@ -237,7 +246,7 @@ Create new provider:
237
246
  url: { type: 'string', description: 'The canonical URL' },
238
247
  scope: {
239
248
  type: 'string',
240
- enum: ['domain', 'subdomain', 'subfolder', 'url'],
249
+ enum: ['domain', 'subdomain', 'url'],
241
250
  description: 'Scope of the canonical',
242
251
  },
243
252
  note: { type: 'string', description: 'Optional note' },
@@ -318,6 +327,9 @@ Create new provider:
318
327
  if (server.recommended !== undefined) {
319
328
  content += `**Recommended:** ${server.recommended ? 'Yes' : 'No'}\n`;
320
329
  }
330
+ if (server.verified_no_remote_canonicals !== undefined) {
331
+ content += `**Verified No Remote Canonicals:** ${server.verified_no_remote_canonicals ? 'Yes' : 'No'}\n`;
332
+ }
321
333
  if (server.updated_at) {
322
334
  content += `**Updated:** ${server.updated_at}\n`;
323
335
  }
@@ -60,6 +60,7 @@ import { cleanupGoodJobs } from './tools/cleanup-good-jobs.js';
60
60
  import { runExamForMirror } from './tools/run-exam-for-mirror.js';
61
61
  import { getExamResult } from './tools/get-exam-result.js';
62
62
  import { saveResultsForMirror } from './tools/save-results-for-mirror.js';
63
+ import { listProctorRuns } from './tools/list-proctor-runs.js';
63
64
  // Discovered URLs tools
64
65
  import { listDiscoveredUrls } from './tools/list-discovered-urls.js';
65
66
  import { markDiscoveredUrlProcessed } from './tools/mark-discovered-url-processed.js';
@@ -230,6 +231,7 @@ const ALL_TOOLS = [
230
231
  { factory: runExamForMirror, groups: ['proctor'], isWriteOperation: true },
231
232
  { factory: getExamResult, groups: ['proctor'], isWriteOperation: false },
232
233
  { factory: saveResultsForMirror, groups: ['proctor'], isWriteOperation: true },
234
+ { factory: listProctorRuns, groups: ['proctor'], isWriteOperation: false },
233
235
  // Discovered URLs tools
234
236
  { factory: listDiscoveredUrls, groups: ['discovered_urls'], isWriteOperation: false },
235
237
  {
@@ -367,7 +369,7 @@ function shouldIncludeTool(toolDef, enabledGroups) {
367
369
  * - good_jobs: GoodJob background job management tools (read + write)
368
370
  * - good_jobs_readonly: GoodJob tools (read only)
369
371
  * - proctor: Proctor exam execution and result storage tools (read + write)
370
- * - proctor_readonly: Proctor tools (read only - get_exam_result for retrieving stored results)
372
+ * - proctor_readonly: Proctor tools (read only - get_exam_result and list_proctor_runs)
371
373
  * - discovered_urls: Discovered URL management tools for processing URLs into MCP implementations (read + write)
372
374
  * - discovered_urls_readonly: Discovered URL tools (read only - list and stats)
373
375
  * - notifications: Notification email tools - send_impl_posted_notif (write-only, no readonly variant)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulsemcp-cms-admin-mcp-server",
3
- "version": "0.8.0",
3
+ "version": "0.9.2",
4
4
  "description": "Local implementation of PulseMCP CMS Admin MCP server",
5
5
  "mcpName": "com.pulsemcp.servers/pulsemcp-cms-admin",
6
6
  "main": "build/index.js",
@@ -64,6 +64,9 @@ export async function createMCPImplementation(apiKey, baseUrl, params) {
64
64
  if (params.recommended !== undefined) {
65
65
  formData.append('mcp_implementation[recommended]', params.recommended.toString());
66
66
  }
67
+ if (params.verified_no_remote_canonicals !== undefined) {
68
+ formData.append('mcp_implementation[verified_no_remote_canonicals]', params.verified_no_remote_canonicals.toString());
69
+ }
67
70
  // Date overrides
68
71
  if (params.created_on_override !== undefined) {
69
72
  formData.append('mcp_implementation[created_on_override]', params.created_on_override);
@@ -0,0 +1,11 @@
1
+ import type { ProctorRunsResponse } from '../../types.js';
2
+ export declare function getProctorRuns(apiKey: string, baseUrl: string, params?: {
3
+ q?: string;
4
+ recommended?: boolean;
5
+ tenant_ids?: string;
6
+ sort?: 'slug' | 'name' | 'mirrors' | 'recommended' | 'tenants' | 'latest_tested' | 'last_auth_check' | 'last_tools_list';
7
+ direction?: 'asc' | 'desc';
8
+ limit?: number;
9
+ offset?: number;
10
+ }): Promise<ProctorRunsResponse>;
11
+ //# sourceMappingURL=get-proctor-runs.d.ts.map
@@ -0,0 +1,68 @@
1
+ export async function getProctorRuns(apiKey, baseUrl, params) {
2
+ const url = new URL('/api/proctor_runs', baseUrl);
3
+ if (params?.q) {
4
+ url.searchParams.append('q', params.q);
5
+ }
6
+ if (params?.recommended) {
7
+ url.searchParams.append('recommended', '1');
8
+ }
9
+ if (params?.tenant_ids) {
10
+ url.searchParams.append('tenant_ids', params.tenant_ids);
11
+ }
12
+ if (params?.sort) {
13
+ url.searchParams.append('sort', params.sort);
14
+ }
15
+ if (params?.direction) {
16
+ url.searchParams.append('direction', params.direction);
17
+ }
18
+ if (params?.limit) {
19
+ url.searchParams.append('limit', params.limit.toString());
20
+ }
21
+ if (params?.offset !== undefined && params.offset > 0) {
22
+ url.searchParams.append('offset', params.offset.toString());
23
+ }
24
+ const response = await fetch(url.toString(), {
25
+ method: 'GET',
26
+ headers: {
27
+ 'X-API-Key': apiKey,
28
+ Accept: 'application/json',
29
+ },
30
+ });
31
+ if (!response.ok) {
32
+ if (response.status === 401) {
33
+ throw new Error('Invalid API key');
34
+ }
35
+ if (response.status === 403) {
36
+ throw new Error('User lacks admin privileges');
37
+ }
38
+ throw new Error(`Failed to fetch proctor runs: ${response.status} ${response.statusText}`);
39
+ }
40
+ const data = (await response.json());
41
+ return {
42
+ runs: data.data.map((run) => ({
43
+ id: run.id,
44
+ slug: run.slug,
45
+ name: run.name,
46
+ recommended: run.recommended,
47
+ mirrors_count: run.mirrors_count,
48
+ tenant_count: run.tenant_count,
49
+ latest_version: run.latest_version,
50
+ latest_mirror_id: run.latest_mirror_id,
51
+ latest_mirror_name: run.latest_mirror_name,
52
+ latest_tested: run.latest_tested,
53
+ last_auth_check_days: run.last_auth_check_days,
54
+ last_tools_list_days: run.last_tools_list_days,
55
+ auth_types: run.auth_types,
56
+ num_tools: run.num_tools,
57
+ packages: run.packages,
58
+ remotes: run.remotes,
59
+ })),
60
+ pagination: {
61
+ current_page: data.meta.current_page,
62
+ total_pages: data.meta.total_pages,
63
+ total_count: data.meta.total_count,
64
+ has_next: data.meta.has_next,
65
+ limit: data.meta.limit,
66
+ },
67
+ };
68
+ }
@@ -74,6 +74,9 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
74
74
  if (params.recommended !== undefined) {
75
75
  formData.append('mcp_implementation[recommended]', params.recommended.toString());
76
76
  }
77
+ if (params.verified_no_remote_canonicals !== undefined) {
78
+ formData.append('mcp_implementation[verified_no_remote_canonicals]', params.verified_no_remote_canonicals.toString());
79
+ }
77
80
  // Date overrides
78
81
  if (params.created_on_override !== undefined) {
79
82
  formData.append('mcp_implementation[created_on_override]', params.created_on_override);
@@ -45,6 +45,8 @@ export interface RailsImplementation {
45
45
  downloads_estimate_last_30_days?: number;
46
46
  downloads_estimate_total?: number;
47
47
  mcp_server_remotes_count?: number;
48
+ recommended?: boolean;
49
+ verified_no_remote_canonicals?: boolean;
48
50
  tags?: MCPServerTag[];
49
51
  remotes?: MCPServerRemote[];
50
52
  created_at?: string;
@@ -47,6 +47,7 @@ export function mapToUnifiedServer(impl) {
47
47
  package_name: impl.package_name,
48
48
  // Flags
49
49
  recommended: impl.recommended,
50
+ verified_no_remote_canonicals: mcpServer.verified_no_remote_canonicals,
50
51
  // Canonical URLs
51
52
  canonical_urls: impl.canonical,
52
53
  // Remote endpoints
@@ -49,6 +49,8 @@ export async function updateUnifiedMCPServer(apiKey, baseUrl, implementationId,
49
49
  // Flags
50
50
  if (params.recommended !== undefined)
51
51
  implParams.recommended = params.recommended;
52
+ if (params.verified_no_remote_canonicals !== undefined)
53
+ implParams.verified_no_remote_canonicals = params.verified_no_remote_canonicals;
52
54
  // Date overrides
53
55
  if (params.created_on_override !== undefined)
54
56
  implParams.created_on_override = params.created_on_override;
@@ -958,6 +958,12 @@ export function createMockPulseMCPAdminClient(mockData) {
958
958
  errors: [],
959
959
  };
960
960
  },
961
+ async getProctorRuns() {
962
+ return {
963
+ runs: [],
964
+ pagination: { current_page: 1, total_pages: 1, total_count: 0, has_next: false, limit: 30 },
965
+ };
966
+ },
961
967
  // Discovered URL methods
962
968
  async getDiscoveredUrls() {
963
969
  return {
@@ -1,5 +1,5 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import type { Post, PostsResponse, CreatePostParams, UpdatePostParams, ImageUploadResponse, Author, AuthorsResponse, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, SaveMCPImplementationParams, CreateMCPImplementationParams, Provider, ProvidersResponse, OfficialMirrorQueueStatus, OfficialMirrorQueueResponse, OfficialMirrorQueueItemDetail, OfficialMirrorQueueActionResponse, UnofficialMirror, UnofficialMirrorsResponse, CreateUnofficialMirrorParams, UpdateUnofficialMirrorParams, OfficialMirrorRest, OfficialMirrorsResponse, Tenant, TenantsResponse, McpJson, McpJsonsResponse, CreateMcpJsonParams, UpdateMcpJsonParams, UnifiedMCPServer, UnifiedMCPServersResponse, UpdateUnifiedMCPServerParams, Redirect, RedirectsResponse, RedirectStatus, CreateRedirectParams, UpdateRedirectParams, GoodJob, GoodJobsResponse, GoodJobStatus, GoodJobCronSchedule, GoodJobProcess, GoodJobStatistics, GoodJobActionResponse, GoodJobCleanupResponse, ProctorRunExamParams, ProctorRunExamResponse, ProctorSaveResultsParams, ProctorSaveResultsResponse, DiscoveredUrlsResponse, MarkDiscoveredUrlProcessedParams, MarkDiscoveredUrlProcessedResponse, DiscoveredUrlStats } from './types.js';
2
+ import type { Post, PostsResponse, CreatePostParams, UpdatePostParams, ImageUploadResponse, Author, AuthorsResponse, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, SaveMCPImplementationParams, CreateMCPImplementationParams, Provider, ProvidersResponse, OfficialMirrorQueueStatus, OfficialMirrorQueueResponse, OfficialMirrorQueueItemDetail, OfficialMirrorQueueActionResponse, UnofficialMirror, UnofficialMirrorsResponse, CreateUnofficialMirrorParams, UpdateUnofficialMirrorParams, OfficialMirrorRest, OfficialMirrorsResponse, Tenant, TenantsResponse, McpJson, McpJsonsResponse, CreateMcpJsonParams, UpdateMcpJsonParams, UnifiedMCPServer, UnifiedMCPServersResponse, UpdateUnifiedMCPServerParams, Redirect, RedirectsResponse, RedirectStatus, CreateRedirectParams, UpdateRedirectParams, GoodJob, GoodJobsResponse, GoodJobStatus, GoodJobCronSchedule, GoodJobProcess, GoodJobStatistics, GoodJobActionResponse, GoodJobCleanupResponse, ProctorRunExamParams, ProctorRunExamResponse, ProctorSaveResultsParams, ProctorSaveResultsResponse, ProctorRunsResponse, GetProctorRunsParams, DiscoveredUrlsResponse, MarkDiscoveredUrlProcessedParams, MarkDiscoveredUrlProcessedResponse, DiscoveredUrlStats } from './types.js';
3
3
  export interface IPulseMCPAdminClient {
4
4
  getPosts(params?: {
5
5
  search?: string;
@@ -160,6 +160,7 @@ export interface IPulseMCPAdminClient {
160
160
  }): Promise<GoodJobCleanupResponse>;
161
161
  runExamForMirror(params: ProctorRunExamParams): Promise<ProctorRunExamResponse>;
162
162
  saveResultsForMirror(params: ProctorSaveResultsParams): Promise<ProctorSaveResultsResponse>;
163
+ getProctorRuns(params?: GetProctorRunsParams): Promise<ProctorRunsResponse>;
163
164
  getDiscoveredUrls(params?: {
164
165
  status?: 'pending' | 'processed' | 'all';
165
166
  page?: number;
@@ -331,6 +332,7 @@ export declare class PulseMCPAdminClient implements IPulseMCPAdminClient {
331
332
  }): Promise<GoodJobCleanupResponse>;
332
333
  runExamForMirror(params: ProctorRunExamParams): Promise<ProctorRunExamResponse>;
333
334
  saveResultsForMirror(params: ProctorSaveResultsParams): Promise<ProctorSaveResultsResponse>;
335
+ getProctorRuns(params?: GetProctorRunsParams): Promise<ProctorRunsResponse>;
334
336
  getDiscoveredUrls(params?: {
335
337
  status?: 'pending' | 'processed' | 'all';
336
338
  page?: number;
package/shared/server.js CHANGED
@@ -261,6 +261,10 @@ export class PulseMCPAdminClient {
261
261
  const { saveResultsForMirror } = await import('./pulsemcp-admin-client/lib/save-results-for-mirror.js');
262
262
  return saveResultsForMirror(this.apiKey, this.baseUrl, params);
263
263
  }
264
+ async getProctorRuns(params) {
265
+ const { getProctorRuns } = await import('./pulsemcp-admin-client/lib/get-proctor-runs.js');
266
+ return getProctorRuns(this.apiKey, this.baseUrl, params);
267
+ }
264
268
  // Discovered URL REST API methods
265
269
  async getDiscoveredUrls(params) {
266
270
  const { getDiscoveredUrls } = await import('./pulsemcp-admin-client/lib/get-discovered-urls.js');
@@ -14,7 +14,7 @@ The response includes:
14
14
  - **Basic info**: name, descriptions, status, classification, implementation language
15
15
  - **Provider**: organization/person details
16
16
  - **Source code location**: GitHub repository info with stars and last update
17
- - **Canonical URLs**: authoritative URLs with scope (domain, subdomain, subfolder, url)
17
+ - **Canonical URLs**: authoritative URLs with scope (domain, subdomain, url)
18
18
  - **Remote endpoints**: all deployment endpoints with transport, platform, auth, and cost info
19
19
  - **Tags**: categorization tags
20
20
  - **Download statistics**: npm download estimates
@@ -48,7 +48,7 @@ Example response:
48
48
  "github_stars": 5000
49
49
  },
50
50
  "canonical_urls": [
51
- { "url": "https://github.com/modelcontextprotocol/servers", "scope": "subfolder" }
51
+ { "url": "https://github.com/modelcontextprotocol/servers", "scope": "domain" }
52
52
  ],
53
53
  "remotes": [
54
54
  {
@@ -102,6 +102,9 @@ Example response:
102
102
  if (server.recommended !== undefined) {
103
103
  content += `**Recommended:** ${server.recommended ? 'Yes' : 'No'}\n`;
104
104
  }
105
+ if (server.verified_no_remote_canonicals !== undefined) {
106
+ content += `**Verified No Remote Canonicals:** ${server.verified_no_remote_canonicals ? 'Yes' : 'No'}\n`;
107
+ }
105
108
  if (server.short_description) {
106
109
  content += `\n**Short Description:**\n${server.short_description}\n`;
107
110
  }
@@ -0,0 +1,58 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { ClientFactory } from '../server.js';
3
+ export declare function listProctorRuns(_server: Server, clientFactory: ClientFactory): {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: string;
8
+ properties: {
9
+ q: {
10
+ type: string;
11
+ description: "Search by server slug, implementation name, or mirror name";
12
+ };
13
+ recommended: {
14
+ type: string;
15
+ description: "When true, filter to recommended servers only. When false or omitted, shows all servers";
16
+ };
17
+ tenant_ids: {
18
+ type: string;
19
+ description: "Comma-separated tenant IDs to filter by (OR logic)";
20
+ };
21
+ sort: {
22
+ type: string;
23
+ enum: string[];
24
+ description: "Column to sort by: slug, name, mirrors, recommended, tenants, latest_tested, last_auth_check, last_tools_list";
25
+ };
26
+ direction: {
27
+ type: string;
28
+ enum: string[];
29
+ description: "Sort direction: asc or desc. Default: asc";
30
+ };
31
+ limit: {
32
+ type: string;
33
+ minimum: number;
34
+ maximum: number;
35
+ description: "Results per page, range 1-100. Default: 30";
36
+ };
37
+ offset: {
38
+ type: string;
39
+ minimum: number;
40
+ description: "Pagination offset. Default: 0";
41
+ };
42
+ };
43
+ };
44
+ handler: (args: unknown) => Promise<{
45
+ content: {
46
+ type: string;
47
+ text: string;
48
+ }[];
49
+ isError?: undefined;
50
+ } | {
51
+ content: {
52
+ type: string;
53
+ text: string;
54
+ }[];
55
+ isError: boolean;
56
+ }>;
57
+ };
58
+ //# sourceMappingURL=list-proctor-runs.d.ts.map
@@ -0,0 +1,171 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ q: 'Search by server slug, implementation name, or mirror name',
4
+ recommended: 'When true, filter to recommended servers only. When false or omitted, shows all servers',
5
+ tenant_ids: 'Comma-separated tenant IDs to filter by (OR logic)',
6
+ sort: 'Column to sort by: slug, name, mirrors, recommended, tenants, latest_tested, last_auth_check, last_tools_list',
7
+ direction: 'Sort direction: asc or desc. Default: asc',
8
+ limit: 'Results per page, range 1-100. Default: 30',
9
+ offset: 'Pagination offset. Default: 0',
10
+ };
11
+ const ListProctorRunsSchema = z.object({
12
+ q: z.string().optional().describe(PARAM_DESCRIPTIONS.q),
13
+ recommended: z.boolean().optional().describe(PARAM_DESCRIPTIONS.recommended),
14
+ tenant_ids: z.string().optional().describe(PARAM_DESCRIPTIONS.tenant_ids),
15
+ sort: z
16
+ .enum([
17
+ 'slug',
18
+ 'name',
19
+ 'mirrors',
20
+ 'recommended',
21
+ 'tenants',
22
+ 'latest_tested',
23
+ 'last_auth_check',
24
+ 'last_tools_list',
25
+ ])
26
+ .optional()
27
+ .describe(PARAM_DESCRIPTIONS.sort),
28
+ direction: z.enum(['asc', 'desc']).optional().describe(PARAM_DESCRIPTIONS.direction),
29
+ limit: z.number().min(1).max(100).optional().describe(PARAM_DESCRIPTIONS.limit),
30
+ offset: z.number().min(0).optional().describe(PARAM_DESCRIPTIONS.offset),
31
+ });
32
+ export function listProctorRuns(_server, clientFactory) {
33
+ return {
34
+ name: 'list_proctor_runs',
35
+ description: `List proctor run summaries for MCP servers. Shows testing status across all servers including auth-check and tools-list exam results. Useful for identifying untested servers or servers that need re-testing.
36
+
37
+ Default sort order: untested servers first, then stalest auth check, then stalest tools list, then alphabetical by slug.
38
+
39
+ Example response:
40
+ {
41
+ "runs": [
42
+ {
43
+ "id": 123,
44
+ "slug": "some-server",
45
+ "name": "Some Server",
46
+ "recommended": true,
47
+ "mirrors_count": 2,
48
+ "latest_tested": true,
49
+ "last_auth_check_days": 2,
50
+ "last_tools_list_days": 3,
51
+ "auth_types": ["oauth2"],
52
+ "num_tools": 5,
53
+ "packages": ["npm"],
54
+ "remotes": ["streamable-http"]
55
+ }
56
+ ],
57
+ "pagination": { "current_page": 1, "total_pages": 2, "total_count": 45, "has_next": true }
58
+ }
59
+
60
+ Use cases:
61
+ - Find servers that haven't been tested yet (untested appear first by default)
62
+ - Check which servers have stale proctor results
63
+ - Filter to recommended servers to prioritize testing
64
+ - Search for a specific server's testing status`,
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ q: { type: 'string', description: PARAM_DESCRIPTIONS.q },
69
+ recommended: { type: 'boolean', description: PARAM_DESCRIPTIONS.recommended },
70
+ tenant_ids: { type: 'string', description: PARAM_DESCRIPTIONS.tenant_ids },
71
+ sort: {
72
+ type: 'string',
73
+ enum: [
74
+ 'slug',
75
+ 'name',
76
+ 'mirrors',
77
+ 'recommended',
78
+ 'tenants',
79
+ 'latest_tested',
80
+ 'last_auth_check',
81
+ 'last_tools_list',
82
+ ],
83
+ description: PARAM_DESCRIPTIONS.sort,
84
+ },
85
+ direction: {
86
+ type: 'string',
87
+ enum: ['asc', 'desc'],
88
+ description: PARAM_DESCRIPTIONS.direction,
89
+ },
90
+ limit: { type: 'number', minimum: 1, maximum: 100, description: PARAM_DESCRIPTIONS.limit },
91
+ offset: { type: 'number', minimum: 0, description: PARAM_DESCRIPTIONS.offset },
92
+ },
93
+ },
94
+ handler: async (args) => {
95
+ const validatedArgs = ListProctorRunsSchema.parse(args);
96
+ const client = clientFactory();
97
+ try {
98
+ const response = await client.getProctorRuns({
99
+ q: validatedArgs.q,
100
+ recommended: validatedArgs.recommended,
101
+ tenant_ids: validatedArgs.tenant_ids,
102
+ sort: validatedArgs.sort,
103
+ direction: validatedArgs.direction,
104
+ limit: validatedArgs.limit,
105
+ offset: validatedArgs.offset,
106
+ });
107
+ let content = `Found ${response.runs.length} proctor run summaries`;
108
+ if (response.pagination) {
109
+ content += ` (page ${response.pagination.current_page} of ${response.pagination.total_pages}, total: ${response.pagination.total_count})`;
110
+ }
111
+ content += ':\n\n';
112
+ for (const [index, run] of response.runs.entries()) {
113
+ content += `${index + 1}. **${run.slug}**`;
114
+ if (run.name) {
115
+ content += ` — ${run.name}`;
116
+ }
117
+ content += ` (ID: ${run.id})\n`;
118
+ if (run.recommended) {
119
+ content += ` Recommended: yes\n`;
120
+ }
121
+ content += ` Mirrors: ${run.mirrors_count}, Tenants: ${run.tenant_count}\n`;
122
+ if (run.latest_version) {
123
+ content += ` Latest Version: ${run.latest_version}`;
124
+ if (run.latest_mirror_name) {
125
+ content += ` (mirror: ${run.latest_mirror_name}, ID: ${run.latest_mirror_id})`;
126
+ }
127
+ content += '\n';
128
+ }
129
+ content += ` Latest Tested: ${run.latest_tested ? 'yes' : 'no'}\n`;
130
+ if (run.last_auth_check_days !== null) {
131
+ content += ` Last Auth Check: ${run.last_auth_check_days} day(s) ago\n`;
132
+ }
133
+ else {
134
+ content += ` Last Auth Check: never\n`;
135
+ }
136
+ if (run.last_tools_list_days !== null) {
137
+ content += ` Last Tools List: ${run.last_tools_list_days} day(s) ago\n`;
138
+ }
139
+ else {
140
+ content += ` Last Tools List: never\n`;
141
+ }
142
+ if (run.auth_types.length > 0) {
143
+ content += ` Auth Types: ${run.auth_types.join(', ')}\n`;
144
+ }
145
+ if (run.num_tools !== null) {
146
+ content += ` Num Tools: ${run.num_tools}\n`;
147
+ }
148
+ if (run.packages.length > 0) {
149
+ content += ` Packages: ${run.packages.join(', ')}\n`;
150
+ }
151
+ if (run.remotes.length > 0) {
152
+ content += ` Remotes: ${run.remotes.join(', ')}\n`;
153
+ }
154
+ content += '\n';
155
+ }
156
+ return { content: [{ type: 'text', text: content.trim() }] };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ content: [
161
+ {
162
+ type: 'text',
163
+ text: `Error fetching proctor runs: ${error instanceof Error ? error.message : String(error)}`,
164
+ },
165
+ ],
166
+ isError: true,
167
+ };
168
+ }
169
+ },
170
+ };
171
+ }
@@ -149,7 +149,11 @@ export declare function saveMCPImplementation(_server: Server, clientFactory: Cl
149
149
  };
150
150
  required: string[];
151
151
  };
152
- description: "Array of canonical URL configurations. Each entry must have: url (the canonical URL), scope (one of \"domain\", \"subdomain\", \"subfolder\", or \"url\"), and optional note for additional context.";
152
+ description: "Array of canonical URL configurations. Each entry must have: url (the canonical URL), scope (one of \"domain\", \"subdomain\", or \"url\"), and optional note for additional context.";
153
+ };
154
+ verified_no_remote_canonicals: {
155
+ type: string;
156
+ description: "Mark that this server has been verified to have no remote canonical URLs (true = verified no remote canonicals exist, false = reset/canonicals found)";
153
157
  };
154
158
  internal_notes: {
155
159
  type: string;
@@ -26,7 +26,9 @@ const PARAM_DESCRIPTIONS = {
26
26
  // Remote endpoints
27
27
  remote: 'Array of remote endpoint configurations for MCP servers. Each remote can have: id (existing remote ID or blank for new), url_direct, url_setup, transport (e.g., "sse"), host_platform (e.g., "smithery"), host_infrastructure (e.g., "cloudflare"), authentication_method (e.g., "open"), cost (e.g., "free"), status (defaults to "live"), display_name, and internal_notes.',
28
28
  // Canonical URLs
29
- canonical: 'Array of canonical URL configurations. Each entry must have: url (the canonical URL), scope (one of "domain", "subdomain", "subfolder", or "url"), and optional note for additional context.',
29
+ canonical: 'Array of canonical URL configurations. Each entry must have: url (the canonical URL), scope (one of "domain", "subdomain", or "url"), and optional note for additional context.',
30
+ // Flags
31
+ verified_no_remote_canonicals: 'Mark that this server has been verified to have no remote canonical URLs (true = verified no remote canonicals exist, false = reset/canonicals found)',
30
32
  // Other fields
31
33
  internal_notes: 'Admin-only notes. Not displayed publicly. Used for tracking submission sources, reviewer comments, etc.',
32
34
  };
@@ -83,11 +85,16 @@ const SaveMCPImplementationSchema = z.object({
83
85
  canonical: z
84
86
  .array(z.object({
85
87
  url: z.string(),
86
- scope: z.enum(['domain', 'subdomain', 'subfolder', 'url']),
88
+ scope: z.enum(['domain', 'subdomain', 'url']),
87
89
  note: z.string().optional(),
88
90
  }))
89
91
  .optional()
90
92
  .describe(PARAM_DESCRIPTIONS.canonical),
93
+ // Flags
94
+ verified_no_remote_canonicals: z
95
+ .boolean()
96
+ .optional()
97
+ .describe(PARAM_DESCRIPTIONS.verified_no_remote_canonicals),
91
98
  // Other fields
92
99
  internal_notes: z.string().optional().describe(PARAM_DESCRIPTIONS.internal_notes),
93
100
  });
@@ -294,13 +301,18 @@ Use cases:
294
301
  type: 'object',
295
302
  properties: {
296
303
  url: { type: 'string' },
297
- scope: { type: 'string', enum: ['domain', 'subdomain', 'subfolder', 'url'] },
304
+ scope: { type: 'string', enum: ['domain', 'subdomain', 'url'] },
298
305
  note: { type: 'string' },
299
306
  },
300
307
  required: ['url', 'scope'],
301
308
  },
302
309
  description: PARAM_DESCRIPTIONS.canonical,
303
310
  },
311
+ // Flags
312
+ verified_no_remote_canonicals: {
313
+ type: 'boolean',
314
+ description: PARAM_DESCRIPTIONS.verified_no_remote_canonicals,
315
+ },
304
316
  // Other fields
305
317
  internal_notes: {
306
318
  type: 'string',
@@ -88,6 +88,10 @@ export declare function updateMCPServer(_server: Server, clientFactory: ClientFa
88
88
  type: string;
89
89
  description: "Mark this server as recommended by PulseMCP";
90
90
  };
91
+ verified_no_remote_canonicals: {
92
+ type: string;
93
+ description: "Mark that this server has been verified to have no remote canonical URLs (true = verified no remote canonicals exist, false = reset/canonicals found)";
94
+ };
91
95
  created_on_override: {
92
96
  type: string;
93
97
  description: "Override the automatically derived created date (ISO date string, e.g., \"2025-01-15\")";
@@ -16,6 +16,7 @@ const PARAM_DESCRIPTIONS = {
16
16
  package_registry: 'Package registry: npm, pypi, cargo, etc.',
17
17
  package_name: 'Package name on the registry (e.g., "@modelcontextprotocol/server-filesystem")',
18
18
  recommended: 'Mark this server as recommended by PulseMCP',
19
+ verified_no_remote_canonicals: 'Mark that this server has been verified to have no remote canonical URLs (true = verified no remote canonicals exist, false = reset/canonicals found)',
19
20
  created_on_override: 'Override the automatically derived created date (ISO date string, e.g., "2025-01-15")',
20
21
  tags: 'Tags for the server. Replaces all existing tags when provided. Use tag slugs.',
21
22
  canonical_urls: 'Authoritative URLs for the server. Replaces all existing canonical URLs when provided.',
@@ -25,8 +26,8 @@ const PARAM_DESCRIPTIONS = {
25
26
  const CanonicalUrlSchema = z.object({
26
27
  url: z.string().describe('The canonical URL'),
27
28
  scope: z
28
- .enum(['domain', 'subdomain', 'subfolder', 'url'])
29
- .describe('Scope of the canonical: domain, subdomain, subfolder, or url (exact match)'),
29
+ .enum(['domain', 'subdomain', 'url'])
30
+ .describe('Scope of the canonical: domain, subdomain, or url (exact match)'),
30
31
  note: z.string().optional().describe('Optional note about this canonical URL'),
31
32
  });
32
33
  const RemoteEndpointSchema = z.object({
@@ -82,6 +83,10 @@ const UpdateMCPServerSchema = z.object({
82
83
  package_registry: z.string().optional().describe(PARAM_DESCRIPTIONS.package_registry),
83
84
  package_name: z.string().optional().describe(PARAM_DESCRIPTIONS.package_name),
84
85
  recommended: z.boolean().optional().describe(PARAM_DESCRIPTIONS.recommended),
86
+ verified_no_remote_canonicals: z
87
+ .boolean()
88
+ .optional()
89
+ .describe(PARAM_DESCRIPTIONS.verified_no_remote_canonicals),
85
90
  created_on_override: z.string().optional().describe(PARAM_DESCRIPTIONS.created_on_override),
86
91
  tags: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.tags),
87
92
  canonical_urls: z
@@ -126,7 +131,7 @@ Providing canonical_urls replaces ALL existing canonical URLs:
126
131
  {
127
132
  "implementation_id": 456,
128
133
  "canonical_urls": [
129
- { "url": "https://github.com/org/repo", "scope": "subfolder" },
134
+ { "url": "https://github.com/org/repo", "scope": "domain" },
130
135
  { "url": "https://npmjs.com/package/name", "scope": "url" }
131
136
  ]
132
137
  }
@@ -220,6 +225,10 @@ Create new provider:
220
225
  package_registry: { type: 'string', description: PARAM_DESCRIPTIONS.package_registry },
221
226
  package_name: { type: 'string', description: PARAM_DESCRIPTIONS.package_name },
222
227
  recommended: { type: 'boolean', description: PARAM_DESCRIPTIONS.recommended },
228
+ verified_no_remote_canonicals: {
229
+ type: 'boolean',
230
+ description: PARAM_DESCRIPTIONS.verified_no_remote_canonicals,
231
+ },
223
232
  created_on_override: {
224
233
  type: 'string',
225
234
  description: PARAM_DESCRIPTIONS.created_on_override,
@@ -237,7 +246,7 @@ Create new provider:
237
246
  url: { type: 'string', description: 'The canonical URL' },
238
247
  scope: {
239
248
  type: 'string',
240
- enum: ['domain', 'subdomain', 'subfolder', 'url'],
249
+ enum: ['domain', 'subdomain', 'url'],
241
250
  description: 'Scope of the canonical',
242
251
  },
243
252
  note: { type: 'string', description: 'Optional note' },
@@ -318,6 +327,9 @@ Create new provider:
318
327
  if (server.recommended !== undefined) {
319
328
  content += `**Recommended:** ${server.recommended ? 'Yes' : 'No'}\n`;
320
329
  }
330
+ if (server.verified_no_remote_canonicals !== undefined) {
331
+ content += `**Verified No Remote Canonicals:** ${server.verified_no_remote_canonicals ? 'Yes' : 'No'}\n`;
332
+ }
321
333
  if (server.updated_at) {
322
334
  content += `**Updated:** ${server.updated_at}\n`;
323
335
  }
package/shared/tools.d.ts CHANGED
@@ -23,7 +23,7 @@ import { ClientFactory } from './server.js';
23
23
  * - mcp_servers / mcp_servers_readonly: Unified MCP server tools (abstracted interface)
24
24
  * - redirects / redirects_readonly: URL redirect management tools
25
25
  * - good_jobs / good_jobs_readonly: GoodJob background job management tools
26
- * - proctor / proctor_readonly: Proctor exam execution and result storage tools. The readonly variant includes get_exam_result for retrieving stored results without running exams or saving
26
+ * - proctor / proctor_readonly: Proctor exam execution and result storage tools. The readonly variant includes get_exam_result and list_proctor_runs for retrieving stored results without running exams or saving
27
27
  * - discovered_urls / discovered_urls_readonly: Discovered URL management tools for processing URLs into MCP implementations
28
28
  * - notifications: Notification email tools (send_impl_posted_notif). Separated from server_directory so notification capability can be granted independently.
29
29
  */
@@ -69,7 +69,7 @@ export declare function parseEnabledToolGroups(enabledGroupsParam?: string): Too
69
69
  * - good_jobs: GoodJob background job management tools (read + write)
70
70
  * - good_jobs_readonly: GoodJob tools (read only)
71
71
  * - proctor: Proctor exam execution and result storage tools (read + write)
72
- * - proctor_readonly: Proctor tools (read only - get_exam_result for retrieving stored results)
72
+ * - proctor_readonly: Proctor tools (read only - get_exam_result and list_proctor_runs)
73
73
  * - discovered_urls: Discovered URL management tools for processing URLs into MCP implementations (read + write)
74
74
  * - discovered_urls_readonly: Discovered URL tools (read only - list and stats)
75
75
  * - notifications: Notification email tools - send_impl_posted_notif (write-only, no readonly variant)
package/shared/tools.js CHANGED
@@ -60,6 +60,7 @@ import { cleanupGoodJobs } from './tools/cleanup-good-jobs.js';
60
60
  import { runExamForMirror } from './tools/run-exam-for-mirror.js';
61
61
  import { getExamResult } from './tools/get-exam-result.js';
62
62
  import { saveResultsForMirror } from './tools/save-results-for-mirror.js';
63
+ import { listProctorRuns } from './tools/list-proctor-runs.js';
63
64
  // Discovered URLs tools
64
65
  import { listDiscoveredUrls } from './tools/list-discovered-urls.js';
65
66
  import { markDiscoveredUrlProcessed } from './tools/mark-discovered-url-processed.js';
@@ -230,6 +231,7 @@ const ALL_TOOLS = [
230
231
  { factory: runExamForMirror, groups: ['proctor'], isWriteOperation: true },
231
232
  { factory: getExamResult, groups: ['proctor'], isWriteOperation: false },
232
233
  { factory: saveResultsForMirror, groups: ['proctor'], isWriteOperation: true },
234
+ { factory: listProctorRuns, groups: ['proctor'], isWriteOperation: false },
233
235
  // Discovered URLs tools
234
236
  { factory: listDiscoveredUrls, groups: ['discovered_urls'], isWriteOperation: false },
235
237
  {
@@ -367,7 +369,7 @@ function shouldIncludeTool(toolDef, enabledGroups) {
367
369
  * - good_jobs: GoodJob background job management tools (read + write)
368
370
  * - good_jobs_readonly: GoodJob tools (read only)
369
371
  * - proctor: Proctor exam execution and result storage tools (read + write)
370
- * - proctor_readonly: Proctor tools (read only - get_exam_result for retrieving stored results)
372
+ * - proctor_readonly: Proctor tools (read only - get_exam_result and list_proctor_runs)
371
373
  * - discovered_urls: Discovered URL management tools for processing URLs into MCP implementations (read + write)
372
374
  * - discovered_urls_readonly: Discovered URL tools (read only - list and stats)
373
375
  * - notifications: Notification email tools - send_impl_posted_notif (write-only, no readonly variant)
package/shared/types.d.ts CHANGED
@@ -105,7 +105,7 @@ export interface RemoteEndpointParams {
105
105
  }
106
106
  export interface CanonicalUrlParams {
107
107
  url: string;
108
- scope: 'domain' | 'subdomain' | 'subfolder' | 'url';
108
+ scope: 'domain' | 'subdomain' | 'url';
109
109
  note?: string;
110
110
  }
111
111
  export interface MCPServer {
@@ -124,6 +124,8 @@ export interface MCPServer {
124
124
  downloads_estimate_last_four_weeks?: number;
125
125
  visitors_estimate_total?: number;
126
126
  mcp_server_remotes_count?: number;
127
+ recommended?: boolean;
128
+ verified_no_remote_canonicals?: boolean;
127
129
  tags?: MCPServerTag[];
128
130
  remotes?: MCPServerRemote[];
129
131
  }
@@ -203,6 +205,7 @@ export interface SaveMCPImplementationParams {
203
205
  package_registry?: string;
204
206
  package_name?: string;
205
207
  recommended?: boolean;
208
+ verified_no_remote_canonicals?: boolean;
206
209
  created_on_override?: string;
207
210
  tags?: string[];
208
211
  remote?: RemoteEndpointParams[];
@@ -238,6 +241,7 @@ export interface CreateMCPImplementationParams {
238
241
  package_registry?: string;
239
242
  package_name?: string;
240
243
  recommended?: boolean;
244
+ verified_no_remote_canonicals?: boolean;
241
245
  created_on_override?: string;
242
246
  tags?: string[];
243
247
  remote?: RemoteEndpointParams[];
@@ -515,6 +519,7 @@ export interface UnifiedMCPServer {
515
519
  package_registry?: string;
516
520
  package_name?: string;
517
521
  recommended?: boolean;
522
+ verified_no_remote_canonicals?: boolean;
518
523
  canonical_urls?: CanonicalUrl[];
519
524
  remotes?: RemoteEndpoint[];
520
525
  tags?: MCPServerTag[];
@@ -564,6 +569,7 @@ export interface UpdateUnifiedMCPServerParams {
564
569
  package_registry?: string;
565
570
  package_name?: string;
566
571
  recommended?: boolean;
572
+ verified_no_remote_canonicals?: boolean;
567
573
  created_on_override?: string;
568
574
  tags?: string[];
569
575
  canonical_urls?: CanonicalUrl[];
@@ -691,6 +697,43 @@ export interface ProctorSaveResultsResponse {
691
697
  error: string;
692
698
  }>;
693
699
  }
700
+ export interface ProctorRun {
701
+ id: number;
702
+ slug: string;
703
+ name: string | null;
704
+ recommended: boolean;
705
+ mirrors_count: number;
706
+ tenant_count: number;
707
+ latest_version: string | null;
708
+ latest_mirror_id: number | null;
709
+ latest_mirror_name: string | null;
710
+ latest_tested: boolean;
711
+ last_auth_check_days: number | null;
712
+ last_tools_list_days: number | null;
713
+ auth_types: string[];
714
+ num_tools: number | null;
715
+ packages: string[];
716
+ remotes: string[];
717
+ }
718
+ export interface ProctorRunsResponse {
719
+ runs: ProctorRun[];
720
+ pagination?: {
721
+ current_page: number;
722
+ total_pages: number;
723
+ total_count: number;
724
+ has_next?: boolean;
725
+ limit?: number;
726
+ };
727
+ }
728
+ export interface GetProctorRunsParams {
729
+ q?: string;
730
+ recommended?: boolean;
731
+ tenant_ids?: string;
732
+ sort?: 'slug' | 'name' | 'mirrors' | 'recommended' | 'tenants' | 'latest_tested' | 'last_auth_check' | 'last_tools_list';
733
+ direction?: 'asc' | 'desc';
734
+ limit?: number;
735
+ offset?: number;
736
+ }
694
737
  export type DiscoveredUrlResult = 'posted' | 'skipped' | 'rejected' | 'error';
695
738
  export interface DiscoveredUrl {
696
739
  id: number;