proctor-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +247 -0
  2. package/build/index.integration-with-mock.js +143 -0
  3. package/build/index.js +57 -0
  4. package/package.json +43 -0
  5. package/shared/index.d.ts +7 -0
  6. package/shared/index.js +4 -0
  7. package/shared/logging.d.ts +20 -0
  8. package/shared/logging.js +34 -0
  9. package/shared/proctor-client/lib/cancel-exam.d.ts +6 -0
  10. package/shared/proctor-client/lib/cancel-exam.js +36 -0
  11. package/shared/proctor-client/lib/destroy-machine.d.ts +7 -0
  12. package/shared/proctor-client/lib/destroy-machine.js +31 -0
  13. package/shared/proctor-client/lib/get-machines.d.ts +6 -0
  14. package/shared/proctor-client/lib/get-machines.js +27 -0
  15. package/shared/proctor-client/lib/get-metadata.d.ts +6 -0
  16. package/shared/proctor-client/lib/get-metadata.js +23 -0
  17. package/shared/proctor-client/lib/get-prior-result.d.ts +6 -0
  18. package/shared/proctor-client/lib/get-prior-result.js +35 -0
  19. package/shared/proctor-client/lib/run-exam.d.ts +7 -0
  20. package/shared/proctor-client/lib/run-exam.js +90 -0
  21. package/shared/proctor-client/lib/save-result.d.ts +6 -0
  22. package/shared/proctor-client/lib/save-result.js +42 -0
  23. package/shared/server.d.ts +66 -0
  24. package/shared/server.js +65 -0
  25. package/shared/tools/cancel-exam.d.ts +34 -0
  26. package/shared/tools/cancel-exam.js +99 -0
  27. package/shared/tools/destroy-machine.d.ts +30 -0
  28. package/shared/tools/destroy-machine.js +75 -0
  29. package/shared/tools/get-machines.d.ts +25 -0
  30. package/shared/tools/get-machines.js +83 -0
  31. package/shared/tools/get-metadata.d.ts +25 -0
  32. package/shared/tools/get-metadata.js +63 -0
  33. package/shared/tools/get-prior-result.d.ts +38 -0
  34. package/shared/tools/get-prior-result.js +106 -0
  35. package/shared/tools/run-exam.d.ts +58 -0
  36. package/shared/tools/run-exam.js +189 -0
  37. package/shared/tools/save-result.d.ts +52 -0
  38. package/shared/tools/save-result.js +122 -0
  39. package/shared/tools.d.ts +44 -0
  40. package/shared/tools.js +128 -0
  41. package/shared/types.d.ts +151 -0
  42. package/shared/types.js +4 -0
@@ -0,0 +1,122 @@
1
+ import { z } from 'zod';
2
+ // Parameter descriptions - single source of truth
3
+ const PARAM_DESCRIPTIONS = {
4
+ runtime_id: 'Runtime ID used for the exam, or "__custom__" if a custom Docker image was used.',
5
+ exam_id: 'Exam ID that was executed.',
6
+ mcp_server_slug: 'Slug of the MCP server that was tested.',
7
+ mirror_id: 'ID of the unofficial mirror associated with this test.',
8
+ results: 'Exam results as a JSON string or object. This is the full result from run_exam.',
9
+ custom_runtime_image: 'Required if runtime_id is "__custom__". The Docker image URL that was used.',
10
+ };
11
+ const SaveResultSchema = z.object({
12
+ runtime_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.runtime_id),
13
+ exam_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.exam_id),
14
+ mcp_server_slug: z.string().min(1).describe(PARAM_DESCRIPTIONS.mcp_server_slug),
15
+ mirror_id: z.number().describe(PARAM_DESCRIPTIONS.mirror_id),
16
+ results: z.union([z.string(), z.record(z.unknown())]).describe(PARAM_DESCRIPTIONS.results),
17
+ custom_runtime_image: z.string().optional().describe(PARAM_DESCRIPTIONS.custom_runtime_image),
18
+ });
19
+ export function saveResult(_server, clientFactory) {
20
+ return {
21
+ name: 'save_result',
22
+ description: `Save exam results to the database for future comparison.
23
+
24
+ Stores the results of a Proctor exam run so they can be retrieved later for
25
+ comparison with new test runs.
26
+
27
+ **Returns:**
28
+ - success: boolean indicating if the save was successful
29
+ - id: ID of the saved result record
30
+
31
+ **Use cases:**
32
+ - Persist exam results after running tests
33
+ - Create a baseline for future comparisons
34
+ - Track test history for an MCP server
35
+ - Enable regression testing by comparing against prior results
36
+
37
+ **Note:**
38
+ - The mirror_id must be a valid unofficial mirror ID
39
+ - Results should be the full output from run_exam
40
+ - Custom runtime images require the custom_runtime_image parameter`,
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ runtime_id: {
45
+ type: 'string',
46
+ description: PARAM_DESCRIPTIONS.runtime_id,
47
+ },
48
+ exam_id: {
49
+ type: 'string',
50
+ description: PARAM_DESCRIPTIONS.exam_id,
51
+ },
52
+ mcp_server_slug: {
53
+ type: 'string',
54
+ description: PARAM_DESCRIPTIONS.mcp_server_slug,
55
+ },
56
+ mirror_id: {
57
+ type: 'number',
58
+ description: PARAM_DESCRIPTIONS.mirror_id,
59
+ },
60
+ results: {
61
+ oneOf: [{ type: 'string' }, { type: 'object' }],
62
+ description: PARAM_DESCRIPTIONS.results,
63
+ },
64
+ custom_runtime_image: {
65
+ type: 'string',
66
+ description: PARAM_DESCRIPTIONS.custom_runtime_image,
67
+ },
68
+ },
69
+ required: ['runtime_id', 'exam_id', 'mcp_server_slug', 'mirror_id', 'results'],
70
+ },
71
+ handler: async (args) => {
72
+ const validatedArgs = SaveResultSchema.parse(args);
73
+ // Validate custom runtime requirements
74
+ if (validatedArgs.runtime_id === '__custom__' && !validatedArgs.custom_runtime_image) {
75
+ return {
76
+ content: [
77
+ {
78
+ type: 'text',
79
+ text: 'Error: custom_runtime_image is required when runtime_id is "__custom__"',
80
+ },
81
+ ],
82
+ isError: true,
83
+ };
84
+ }
85
+ const client = clientFactory();
86
+ try {
87
+ const response = await client.saveResult({
88
+ runtime_id: validatedArgs.runtime_id,
89
+ exam_id: validatedArgs.exam_id,
90
+ mcp_server_slug: validatedArgs.mcp_server_slug,
91
+ mirror_id: validatedArgs.mirror_id,
92
+ results: validatedArgs.results,
93
+ custom_runtime_image: validatedArgs.custom_runtime_image,
94
+ });
95
+ let content = '## Result Saved\n\n';
96
+ content += `**Success:** ${response.success}\n`;
97
+ content += `**Result ID:** ${response.id}\n\n`;
98
+ content +=
99
+ 'The exam result has been saved and can be retrieved for comparison using get_prior_result.';
100
+ return {
101
+ content: [
102
+ {
103
+ type: 'text',
104
+ text: content.trim(),
105
+ },
106
+ ],
107
+ };
108
+ }
109
+ catch (error) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: 'text',
114
+ text: `Error saving result: ${error instanceof Error ? error.message : String(error)}`,
115
+ },
116
+ ],
117
+ isError: true,
118
+ };
119
+ }
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,44 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ClientFactory } from './server.js';
3
+ /**
4
+ * Tool group definitions - groups of related tools that can be enabled/disabled together
5
+ *
6
+ * Each group has two variants:
7
+ * - Base group (e.g., 'exams'): Includes all tools (read + write operations)
8
+ * - Readonly group (e.g., 'exams_readonly'): Includes only read operations
9
+ *
10
+ * Groups:
11
+ * - exams / exams_readonly: Exam execution and result management tools
12
+ * - machines / machines_readonly: Fly.io machine management tools
13
+ */
14
+ export type ToolGroup = 'exams' | 'exams_readonly' | 'machines' | 'machines_readonly';
15
+ /**
16
+ * Parse enabled tool groups from environment variable or parameter
17
+ * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "exams,machines_readonly")
18
+ * @returns Array of enabled tool groups
19
+ */
20
+ export declare function parseEnabledToolGroups(enabledGroupsParam?: string): ToolGroup[];
21
+ /**
22
+ * Creates a function to register all tools with the server.
23
+ * This pattern uses individual tool files for better modularity and testability.
24
+ *
25
+ * Each tool is defined in its own file under the `tools/` directory and follows
26
+ * a factory pattern that accepts the server and clientFactory as parameters.
27
+ *
28
+ * Tool groups can be enabled/disabled via the TOOL_GROUPS environment variable
29
+ * (comma-separated list, e.g., "exams,machines_readonly"). If not set, all
30
+ * base tool groups are enabled by default (full read+write access).
31
+ *
32
+ * Available tool groups:
33
+ * - exams: All exam-related tools (read + write)
34
+ * - exams_readonly: Exam tools (read only - get_proctor_metadata, get_prior_result)
35
+ * - machines: All machine management tools (read + write)
36
+ * - machines_readonly: Machine tools (read only - get_machines)
37
+ *
38
+ * @param clientFactory - Factory function that creates client instances
39
+ * @param enabledGroups - Optional comma-separated list of enabled tool groups (overrides env var)
40
+ * @returns Function that registers all tools with a server
41
+ */
42
+ export declare function createRegisterTools(clientFactory: ClientFactory, enabledGroups?: string): (server: Server) => void;
43
+ export declare function registerTools(server: Server): void;
44
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ import { getMetadata } from './tools/get-metadata.js';
3
+ import { runExam } from './tools/run-exam.js';
4
+ import { saveResult } from './tools/save-result.js';
5
+ import { getPriorResult } from './tools/get-prior-result.js';
6
+ import { getMachines } from './tools/get-machines.js';
7
+ import { destroyMachine } from './tools/destroy-machine.js';
8
+ import { cancelExam } from './tools/cancel-exam.js';
9
+ const ALL_TOOLS = [
10
+ // Exam tools
11
+ { factory: getMetadata, group: 'exams', isWriteOperation: false },
12
+ { factory: runExam, group: 'exams', isWriteOperation: true },
13
+ { factory: saveResult, group: 'exams', isWriteOperation: true },
14
+ { factory: getPriorResult, group: 'exams', isWriteOperation: false },
15
+ // Machine management tools
16
+ { factory: getMachines, group: 'machines', isWriteOperation: false },
17
+ { factory: destroyMachine, group: 'machines', isWriteOperation: true },
18
+ { factory: cancelExam, group: 'machines', isWriteOperation: true },
19
+ ];
20
+ /**
21
+ * All valid tool groups (base groups and their _readonly variants)
22
+ */
23
+ const VALID_TOOL_GROUPS = ['exams', 'exams_readonly', 'machines', 'machines_readonly'];
24
+ /**
25
+ * Base groups (without _readonly suffix) - used for default "all groups" behavior
26
+ */
27
+ const BASE_TOOL_GROUPS = ['exams', 'machines'];
28
+ /**
29
+ * Parse enabled tool groups from environment variable or parameter
30
+ * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "exams,machines_readonly")
31
+ * @returns Array of enabled tool groups
32
+ */
33
+ export function parseEnabledToolGroups(enabledGroupsParam) {
34
+ const groupsStr = enabledGroupsParam || process.env.TOOL_GROUPS || '';
35
+ if (!groupsStr) {
36
+ // Default: all base groups enabled (full read+write access)
37
+ return [...BASE_TOOL_GROUPS];
38
+ }
39
+ const groups = groupsStr.split(',').map((g) => g.trim());
40
+ const validGroups = [];
41
+ for (const group of groups) {
42
+ if (VALID_TOOL_GROUPS.includes(group) &&
43
+ !validGroups.includes(group)) {
44
+ validGroups.push(group);
45
+ }
46
+ else if (!VALID_TOOL_GROUPS.includes(group)) {
47
+ console.warn(`Unknown tool group: ${group}`);
48
+ }
49
+ }
50
+ return validGroups;
51
+ }
52
+ /**
53
+ * Check if a tool should be included based on enabled groups
54
+ * @param toolDef - The tool definition to check
55
+ * @param enabledGroups - Array of enabled tool groups
56
+ * @returns true if the tool should be included
57
+ */
58
+ function shouldIncludeTool(toolDef, enabledGroups) {
59
+ const baseGroup = toolDef.group;
60
+ const readonlyGroup = `${baseGroup}_readonly`;
61
+ // Check if the base group (full access) is enabled
62
+ if (enabledGroups.includes(baseGroup)) {
63
+ return true;
64
+ }
65
+ // Check if the readonly group is enabled (only include read operations)
66
+ if (enabledGroups.includes(readonlyGroup) && !toolDef.isWriteOperation) {
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ /**
72
+ * Creates a function to register all tools with the server.
73
+ * This pattern uses individual tool files for better modularity and testability.
74
+ *
75
+ * Each tool is defined in its own file under the `tools/` directory and follows
76
+ * a factory pattern that accepts the server and clientFactory as parameters.
77
+ *
78
+ * Tool groups can be enabled/disabled via the TOOL_GROUPS environment variable
79
+ * (comma-separated list, e.g., "exams,machines_readonly"). If not set, all
80
+ * base tool groups are enabled by default (full read+write access).
81
+ *
82
+ * Available tool groups:
83
+ * - exams: All exam-related tools (read + write)
84
+ * - exams_readonly: Exam tools (read only - get_proctor_metadata, get_prior_result)
85
+ * - machines: All machine management tools (read + write)
86
+ * - machines_readonly: Machine tools (read only - get_machines)
87
+ *
88
+ * @param clientFactory - Factory function that creates client instances
89
+ * @param enabledGroups - Optional comma-separated list of enabled tool groups (overrides env var)
90
+ * @returns Function that registers all tools with a server
91
+ */
92
+ export function createRegisterTools(clientFactory, enabledGroups) {
93
+ return (server) => {
94
+ const enabledToolGroups = parseEnabledToolGroups(enabledGroups);
95
+ // Filter tools based on enabled groups
96
+ const enabledTools = ALL_TOOLS.filter((toolDef) => shouldIncludeTool(toolDef, enabledToolGroups));
97
+ // Create tool instances
98
+ const tools = enabledTools.map((toolDef) => toolDef.factory(server, clientFactory));
99
+ // List available tools
100
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
101
+ return {
102
+ tools: tools.map((tool) => ({
103
+ name: tool.name,
104
+ description: tool.description,
105
+ inputSchema: tool.inputSchema,
106
+ })),
107
+ };
108
+ });
109
+ // Handle tool calls
110
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
111
+ const { name, arguments: args } = request.params;
112
+ const tool = tools.find((t) => t.name === name);
113
+ if (!tool) {
114
+ throw new Error(`Unknown tool: ${name}`);
115
+ }
116
+ return await tool.handler(args);
117
+ });
118
+ };
119
+ }
120
+ // Keep the original registerTools for backward compatibility
121
+ export function registerTools(server) {
122
+ // This maintains compatibility but doesn't use dependency injection
123
+ const factory = () => {
124
+ throw new Error('No client factory provided - use createRegisterTools for dependency injection');
125
+ };
126
+ const register = createRegisterTools(factory);
127
+ register(server);
128
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Type definitions for Proctor MCP Server
3
+ */
4
+ /**
5
+ * Runtime configuration for running Proctor exams
6
+ */
7
+ export interface ProctorRuntime {
8
+ id: string;
9
+ name: string;
10
+ image: string;
11
+ }
12
+ /**
13
+ * Exam configuration available in Proctor
14
+ */
15
+ export interface ProctorExam {
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ }
20
+ /**
21
+ * Metadata response from the Proctor API
22
+ */
23
+ export interface ProctorMetadataResponse {
24
+ runtimes: ProctorRuntime[];
25
+ exams: ProctorExam[];
26
+ }
27
+ /**
28
+ * Log entry from a running exam
29
+ */
30
+ export interface ExamLogEntry {
31
+ time?: string;
32
+ message?: string;
33
+ [key: string]: unknown;
34
+ }
35
+ /**
36
+ * Streaming response types from run_exam endpoint
37
+ */
38
+ export interface ExamStreamLog {
39
+ type: 'log';
40
+ data: ExamLogEntry;
41
+ }
42
+ export interface ExamStreamResult {
43
+ type: 'result';
44
+ data: ExamResult;
45
+ }
46
+ export interface ExamStreamError {
47
+ type: 'error';
48
+ data: {
49
+ error: string;
50
+ };
51
+ }
52
+ export type ExamStreamEntry = ExamStreamLog | ExamStreamResult | ExamStreamError;
53
+ /**
54
+ * Final exam result
55
+ */
56
+ export interface ExamResult {
57
+ status?: string;
58
+ input?: {
59
+ 'mcp.json'?: Record<string, unknown>;
60
+ 'server.json'?: Record<string, unknown>;
61
+ };
62
+ [key: string]: unknown;
63
+ }
64
+ /**
65
+ * Parameters for running an exam
66
+ */
67
+ export interface RunExamParams {
68
+ runtime_id: string;
69
+ exam_id: string;
70
+ mcp_config: string;
71
+ server_json?: string;
72
+ custom_runtime_image?: string;
73
+ max_retries?: number;
74
+ mcp_server_slug?: string;
75
+ mcp_json_id?: number;
76
+ }
77
+ /**
78
+ * Parameters for saving exam results
79
+ */
80
+ export interface SaveResultParams {
81
+ runtime_id: string;
82
+ exam_id: string;
83
+ mcp_server_slug: string;
84
+ mirror_id: number;
85
+ results: string | Record<string, unknown>;
86
+ custom_runtime_image?: string;
87
+ }
88
+ /**
89
+ * Response from save_result endpoint
90
+ */
91
+ export interface SaveResultResponse {
92
+ success: boolean;
93
+ id: number;
94
+ }
95
+ /**
96
+ * Parameters for getting prior results
97
+ */
98
+ export interface PriorResultParams {
99
+ mirror_id: number;
100
+ exam_id: string;
101
+ input_json?: string;
102
+ }
103
+ /**
104
+ * Response from prior_result endpoint
105
+ */
106
+ export interface PriorResultResponse {
107
+ id: number;
108
+ datetime_performed: string;
109
+ results: ExamResult;
110
+ runtime_image: string;
111
+ match_type: 'exact' | 'entry_key';
112
+ }
113
+ /**
114
+ * Fly.io machine information
115
+ */
116
+ export interface FlyMachine {
117
+ id: string;
118
+ name?: string;
119
+ state?: string;
120
+ region?: string;
121
+ created_at?: string;
122
+ [key: string]: unknown;
123
+ }
124
+ /**
125
+ * Response from machines endpoint
126
+ */
127
+ export interface MachinesResponse {
128
+ machines: FlyMachine[];
129
+ }
130
+ /**
131
+ * Parameters for canceling an exam
132
+ */
133
+ export interface CancelExamParams {
134
+ machine_id: string;
135
+ exam_id: string;
136
+ }
137
+ /**
138
+ * Response from cancel_exam endpoint
139
+ */
140
+ export interface CancelExamResponse {
141
+ success?: boolean;
142
+ message?: string;
143
+ [key: string]: unknown;
144
+ }
145
+ /**
146
+ * Error response from API
147
+ */
148
+ export interface ApiError {
149
+ error: string;
150
+ }
151
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for Proctor MCP Server
3
+ */
4
+ export {};