proctor-mcp-server 0.1.5 → 0.1.6
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/package.json +1 -1
- package/shared/index.d.ts +1 -0
- package/shared/index.js +1 -0
- package/shared/tools/run-exam.d.ts +7 -0
- package/shared/tools/run-exam.js +12 -2
- package/shared/utils/truncation.d.ts +19 -0
- package/shared/utils/truncation.js +170 -0
package/package.json
CHANGED
package/shared/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export type { IProctorClient, ClientFactory, CreateMCPServerOptions } from './se
|
|
|
3
3
|
export { createRegisterTools, parseEnabledToolGroups } from './tools.js';
|
|
4
4
|
export type { ToolGroup } from './tools.js';
|
|
5
5
|
export { logServerStart, logError, logWarning, logDebug } from './logging.js';
|
|
6
|
+
export { truncateStrings, deepClone } from './utils/truncation.js';
|
|
6
7
|
export type { ProctorRuntime, ProctorExam, ProctorMetadataResponse, ExamLogEntry, ExamStreamLog, ExamStreamResult, ExamStreamError, ExamStreamEntry, ExamResult, RunExamParams, FlyMachine, MachinesResponse, CancelExamParams, CancelExamResponse, ApiError, } from './types.js';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/shared/index.js
CHANGED
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
export { createMCPServer, ProctorClient } from './server.js';
|
|
3
3
|
export { createRegisterTools, parseEnabledToolGroups } from './tools.js';
|
|
4
4
|
export { logServerStart, logError, logWarning, logDebug } from './logging.js';
|
|
5
|
+
export { truncateStrings, deepClone } from './utils/truncation.js';
|
|
@@ -58,6 +58,13 @@ export declare function runExam(_server: Server, clientFactory: ClientFactory):
|
|
|
58
58
|
};
|
|
59
59
|
required: string[];
|
|
60
60
|
};
|
|
61
|
+
expand_fields: {
|
|
62
|
+
type: string;
|
|
63
|
+
items: {
|
|
64
|
+
type: string;
|
|
65
|
+
};
|
|
66
|
+
description: "Array of dot-notation paths to show in full (not truncated). By default, long strings (>200 chars) and deep objects in exam results are auto-truncated to reduce response size. Use this to expand specific fields. Examples: [\"tools[].inputSchema\", \"input\"].";
|
|
67
|
+
};
|
|
61
68
|
};
|
|
62
69
|
required: string[];
|
|
63
70
|
};
|
package/shared/tools/run-exam.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { truncateStrings, deepClone } from '../utils/truncation.js';
|
|
2
3
|
// Parameter descriptions - single source of truth
|
|
3
4
|
const PARAM_DESCRIPTIONS = {
|
|
4
5
|
runtime_id: 'Runtime ID from get_proctor_metadata, or "__custom__" for a custom Docker image. Example: "v0.0.37"',
|
|
@@ -100,6 +101,7 @@ When provided, these credentials are passed to proctor-mcp-client which loads th
|
|
|
100
101
|
- client_id: OAuth client ID
|
|
101
102
|
- client_secret: OAuth client secret
|
|
102
103
|
- expires_at: ISO 8601 timestamp when the access token expires`,
|
|
104
|
+
expand_fields: 'Array of dot-notation paths to show in full (not truncated). By default, long strings (>200 chars) and deep objects in exam results are auto-truncated to reduce response size. Use this to expand specific fields. Examples: ["tools[].inputSchema", "input"].',
|
|
103
105
|
};
|
|
104
106
|
const PreloadedCredentialsSchema = z.object({
|
|
105
107
|
server_key: z.string().min(1),
|
|
@@ -140,6 +142,7 @@ const RunExamSchema = z.object({
|
|
|
140
142
|
custom_runtime_image: z.string().optional().describe(PARAM_DESCRIPTIONS.custom_runtime_image),
|
|
141
143
|
max_retries: z.number().min(0).max(10).optional().describe(PARAM_DESCRIPTIONS.max_retries),
|
|
142
144
|
preloaded_credentials: OptionalPreloadedCredentialsSchema.optional().describe(PARAM_DESCRIPTIONS.preloaded_credentials),
|
|
145
|
+
expand_fields: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.expand_fields),
|
|
143
146
|
});
|
|
144
147
|
export function runExam(_server, clientFactory) {
|
|
145
148
|
return {
|
|
@@ -171,7 +174,8 @@ The mcp_json parameter accepts a JSON object with server configurations. Each se
|
|
|
171
174
|
- Use get_proctor_metadata first to discover available runtimes and exams
|
|
172
175
|
- The mcp_json must be a valid JSON string representing the mcp.json format
|
|
173
176
|
- Custom runtime images require the "__custom__" runtime_id and custom_runtime_image parameter
|
|
174
|
-
- Underscore-prefixed fields in mcp_json are used for exam setup only and stripped before server execution
|
|
177
|
+
- Underscore-prefixed fields in mcp_json are used for exam setup only and stripped before server execution
|
|
178
|
+
- Results are auto-truncated to reduce response size. Long strings (>200 chars) and deep nested objects are replaced with truncation messages. Use the expand_fields parameter to retrieve full content for specific fields`,
|
|
175
179
|
inputSchema: {
|
|
176
180
|
type: 'object',
|
|
177
181
|
properties: {
|
|
@@ -213,6 +217,11 @@ The mcp_json parameter accepts a JSON object with server configurations. Each se
|
|
|
213
217
|
},
|
|
214
218
|
required: ['server_key', 'access_token'],
|
|
215
219
|
},
|
|
220
|
+
expand_fields: {
|
|
221
|
+
type: 'array',
|
|
222
|
+
items: { type: 'string' },
|
|
223
|
+
description: PARAM_DESCRIPTIONS.expand_fields,
|
|
224
|
+
},
|
|
216
225
|
},
|
|
217
226
|
required: ['runtime_id', 'exam_id', 'mcp_json'],
|
|
218
227
|
},
|
|
@@ -293,8 +302,9 @@ The mcp_json parameter accepts a JSON object with server configurations. Each se
|
|
|
293
302
|
};
|
|
294
303
|
}
|
|
295
304
|
if (finalResult) {
|
|
305
|
+
const truncatedResult = truncateStrings(deepClone(finalResult), validatedArgs.expand_fields || []);
|
|
296
306
|
content += '### Result\n\n```json\n';
|
|
297
|
-
content += JSON.stringify(
|
|
307
|
+
content += JSON.stringify(truncatedResult, null, 2);
|
|
298
308
|
content += '\n```\n';
|
|
299
309
|
}
|
|
300
310
|
return {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for string truncation and field expansion
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Recursively truncates values in an object:
|
|
6
|
+
* 1. Strings longer than 200 chars are replaced with truncation message
|
|
7
|
+
* 2. At depth >= 6, any value serializing to > 500 chars is replaced with truncation message
|
|
8
|
+
*
|
|
9
|
+
* @param obj - The object to process
|
|
10
|
+
* @param expandFields - Array of dot-notation paths to exclude from truncation
|
|
11
|
+
* @param currentPath - Current path in the object (for internal recursion)
|
|
12
|
+
* @returns Object with truncated values
|
|
13
|
+
*/
|
|
14
|
+
export declare function truncateStrings(obj: unknown, expandFields?: string[], currentPath?: string): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Deep clones an object using JSON serialization.
|
|
17
|
+
*/
|
|
18
|
+
export declare function deepClone<T>(obj: T): T;
|
|
19
|
+
//# sourceMappingURL=truncation.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for string truncation and field expansion
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_STRING_MAX_LENGTH = 200;
|
|
5
|
+
const DEEP_VALUE_MAX_LENGTH = 500;
|
|
6
|
+
const DEPTH_THRESHOLD = 6; // Start truncating complex values at depth 6 (so depth 5 keys are visible)
|
|
7
|
+
/**
|
|
8
|
+
* Creates truncation message with specific path for expansion.
|
|
9
|
+
* Returns a complete message (not a suffix) to ensure JSON validity.
|
|
10
|
+
*/
|
|
11
|
+
function getTruncationMessage(path, type) {
|
|
12
|
+
const wildcardPath = path.replace(/\[\d+\]/g, '[]');
|
|
13
|
+
if (type === 'string') {
|
|
14
|
+
return `[TRUNCATED - use expand_fields: ["${wildcardPath}"] to see full content]`;
|
|
15
|
+
}
|
|
16
|
+
return `[DEEP OBJECT TRUNCATED - use expand_fields: ["${wildcardPath}"] to see full content]`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Calculates the depth of a path.
|
|
20
|
+
* Depth is counted as: each key access and each array index access.
|
|
21
|
+
* Examples:
|
|
22
|
+
* - "results" = depth 1
|
|
23
|
+
* - "results[0]" = depth 2
|
|
24
|
+
* - "results[0].tools" = depth 3
|
|
25
|
+
* - "results[0].tools[0]" = depth 4
|
|
26
|
+
* - "results[0].tools[0].inputSchema" = depth 5
|
|
27
|
+
* - "results[0].tools[0].inputSchema.properties" = depth 6 (truncation starts here)
|
|
28
|
+
*/
|
|
29
|
+
function getDepth(path) {
|
|
30
|
+
if (!path)
|
|
31
|
+
return 0;
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < path.length) {
|
|
35
|
+
// Skip to the next segment
|
|
36
|
+
if (path[i] === '.') {
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (path[i] === '[') {
|
|
41
|
+
// Array index or bracket notation - count as one depth level
|
|
42
|
+
depth++;
|
|
43
|
+
// Skip past the closing bracket
|
|
44
|
+
const closeBracket = path.indexOf(']', i);
|
|
45
|
+
if (closeBracket === -1)
|
|
46
|
+
break;
|
|
47
|
+
i = closeBracket + 1;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Key access - count as one depth level
|
|
51
|
+
depth++;
|
|
52
|
+
// Skip to the next delimiter
|
|
53
|
+
while (i < path.length && path[i] !== '.' && path[i] !== '[') {
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return depth;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Recursively truncates values in an object:
|
|
62
|
+
* 1. Strings longer than 200 chars are replaced with truncation message
|
|
63
|
+
* 2. At depth >= 6, any value serializing to > 500 chars is replaced with truncation message
|
|
64
|
+
*
|
|
65
|
+
* @param obj - The object to process
|
|
66
|
+
* @param expandFields - Array of dot-notation paths to exclude from truncation
|
|
67
|
+
* @param currentPath - Current path in the object (for internal recursion)
|
|
68
|
+
* @returns Object with truncated values
|
|
69
|
+
*/
|
|
70
|
+
export function truncateStrings(obj, expandFields = [], currentPath = '') {
|
|
71
|
+
if (obj === null || obj === undefined) {
|
|
72
|
+
return obj;
|
|
73
|
+
}
|
|
74
|
+
const currentDepth = getDepth(currentPath);
|
|
75
|
+
// Check if this path should be expanded (skip all truncation)
|
|
76
|
+
if (shouldExpand(currentPath, expandFields)) {
|
|
77
|
+
// Still need to recurse for nested paths that might not be expanded
|
|
78
|
+
if (Array.isArray(obj)) {
|
|
79
|
+
return obj.map((item, index) => {
|
|
80
|
+
const arrayPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
|
|
81
|
+
return truncateStrings(item, expandFields, arrayPath);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (typeof obj === 'object') {
|
|
85
|
+
const result = {};
|
|
86
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
87
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
88
|
+
result[key] = truncateStrings(value, expandFields, newPath);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
return obj;
|
|
93
|
+
}
|
|
94
|
+
// At depth >= DEPTH_THRESHOLD, check if the serialized value is too large
|
|
95
|
+
if (currentDepth >= DEPTH_THRESHOLD && (typeof obj === 'object' || Array.isArray(obj))) {
|
|
96
|
+
const serialized = JSON.stringify(obj);
|
|
97
|
+
if (serialized.length > DEEP_VALUE_MAX_LENGTH) {
|
|
98
|
+
// Replace the entire value with a truncation message (keeps JSON valid)
|
|
99
|
+
return getTruncationMessage(currentPath, 'deep');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Handle strings - truncate if too long
|
|
103
|
+
if (typeof obj === 'string') {
|
|
104
|
+
if (obj.length > DEFAULT_STRING_MAX_LENGTH) {
|
|
105
|
+
// Replace with truncation message (keeps JSON valid)
|
|
106
|
+
return getTruncationMessage(currentPath, 'string');
|
|
107
|
+
}
|
|
108
|
+
return obj;
|
|
109
|
+
}
|
|
110
|
+
// Handle arrays
|
|
111
|
+
if (Array.isArray(obj)) {
|
|
112
|
+
return obj.map((item, index) => {
|
|
113
|
+
const arrayPath = currentPath ? `${currentPath}[${index}]` : `[${index}]`;
|
|
114
|
+
return truncateStrings(item, expandFields, arrayPath);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Handle objects
|
|
118
|
+
if (typeof obj === 'object') {
|
|
119
|
+
const result = {};
|
|
120
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
121
|
+
const newPath = currentPath ? `${currentPath}.${key}` : key;
|
|
122
|
+
result[key] = truncateStrings(value, expandFields, newPath);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
// For other types (numbers, booleans, etc.), return as-is
|
|
127
|
+
return obj;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Checks if a path should be expanded (not truncated).
|
|
131
|
+
* Supports exact matches, prefix matches, and array wildcard notation.
|
|
132
|
+
*
|
|
133
|
+
* Examples:
|
|
134
|
+
* - "results[0].tools[0].inputSchema" matches "results[].tools[].inputSchema"
|
|
135
|
+
* - "results.tests" matches "results.tests"
|
|
136
|
+
* - "results[0].tools[0].description" matches "results[].tools[].description"
|
|
137
|
+
*/
|
|
138
|
+
function shouldExpand(currentPath, expandFields) {
|
|
139
|
+
if (!currentPath || expandFields.length === 0) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
for (const expandPath of expandFields) {
|
|
143
|
+
// Exact match
|
|
144
|
+
if (currentPath === expandPath) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
// Check if expand path is a prefix (for nested expansion)
|
|
148
|
+
if (currentPath.startsWith(expandPath + '.') || currentPath.startsWith(expandPath + '[')) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
// Convert array indices to wildcards for comparison
|
|
152
|
+
// e.g., "results[0].tools[1].inputSchema" becomes "results[].tools[].inputSchema"
|
|
153
|
+
const normalizedCurrent = currentPath.replace(/\[\d+\]/g, '[]');
|
|
154
|
+
if (normalizedCurrent === expandPath) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
// Check prefix match with normalized path
|
|
158
|
+
if (normalizedCurrent.startsWith(expandPath + '.') ||
|
|
159
|
+
normalizedCurrent.startsWith(expandPath + '[')) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Deep clones an object using JSON serialization.
|
|
167
|
+
*/
|
|
168
|
+
export function deepClone(obj) {
|
|
169
|
+
return JSON.parse(JSON.stringify(obj));
|
|
170
|
+
}
|