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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proctor-mcp-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Local implementation of Proctor MCP server",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
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
  };
@@ -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(finalResult, null, 2);
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
+ }