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

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 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)
@@ -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
+ }
@@ -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
  {
@@ -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,7 @@ 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
30
  // Other fields
31
31
  internal_notes: 'Admin-only notes. Not displayed publicly. Used for tracking submission sources, reviewer comments, etc.',
32
32
  };
@@ -83,7 +83,7 @@ const SaveMCPImplementationSchema = z.object({
83
83
  canonical: z
84
84
  .array(z.object({
85
85
  url: z.string(),
86
- scope: z.enum(['domain', 'subdomain', 'subfolder', 'url']),
86
+ scope: z.enum(['domain', 'subdomain', 'url']),
87
87
  note: z.string().optional(),
88
88
  }))
89
89
  .optional()
@@ -294,7 +294,7 @@ Use cases:
294
294
  type: 'object',
295
295
  properties: {
296
296
  url: { type: 'string' },
297
- scope: { type: 'string', enum: ['domain', 'subdomain', 'subfolder', 'url'] },
297
+ scope: { type: 'string', enum: ['domain', 'subdomain', 'url'] },
298
298
  note: { type: 'string' },
299
299
  },
300
300
  required: ['url', 'scope'],
@@ -25,8 +25,8 @@ const PARAM_DESCRIPTIONS = {
25
25
  const CanonicalUrlSchema = z.object({
26
26
  url: z.string().describe('The canonical URL'),
27
27
  scope: z
28
- .enum(['domain', 'subdomain', 'subfolder', 'url'])
29
- .describe('Scope of the canonical: domain, subdomain, subfolder, or url (exact match)'),
28
+ .enum(['domain', 'subdomain', 'url'])
29
+ .describe('Scope of the canonical: domain, subdomain, or url (exact match)'),
30
30
  note: z.string().optional().describe('Optional note about this canonical URL'),
31
31
  });
32
32
  const RemoteEndpointSchema = z.object({
@@ -126,7 +126,7 @@ Providing canonical_urls replaces ALL existing canonical URLs:
126
126
  {
127
127
  "implementation_id": 456,
128
128
  "canonical_urls": [
129
- { "url": "https://github.com/org/repo", "scope": "subfolder" },
129
+ { "url": "https://github.com/org/repo", "scope": "domain" },
130
130
  { "url": "https://npmjs.com/package/name", "scope": "url" }
131
131
  ]
132
132
  }
@@ -237,7 +237,7 @@ Create new provider:
237
237
  url: { type: 'string', description: 'The canonical URL' },
238
238
  scope: {
239
239
  type: 'string',
240
- enum: ['domain', 'subdomain', 'subfolder', 'url'],
240
+ enum: ['domain', 'subdomain', 'url'],
241
241
  description: 'Scope of the canonical',
242
242
  },
243
243
  note: { type: 'string', description: 'Optional note' },
@@ -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.1",
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",
@@ -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
+ }
@@ -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
  {
@@ -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,7 @@ 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
153
  };
154
154
  internal_notes: {
155
155
  type: string;
@@ -26,7 +26,7 @@ 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
30
  // Other fields
31
31
  internal_notes: 'Admin-only notes. Not displayed publicly. Used for tracking submission sources, reviewer comments, etc.',
32
32
  };
@@ -83,7 +83,7 @@ const SaveMCPImplementationSchema = z.object({
83
83
  canonical: z
84
84
  .array(z.object({
85
85
  url: z.string(),
86
- scope: z.enum(['domain', 'subdomain', 'subfolder', 'url']),
86
+ scope: z.enum(['domain', 'subdomain', 'url']),
87
87
  note: z.string().optional(),
88
88
  }))
89
89
  .optional()
@@ -294,7 +294,7 @@ Use cases:
294
294
  type: 'object',
295
295
  properties: {
296
296
  url: { type: 'string' },
297
- scope: { type: 'string', enum: ['domain', 'subdomain', 'subfolder', 'url'] },
297
+ scope: { type: 'string', enum: ['domain', 'subdomain', 'url'] },
298
298
  note: { type: 'string' },
299
299
  },
300
300
  required: ['url', 'scope'],
@@ -25,8 +25,8 @@ const PARAM_DESCRIPTIONS = {
25
25
  const CanonicalUrlSchema = z.object({
26
26
  url: z.string().describe('The canonical URL'),
27
27
  scope: z
28
- .enum(['domain', 'subdomain', 'subfolder', 'url'])
29
- .describe('Scope of the canonical: domain, subdomain, subfolder, or url (exact match)'),
28
+ .enum(['domain', 'subdomain', 'url'])
29
+ .describe('Scope of the canonical: domain, subdomain, or url (exact match)'),
30
30
  note: z.string().optional().describe('Optional note about this canonical URL'),
31
31
  });
32
32
  const RemoteEndpointSchema = z.object({
@@ -126,7 +126,7 @@ Providing canonical_urls replaces ALL existing canonical URLs:
126
126
  {
127
127
  "implementation_id": 456,
128
128
  "canonical_urls": [
129
- { "url": "https://github.com/org/repo", "scope": "subfolder" },
129
+ { "url": "https://github.com/org/repo", "scope": "domain" },
130
130
  { "url": "https://npmjs.com/package/name", "scope": "url" }
131
131
  ]
132
132
  }
@@ -237,7 +237,7 @@ Create new provider:
237
237
  url: { type: 'string', description: 'The canonical URL' },
238
238
  scope: {
239
239
  type: 'string',
240
- enum: ['domain', 'subdomain', 'subfolder', 'url'],
240
+ enum: ['domain', 'subdomain', 'url'],
241
241
  description: 'Scope of the canonical',
242
242
  },
243
243
  note: { type: 'string', description: 'Optional note' },
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 {
@@ -691,6 +691,43 @@ export interface ProctorSaveResultsResponse {
691
691
  error: string;
692
692
  }>;
693
693
  }
694
+ export interface ProctorRun {
695
+ id: number;
696
+ slug: string;
697
+ name: string | null;
698
+ recommended: boolean;
699
+ mirrors_count: number;
700
+ tenant_count: number;
701
+ latest_version: string | null;
702
+ latest_mirror_id: number | null;
703
+ latest_mirror_name: string | null;
704
+ latest_tested: boolean;
705
+ last_auth_check_days: number | null;
706
+ last_tools_list_days: number | null;
707
+ auth_types: string[];
708
+ num_tools: number | null;
709
+ packages: string[];
710
+ remotes: string[];
711
+ }
712
+ export interface ProctorRunsResponse {
713
+ runs: ProctorRun[];
714
+ pagination?: {
715
+ current_page: number;
716
+ total_pages: number;
717
+ total_count: number;
718
+ has_next?: boolean;
719
+ limit?: number;
720
+ };
721
+ }
722
+ export interface GetProctorRunsParams {
723
+ q?: string;
724
+ recommended?: boolean;
725
+ tenant_ids?: string;
726
+ sort?: 'slug' | 'name' | 'mirrors' | 'recommended' | 'tenants' | 'latest_tested' | 'last_auth_check' | 'last_tools_list';
727
+ direction?: 'asc' | 'desc';
728
+ limit?: number;
729
+ offset?: number;
730
+ }
694
731
  export type DiscoveredUrlResult = 'posted' | 'skipped' | 'rejected' | 'error';
695
732
  export interface DiscoveredUrl {
696
733
  id: number;