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.
- package/README.md +247 -0
- package/build/index.integration-with-mock.js +143 -0
- package/build/index.js +57 -0
- package/package.json +43 -0
- package/shared/index.d.ts +7 -0
- package/shared/index.js +4 -0
- package/shared/logging.d.ts +20 -0
- package/shared/logging.js +34 -0
- package/shared/proctor-client/lib/cancel-exam.d.ts +6 -0
- package/shared/proctor-client/lib/cancel-exam.js +36 -0
- package/shared/proctor-client/lib/destroy-machine.d.ts +7 -0
- package/shared/proctor-client/lib/destroy-machine.js +31 -0
- package/shared/proctor-client/lib/get-machines.d.ts +6 -0
- package/shared/proctor-client/lib/get-machines.js +27 -0
- package/shared/proctor-client/lib/get-metadata.d.ts +6 -0
- package/shared/proctor-client/lib/get-metadata.js +23 -0
- package/shared/proctor-client/lib/get-prior-result.d.ts +6 -0
- package/shared/proctor-client/lib/get-prior-result.js +35 -0
- package/shared/proctor-client/lib/run-exam.d.ts +7 -0
- package/shared/proctor-client/lib/run-exam.js +90 -0
- package/shared/proctor-client/lib/save-result.d.ts +6 -0
- package/shared/proctor-client/lib/save-result.js +42 -0
- package/shared/server.d.ts +66 -0
- package/shared/server.js +65 -0
- package/shared/tools/cancel-exam.d.ts +34 -0
- package/shared/tools/cancel-exam.js +99 -0
- package/shared/tools/destroy-machine.d.ts +30 -0
- package/shared/tools/destroy-machine.js +75 -0
- package/shared/tools/get-machines.d.ts +25 -0
- package/shared/tools/get-machines.js +83 -0
- package/shared/tools/get-metadata.d.ts +25 -0
- package/shared/tools/get-metadata.js +63 -0
- package/shared/tools/get-prior-result.d.ts +38 -0
- package/shared/tools/get-prior-result.js +106 -0
- package/shared/tools/run-exam.d.ts +58 -0
- package/shared/tools/run-exam.js +189 -0
- package/shared/tools/save-result.d.ts +52 -0
- package/shared/tools/save-result.js +122 -0
- package/shared/tools.d.ts +44 -0
- package/shared/tools.js +128 -0
- package/shared/types.d.ts +151 -0
- package/shared/types.js +4 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProctorMetadataResponse } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Get available runtimes and exams from the Proctor API
|
|
4
|
+
*/
|
|
5
|
+
export declare function getMetadata(apiKey: string, baseUrl: string): Promise<ProctorMetadataResponse>;
|
|
6
|
+
//# sourceMappingURL=get-metadata.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get available runtimes and exams from the Proctor API
|
|
3
|
+
*/
|
|
4
|
+
export async function getMetadata(apiKey, baseUrl) {
|
|
5
|
+
const url = new URL('/api/proctor/metadata', baseUrl);
|
|
6
|
+
const response = await fetch(url.toString(), {
|
|
7
|
+
method: 'GET',
|
|
8
|
+
headers: {
|
|
9
|
+
'X-API-Key': apiKey,
|
|
10
|
+
Accept: 'application/json',
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
if (response.status === 401) {
|
|
15
|
+
throw new Error('Invalid API key');
|
|
16
|
+
}
|
|
17
|
+
if (response.status === 403) {
|
|
18
|
+
throw new Error('User lacks admin privileges or insufficient permissions');
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Failed to get metadata: ${response.status} ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
return (await response.json());
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PriorResultParams, PriorResultResponse } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Get a prior exam result for comparison
|
|
4
|
+
*/
|
|
5
|
+
export declare function getPriorResult(apiKey: string, baseUrl: string, params: PriorResultParams): Promise<PriorResultResponse>;
|
|
6
|
+
//# sourceMappingURL=get-prior-result.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get a prior exam result for comparison
|
|
3
|
+
*/
|
|
4
|
+
export async function getPriorResult(apiKey, baseUrl, params) {
|
|
5
|
+
const url = new URL('/api/proctor/prior_result', baseUrl);
|
|
6
|
+
url.searchParams.append('mirror_id', String(params.mirror_id));
|
|
7
|
+
url.searchParams.append('exam_id', params.exam_id);
|
|
8
|
+
if (params.input_json) {
|
|
9
|
+
url.searchParams.append('input_json', params.input_json);
|
|
10
|
+
}
|
|
11
|
+
const response = await fetch(url.toString(), {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
headers: {
|
|
14
|
+
'X-API-Key': apiKey,
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
if (response.status === 401) {
|
|
20
|
+
throw new Error('Invalid API key');
|
|
21
|
+
}
|
|
22
|
+
if (response.status === 403) {
|
|
23
|
+
throw new Error('User lacks admin privileges or insufficient permissions');
|
|
24
|
+
}
|
|
25
|
+
if (response.status === 400) {
|
|
26
|
+
const errorData = (await response.json());
|
|
27
|
+
throw new Error(`Bad request: ${errorData.error || 'Missing required parameters'}`);
|
|
28
|
+
}
|
|
29
|
+
if (response.status === 404) {
|
|
30
|
+
throw new Error('No prior result found');
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Failed to get prior result: ${response.status} ${response.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
return (await response.json());
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RunExamParams, ExamStreamEntry } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run a Proctor exam against an MCP server.
|
|
4
|
+
* Returns an async generator that yields NDJSON stream entries.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runExam(apiKey: string, baseUrl: string, params: RunExamParams): AsyncGenerator<ExamStreamEntry, void, unknown>;
|
|
7
|
+
//# sourceMappingURL=run-exam.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a Proctor exam against an MCP server.
|
|
3
|
+
* Returns an async generator that yields NDJSON stream entries.
|
|
4
|
+
*/
|
|
5
|
+
export async function* runExam(apiKey, baseUrl, params) {
|
|
6
|
+
const url = new URL('/api/proctor/run_exam', baseUrl);
|
|
7
|
+
const body = {
|
|
8
|
+
runtime_id: params.runtime_id,
|
|
9
|
+
exam_id: params.exam_id,
|
|
10
|
+
mcp_config: params.mcp_config,
|
|
11
|
+
};
|
|
12
|
+
if (params.server_json) {
|
|
13
|
+
body.server_json = params.server_json;
|
|
14
|
+
}
|
|
15
|
+
if (params.custom_runtime_image) {
|
|
16
|
+
body.custom_runtime_image = params.custom_runtime_image;
|
|
17
|
+
}
|
|
18
|
+
if (params.max_retries !== undefined) {
|
|
19
|
+
body.max_retries = params.max_retries;
|
|
20
|
+
}
|
|
21
|
+
if (params.mcp_server_slug) {
|
|
22
|
+
body.mcp_server_slug = params.mcp_server_slug;
|
|
23
|
+
}
|
|
24
|
+
if (params.mcp_json_id !== undefined) {
|
|
25
|
+
body.mcp_json_id = params.mcp_json_id;
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(url.toString(), {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'X-API-Key': apiKey,
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
Accept: 'application/x-ndjson',
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
if (response.status === 401) {
|
|
38
|
+
throw new Error('Invalid API key');
|
|
39
|
+
}
|
|
40
|
+
if (response.status === 403) {
|
|
41
|
+
throw new Error('User lacks admin privileges or insufficient permissions');
|
|
42
|
+
}
|
|
43
|
+
if (response.status === 422) {
|
|
44
|
+
const errorData = (await response.json());
|
|
45
|
+
throw new Error(`Validation error: ${errorData.error || 'Unknown validation error'}`);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Failed to run exam: ${response.status} ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
if (!response.body) {
|
|
50
|
+
throw new Error('No response body received');
|
|
51
|
+
}
|
|
52
|
+
const reader = response.body.getReader();
|
|
53
|
+
const decoder = new TextDecoder();
|
|
54
|
+
let buffer = '';
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done) {
|
|
59
|
+
// Process any remaining data in buffer
|
|
60
|
+
if (buffer.trim()) {
|
|
61
|
+
try {
|
|
62
|
+
yield JSON.parse(buffer.trim());
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Ignore parse errors for incomplete data
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
buffer += decoder.decode(value, { stream: true });
|
|
71
|
+
// Process complete lines (NDJSON format)
|
|
72
|
+
const lines = buffer.split('\n');
|
|
73
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const trimmed = line.trim();
|
|
76
|
+
if (trimmed) {
|
|
77
|
+
try {
|
|
78
|
+
yield JSON.parse(trimmed);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Skip malformed lines
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
reader.releaseLock();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SaveResultParams, SaveResultResponse } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Save exam results to the database
|
|
4
|
+
*/
|
|
5
|
+
export declare function saveResult(apiKey: string, baseUrl: string, params: SaveResultParams): Promise<SaveResultResponse>;
|
|
6
|
+
//# sourceMappingURL=save-result.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save exam results to the database
|
|
3
|
+
*/
|
|
4
|
+
export async function saveResult(apiKey, baseUrl, params) {
|
|
5
|
+
const url = new URL('/api/proctor/save_result', baseUrl);
|
|
6
|
+
const body = {
|
|
7
|
+
runtime_id: params.runtime_id,
|
|
8
|
+
exam_id: params.exam_id,
|
|
9
|
+
mcp_server_slug: params.mcp_server_slug,
|
|
10
|
+
mirror_id: params.mirror_id,
|
|
11
|
+
results: typeof params.results === 'string' ? params.results : JSON.stringify(params.results),
|
|
12
|
+
};
|
|
13
|
+
if (params.custom_runtime_image) {
|
|
14
|
+
body.custom_runtime_image = params.custom_runtime_image;
|
|
15
|
+
}
|
|
16
|
+
const response = await fetch(url.toString(), {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'X-API-Key': apiKey,
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
Accept: 'application/json',
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify(body),
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
if (response.status === 401) {
|
|
27
|
+
throw new Error('Invalid API key');
|
|
28
|
+
}
|
|
29
|
+
if (response.status === 403) {
|
|
30
|
+
throw new Error('User lacks admin privileges or insufficient permissions');
|
|
31
|
+
}
|
|
32
|
+
if (response.status === 404) {
|
|
33
|
+
throw new Error('Mirror not found');
|
|
34
|
+
}
|
|
35
|
+
if (response.status === 422) {
|
|
36
|
+
const errorData = (await response.json());
|
|
37
|
+
throw new Error(`Validation error: ${errorData.error || 'Unknown validation error'}`);
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Failed to save result: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
return (await response.json());
|
|
42
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { ProctorMetadataResponse, RunExamParams, ExamStreamEntry, SaveResultParams, SaveResultResponse, PriorResultParams, PriorResultResponse, MachinesResponse, CancelExamParams, CancelExamResponse } from './types.js';
|
|
3
|
+
export interface IProctorClient {
|
|
4
|
+
getMetadata(): Promise<ProctorMetadataResponse>;
|
|
5
|
+
runExam(params: RunExamParams): AsyncGenerator<ExamStreamEntry, void, unknown>;
|
|
6
|
+
saveResult(params: SaveResultParams): Promise<SaveResultResponse>;
|
|
7
|
+
getPriorResult(params: PriorResultParams): Promise<PriorResultResponse>;
|
|
8
|
+
getMachines(): Promise<MachinesResponse>;
|
|
9
|
+
destroyMachine(machineId: string): Promise<{
|
|
10
|
+
success: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
cancelExam(params: CancelExamParams): Promise<CancelExamResponse>;
|
|
13
|
+
}
|
|
14
|
+
export declare class ProctorClient implements IProctorClient {
|
|
15
|
+
private apiKey;
|
|
16
|
+
private baseUrl;
|
|
17
|
+
constructor(apiKey: string, baseUrl?: string);
|
|
18
|
+
getMetadata(): Promise<ProctorMetadataResponse>;
|
|
19
|
+
runExam(params: RunExamParams): AsyncGenerator<ExamStreamEntry, void, unknown>;
|
|
20
|
+
saveResult(params: SaveResultParams): Promise<SaveResultResponse>;
|
|
21
|
+
getPriorResult(params: PriorResultParams): Promise<PriorResultResponse>;
|
|
22
|
+
getMachines(): Promise<MachinesResponse>;
|
|
23
|
+
destroyMachine(machineId: string): Promise<{
|
|
24
|
+
success: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
cancelExam(params: CancelExamParams): Promise<CancelExamResponse>;
|
|
27
|
+
}
|
|
28
|
+
export type ClientFactory = () => IProctorClient;
|
|
29
|
+
export declare function createMCPServer(): {
|
|
30
|
+
server: Server<{
|
|
31
|
+
method: string;
|
|
32
|
+
params?: {
|
|
33
|
+
[x: string]: unknown;
|
|
34
|
+
_meta?: {
|
|
35
|
+
[x: string]: unknown;
|
|
36
|
+
progressToken?: string | number | undefined;
|
|
37
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
38
|
+
taskId: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
} | undefined;
|
|
41
|
+
} | undefined;
|
|
42
|
+
}, {
|
|
43
|
+
method: string;
|
|
44
|
+
params?: {
|
|
45
|
+
[x: string]: unknown;
|
|
46
|
+
_meta?: {
|
|
47
|
+
[x: string]: unknown;
|
|
48
|
+
progressToken?: string | number | undefined;
|
|
49
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
50
|
+
taskId: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
} | undefined;
|
|
53
|
+
} | undefined;
|
|
54
|
+
}, {
|
|
55
|
+
[x: string]: unknown;
|
|
56
|
+
_meta?: {
|
|
57
|
+
[x: string]: unknown;
|
|
58
|
+
progressToken?: string | number | undefined;
|
|
59
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
60
|
+
taskId: string;
|
|
61
|
+
} | undefined;
|
|
62
|
+
} | undefined;
|
|
63
|
+
}>;
|
|
64
|
+
registerHandlers: (server: Server, clientFactory?: ClientFactory) => Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=server.d.ts.map
|
package/shared/server.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { createRegisterTools } from './tools.js';
|
|
3
|
+
// Proctor API client implementation
|
|
4
|
+
export class ProctorClient {
|
|
5
|
+
apiKey;
|
|
6
|
+
baseUrl;
|
|
7
|
+
constructor(apiKey, baseUrl) {
|
|
8
|
+
this.apiKey = apiKey;
|
|
9
|
+
this.baseUrl = baseUrl || 'https://admin.pulsemcp.com';
|
|
10
|
+
}
|
|
11
|
+
async getMetadata() {
|
|
12
|
+
const { getMetadata } = await import('./proctor-client/lib/get-metadata.js');
|
|
13
|
+
return getMetadata(this.apiKey, this.baseUrl);
|
|
14
|
+
}
|
|
15
|
+
async *runExam(params) {
|
|
16
|
+
const { runExam } = await import('./proctor-client/lib/run-exam.js');
|
|
17
|
+
yield* runExam(this.apiKey, this.baseUrl, params);
|
|
18
|
+
}
|
|
19
|
+
async saveResult(params) {
|
|
20
|
+
const { saveResult } = await import('./proctor-client/lib/save-result.js');
|
|
21
|
+
return saveResult(this.apiKey, this.baseUrl, params);
|
|
22
|
+
}
|
|
23
|
+
async getPriorResult(params) {
|
|
24
|
+
const { getPriorResult } = await import('./proctor-client/lib/get-prior-result.js');
|
|
25
|
+
return getPriorResult(this.apiKey, this.baseUrl, params);
|
|
26
|
+
}
|
|
27
|
+
async getMachines() {
|
|
28
|
+
const { getMachines } = await import('./proctor-client/lib/get-machines.js');
|
|
29
|
+
return getMachines(this.apiKey, this.baseUrl);
|
|
30
|
+
}
|
|
31
|
+
async destroyMachine(machineId) {
|
|
32
|
+
const { destroyMachine } = await import('./proctor-client/lib/destroy-machine.js');
|
|
33
|
+
return destroyMachine(this.apiKey, this.baseUrl, machineId);
|
|
34
|
+
}
|
|
35
|
+
async cancelExam(params) {
|
|
36
|
+
const { cancelExam } = await import('./proctor-client/lib/cancel-exam.js');
|
|
37
|
+
return cancelExam(this.apiKey, this.baseUrl, params);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function createMCPServer() {
|
|
41
|
+
const server = new Server({
|
|
42
|
+
name: 'proctor-mcp-server',
|
|
43
|
+
version: '0.1.0',
|
|
44
|
+
}, {
|
|
45
|
+
capabilities: {
|
|
46
|
+
tools: {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const registerHandlers = async (server, clientFactory) => {
|
|
50
|
+
// Use provided factory or create default client
|
|
51
|
+
const factory = clientFactory ||
|
|
52
|
+
(() => {
|
|
53
|
+
// Get configuration from environment variables
|
|
54
|
+
const apiKey = process.env.PROCTOR_API_KEY;
|
|
55
|
+
const baseUrl = process.env.PROCTOR_API_URL;
|
|
56
|
+
if (!apiKey) {
|
|
57
|
+
throw new Error('PROCTOR_API_KEY environment variable must be configured');
|
|
58
|
+
}
|
|
59
|
+
return new ProctorClient(apiKey, baseUrl);
|
|
60
|
+
});
|
|
61
|
+
const registerTools = createRegisterTools(factory);
|
|
62
|
+
registerTools(server);
|
|
63
|
+
};
|
|
64
|
+
return { server, registerHandlers };
|
|
65
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { ClientFactory } from '../server.js';
|
|
3
|
+
export declare function cancelExam(_server: Server, clientFactory: ClientFactory): {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
machine_id: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: "ID of the Fly.io machine running the exam. Get this from get_machines. Must contain only alphanumeric characters, underscores, and hyphens.";
|
|
12
|
+
};
|
|
13
|
+
exam_id: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: "ID of the exam to cancel. Must match the exam currently running on the machine. Must contain only alphanumeric characters and hyphens.";
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
required: string[];
|
|
19
|
+
};
|
|
20
|
+
handler: (args: unknown) => Promise<{
|
|
21
|
+
content: {
|
|
22
|
+
type: string;
|
|
23
|
+
text: string;
|
|
24
|
+
}[];
|
|
25
|
+
isError?: undefined;
|
|
26
|
+
} | {
|
|
27
|
+
content: {
|
|
28
|
+
type: string;
|
|
29
|
+
text: string;
|
|
30
|
+
}[];
|
|
31
|
+
isError: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=cancel-exam.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Parameter descriptions - single source of truth
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
machine_id: 'ID of the Fly.io machine running the exam. Get this from get_machines. Must contain only alphanumeric characters, underscores, and hyphens.',
|
|
5
|
+
exam_id: 'ID of the exam to cancel. Must match the exam currently running on the machine. Must contain only alphanumeric characters and hyphens.',
|
|
6
|
+
};
|
|
7
|
+
const CancelExamSchema = z.object({
|
|
8
|
+
machine_id: z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.regex(/^[a-zA-Z0-9_-]+$/, 'Machine ID must contain only alphanumeric characters, underscores, and hyphens')
|
|
12
|
+
.describe(PARAM_DESCRIPTIONS.machine_id),
|
|
13
|
+
exam_id: z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.regex(/^[a-zA-Z0-9-]+$/, 'Exam ID must contain only alphanumeric characters and hyphens')
|
|
17
|
+
.describe(PARAM_DESCRIPTIONS.exam_id),
|
|
18
|
+
});
|
|
19
|
+
export function cancelExam(_server, clientFactory) {
|
|
20
|
+
return {
|
|
21
|
+
name: 'cancel_exam',
|
|
22
|
+
description: `Cancel a running Proctor exam.
|
|
23
|
+
|
|
24
|
+
Stops an exam that is currently executing on a Fly machine. This is useful
|
|
25
|
+
when you need to abort a test that is taking too long or encountering issues.
|
|
26
|
+
|
|
27
|
+
**Returns:**
|
|
28
|
+
- Result object indicating the cancellation status
|
|
29
|
+
|
|
30
|
+
**Use cases:**
|
|
31
|
+
- Stop a stuck or slow exam
|
|
32
|
+
- Cancel a test started with wrong parameters
|
|
33
|
+
- Free up resources for other tests
|
|
34
|
+
- Gracefully terminate a running exam before destroying the machine
|
|
35
|
+
|
|
36
|
+
**Note:**
|
|
37
|
+
- The exam must be currently running on the specified machine
|
|
38
|
+
- After cancellation, the machine may still need to be destroyed separately
|
|
39
|
+
- Results from a cancelled exam will not be saved automatically`,
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
machine_id: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: PARAM_DESCRIPTIONS.machine_id,
|
|
46
|
+
},
|
|
47
|
+
exam_id: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: PARAM_DESCRIPTIONS.exam_id,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
required: ['machine_id', 'exam_id'],
|
|
53
|
+
},
|
|
54
|
+
handler: async (args) => {
|
|
55
|
+
const validatedArgs = CancelExamSchema.parse(args);
|
|
56
|
+
const client = clientFactory();
|
|
57
|
+
try {
|
|
58
|
+
const response = await client.cancelExam({
|
|
59
|
+
machine_id: validatedArgs.machine_id,
|
|
60
|
+
exam_id: validatedArgs.exam_id,
|
|
61
|
+
});
|
|
62
|
+
let content = '## Exam Cancellation\n\n';
|
|
63
|
+
content += `**Machine ID:** ${validatedArgs.machine_id}\n`;
|
|
64
|
+
content += `**Exam ID:** ${validatedArgs.exam_id}\n\n`;
|
|
65
|
+
if (response.success) {
|
|
66
|
+
content += 'The exam has been cancelled successfully.';
|
|
67
|
+
}
|
|
68
|
+
else if (response.message) {
|
|
69
|
+
content += `Result: ${response.message}`;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
content += 'Cancellation request sent.\n\n';
|
|
73
|
+
content += '```json\n';
|
|
74
|
+
content += JSON.stringify(response, null, 2);
|
|
75
|
+
content += '\n```';
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: content.trim(),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: `Error cancelling exam: ${error instanceof Error ? error.message : String(error)}`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
isError: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { ClientFactory } from '../server.js';
|
|
3
|
+
export declare function destroyMachine(_server: Server, clientFactory: ClientFactory): {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
machine_id: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: "ID of the Fly.io machine to destroy. Get this from get_machines. Must contain only alphanumeric characters, underscores, and hyphens.";
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
required: string[];
|
|
15
|
+
};
|
|
16
|
+
handler: (args: unknown) => Promise<{
|
|
17
|
+
content: {
|
|
18
|
+
type: string;
|
|
19
|
+
text: string;
|
|
20
|
+
}[];
|
|
21
|
+
isError?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
content: {
|
|
24
|
+
type: string;
|
|
25
|
+
text: string;
|
|
26
|
+
}[];
|
|
27
|
+
isError: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=destroy-machine.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Parameter descriptions - single source of truth
|
|
3
|
+
const PARAM_DESCRIPTIONS = {
|
|
4
|
+
machine_id: 'ID of the Fly.io machine to destroy. Get this from get_machines. Must contain only alphanumeric characters, underscores, and hyphens.',
|
|
5
|
+
};
|
|
6
|
+
const DestroyMachineSchema = z.object({
|
|
7
|
+
machine_id: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.regex(/^[a-zA-Z0-9_-]+$/, 'Machine ID must contain only alphanumeric characters, underscores, and hyphens')
|
|
11
|
+
.describe(PARAM_DESCRIPTIONS.machine_id),
|
|
12
|
+
});
|
|
13
|
+
export function destroyMachine(_server, clientFactory) {
|
|
14
|
+
return {
|
|
15
|
+
name: 'destroy_machine',
|
|
16
|
+
description: `Delete a Fly.io machine.
|
|
17
|
+
|
|
18
|
+
Permanently removes a Fly machine that was used for Proctor exam execution.
|
|
19
|
+
Use this to clean up machines that are no longer needed.
|
|
20
|
+
|
|
21
|
+
**Returns:**
|
|
22
|
+
- success: boolean indicating if the machine was deleted
|
|
23
|
+
|
|
24
|
+
**Use cases:**
|
|
25
|
+
- Clean up machines after exam completion
|
|
26
|
+
- Remove stuck or failed machines
|
|
27
|
+
- Free up resources
|
|
28
|
+
- Remove machines that are no longer needed
|
|
29
|
+
|
|
30
|
+
**Warning:**
|
|
31
|
+
- This action is irreversible
|
|
32
|
+
- Any running processes on the machine will be terminated
|
|
33
|
+
- Use cancel_exam first if there's a running exam you want to stop gracefully`,
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
machine_id: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: PARAM_DESCRIPTIONS.machine_id,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ['machine_id'],
|
|
43
|
+
},
|
|
44
|
+
handler: async (args) => {
|
|
45
|
+
const validatedArgs = DestroyMachineSchema.parse(args);
|
|
46
|
+
const client = clientFactory();
|
|
47
|
+
try {
|
|
48
|
+
const response = await client.destroyMachine(validatedArgs.machine_id);
|
|
49
|
+
let content = '## Machine Destroyed\n\n';
|
|
50
|
+
content += `**Machine ID:** ${validatedArgs.machine_id}\n`;
|
|
51
|
+
content += `**Success:** ${response.success}\n\n`;
|
|
52
|
+
content += 'The machine has been permanently deleted.';
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: content.trim(),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: `Error destroying machine: ${error instanceof Error ? error.message : String(error)}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { ClientFactory } from '../server.js';
|
|
3
|
+
export declare function getMachines(_server: Server, clientFactory: ClientFactory): {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {};
|
|
9
|
+
required: never[];
|
|
10
|
+
};
|
|
11
|
+
handler: () => Promise<{
|
|
12
|
+
content: {
|
|
13
|
+
type: string;
|
|
14
|
+
text: string;
|
|
15
|
+
}[];
|
|
16
|
+
isError?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
content: {
|
|
19
|
+
type: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
isError: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=get-machines.d.ts.map
|