proctor-mcp-server 0.1.4 → 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.
@@ -3,8 +3,16 @@
3
3
  * Integration test entry point with mock client
4
4
  * This file is used for testing the MCP server with mocked external API calls
5
5
  */
6
+ import { readFileSync } from 'fs';
7
+ import { dirname, join } from 'path';
8
+ import { fileURLToPath } from 'url';
6
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
10
  import { createMCPServer, logServerStart, logError } from '../shared/index.js';
11
+ // Read version from package.json
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const packageJsonPath = join(__dirname, '..', 'package.json');
14
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
15
+ const VERSION = packageJson.version;
8
16
  /**
9
17
  * Integration mock implementation of IProctorClient
10
18
  */
@@ -87,7 +95,7 @@ class IntegrationMockProctorClient {
87
95
  }
88
96
  async function main() {
89
97
  // Create server using factory
90
- const { server, registerHandlers } = createMCPServer();
98
+ const { server, registerHandlers } = createMCPServer({ version: VERSION });
91
99
  // Create mock client for testing
92
100
  const mockClient = new IntegrationMockProctorClient();
93
101
  // Register all handlers with mock client
package/build/index.js CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
2
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
6
  import { createMCPServer } from '../shared/index.js';
4
7
  import { logServerStart, logError } from '../shared/logging.js';
8
+ // Read version from package.json
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const packageJsonPath = join(__dirname, '..', 'package.json');
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
12
+ const VERSION = packageJson.version;
5
13
  // Validate required environment variables before starting
6
14
  function validateEnvironment() {
7
15
  const required = [
@@ -42,7 +50,7 @@ async function main() {
42
50
  // Validate environment variables first
43
51
  validateEnvironment();
44
52
  // Create server using factory
45
- const { server, registerHandlers } = createMCPServer();
53
+ const { server, registerHandlers } = createMCPServer({ version: VERSION });
46
54
  // Register all handlers (resources and tools)
47
55
  await registerHandlers(server);
48
56
  // Start server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proctor-mcp-server",
3
- "version": "0.1.4",
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
@@ -1,7 +1,8 @@
1
1
  export { createMCPServer, ProctorClient } from './server.js';
2
- export type { IProctorClient, ClientFactory } from './server.js';
2
+ export type { IProctorClient, ClientFactory, CreateMCPServerOptions } from './server.js';
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';
@@ -22,7 +22,10 @@ export declare class ProctorClient implements IProctorClient {
22
22
  cancelExam(params: CancelExamParams): Promise<CancelExamResponse>;
23
23
  }
24
24
  export type ClientFactory = () => IProctorClient;
25
- export declare function createMCPServer(): {
25
+ export interface CreateMCPServerOptions {
26
+ version: string;
27
+ }
28
+ export declare function createMCPServer(options: CreateMCPServerOptions): {
26
29
  server: Server<{
27
30
  method: string;
28
31
  params?: {
package/shared/server.js CHANGED
@@ -29,10 +29,10 @@ export class ProctorClient {
29
29
  return cancelExam(this.apiKey, this.baseUrl, params);
30
30
  }
31
31
  }
32
- export function createMCPServer() {
32
+ export function createMCPServer(options) {
33
33
  const server = new Server({
34
34
  name: 'proctor-mcp-server',
35
- version: '0.1.2',
35
+ version: options.version,
36
36
  }, {
37
37
  capabilities: {
38
38
  tools: {},
@@ -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),
@@ -110,6 +112,28 @@ const PreloadedCredentialsSchema = z.object({
110
112
  client_secret: z.string().optional(),
111
113
  expires_at: z.string().optional(),
112
114
  });
115
+ // Preprocess to transform empty/incomplete objects to undefined before validation
116
+ // This handles cases where MCP clients send {}, null, or partial objects for optional parameters
117
+ const preprocessPreloadedCredentials = (val) => {
118
+ // If undefined, null, or not an object, let it pass through (or become undefined)
119
+ if (val === undefined || val === null) {
120
+ return undefined;
121
+ }
122
+ if (typeof val !== 'object') {
123
+ return undefined;
124
+ }
125
+ // Check if it's an empty object or missing required fields
126
+ const obj = val;
127
+ if (Object.keys(obj).length === 0) {
128
+ return undefined;
129
+ }
130
+ if (!('server_key' in obj) || !('access_token' in obj)) {
131
+ return undefined;
132
+ }
133
+ // Has required fields, validate it properly
134
+ return val;
135
+ };
136
+ const OptionalPreloadedCredentialsSchema = z.preprocess(preprocessPreloadedCredentials, PreloadedCredentialsSchema.optional());
113
137
  const RunExamSchema = z.object({
114
138
  runtime_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.runtime_id),
115
139
  exam_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.exam_id),
@@ -117,7 +141,8 @@ const RunExamSchema = z.object({
117
141
  server_json: z.string().optional().describe(PARAM_DESCRIPTIONS.server_json),
118
142
  custom_runtime_image: z.string().optional().describe(PARAM_DESCRIPTIONS.custom_runtime_image),
119
143
  max_retries: z.number().min(0).max(10).optional().describe(PARAM_DESCRIPTIONS.max_retries),
120
- preloaded_credentials: PreloadedCredentialsSchema.optional().describe(PARAM_DESCRIPTIONS.preloaded_credentials),
144
+ preloaded_credentials: OptionalPreloadedCredentialsSchema.optional().describe(PARAM_DESCRIPTIONS.preloaded_credentials),
145
+ expand_fields: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.expand_fields),
121
146
  });
122
147
  export function runExam(_server, clientFactory) {
123
148
  return {
@@ -149,7 +174,8 @@ The mcp_json parameter accepts a JSON object with server configurations. Each se
149
174
  - Use get_proctor_metadata first to discover available runtimes and exams
150
175
  - The mcp_json must be a valid JSON string representing the mcp.json format
151
176
  - Custom runtime images require the "__custom__" runtime_id and custom_runtime_image parameter
152
- - 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`,
153
179
  inputSchema: {
154
180
  type: 'object',
155
181
  properties: {
@@ -191,6 +217,11 @@ The mcp_json parameter accepts a JSON object with server configurations. Each se
191
217
  },
192
218
  required: ['server_key', 'access_token'],
193
219
  },
220
+ expand_fields: {
221
+ type: 'array',
222
+ items: { type: 'string' },
223
+ description: PARAM_DESCRIPTIONS.expand_fields,
224
+ },
194
225
  },
195
226
  required: ['runtime_id', 'exam_id', 'mcp_json'],
196
227
  },
@@ -271,8 +302,9 @@ The mcp_json parameter accepts a JSON object with server configurations. Each se
271
302
  };
272
303
  }
273
304
  if (finalResult) {
305
+ const truncatedResult = truncateStrings(deepClone(finalResult), validatedArgs.expand_fields || []);
274
306
  content += '### Result\n\n```json\n';
275
- content += JSON.stringify(finalResult, null, 2);
307
+ content += JSON.stringify(truncatedResult, null, 2);
276
308
  content += '\n```\n';
277
309
  }
278
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
+ }