pulsemcp-cms-admin-mcp-server 0.7.4 → 0.8.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 CHANGED
@@ -94,7 +94,7 @@ This server is built and tested on macOS with Claude Desktop. It should work wit
94
94
  | `cleanup_good_jobs` | good_jobs | write | Clean up old background jobs by status and age. |
95
95
  | `run_exam_for_mirror` | proctor | write | Run proctor exams against unofficial mirrors via Fly Machines. Returns truncated summary with `result_id`. |
96
96
  | `get_exam_result` | proctor | read | Retrieve full untruncated exam results by `result_id`, with optional section/mirror filtering. |
97
- | `save_results_for_mirror` | proctor | write | Save proctor exam results. Accepts `result_id` or explicit results array. |
97
+ | `save_results_for_mirror` | proctor | write | Save proctor exam results via `result_id` from `run_exam_for_mirror`. |
98
98
 
99
99
  # Tool Groups
100
100
 
@@ -3,38 +3,19 @@ import { examResultStore, extractExamId, extractStatus } from '../exam-result-st
3
3
  const PARAM_DESCRIPTIONS = {
4
4
  mirror_id: 'The ID of the unofficial mirror to save results for',
5
5
  runtime_id: 'The runtime ID that was used to run the exams',
6
- result_id: 'The UUID returned by run_exam_for_mirror. When provided, the server retrieves the full result from the local file store — no need to pass the results array. This is the preferred approach.',
7
- results: 'Array of exam results to save. Each result must include exam_id, status, and optional data. Only needed if result_id is not provided.',
8
- exam_id: 'The exam identifier (e.g., "auth-check", "init-tools-list")',
9
- status: 'The result status (e.g., "pass", "fail", "error", "skip")',
10
- data: 'Optional detailed result data. Sensitive fields (tokens, secrets, passwords) will be automatically redacted before storage.',
6
+ result_id: 'The UUID returned by run_exam_for_mirror. The server retrieves the full result from the local file store automatically.',
11
7
  };
12
- const ResultSchema = z.object({
13
- exam_id: z.string().describe(PARAM_DESCRIPTIONS.exam_id),
14
- status: z.string().describe(PARAM_DESCRIPTIONS.status),
15
- data: z.record(z.unknown()).optional().describe(PARAM_DESCRIPTIONS.data),
16
- });
17
- const SaveResultsForMirrorSchema = z
18
- .object({
8
+ const SaveResultsForMirrorSchema = z.object({
19
9
  mirror_id: z.number().describe(PARAM_DESCRIPTIONS.mirror_id),
20
10
  runtime_id: z.string().optional().describe(PARAM_DESCRIPTIONS.runtime_id),
21
- result_id: z.string().uuid().optional().describe(PARAM_DESCRIPTIONS.result_id),
22
- results: z.array(ResultSchema).optional().describe(PARAM_DESCRIPTIONS.results),
23
- })
24
- .refine((data) => data.result_id || (data.results && data.results.length > 0), {
25
- message: 'Either result_id or a non-empty results array must be provided',
26
- })
27
- .refine((data) => !(data.result_id && data.results && data.results.length > 0), {
28
- message: 'Provide either result_id or results, not both. Use result_id (preferred) to retrieve from the store, or results for direct submission.',
11
+ result_id: z.string().uuid().describe(PARAM_DESCRIPTIONS.result_id),
29
12
  });
30
13
  export function saveResultsForMirror(_server, clientFactory) {
31
14
  return {
32
15
  name: 'save_results_for_mirror',
33
16
  description: `Save proctor exam results for an unofficial mirror.
34
17
 
35
- **Preferred**: Pass the \`result_id\` returned by \`run_exam_for_mirror\`. The full result is retrieved from the local file store server-side — no need to pass the large results payload through the LLM context.
36
-
37
- **Fallback**: Pass results directly (as before) if result_id is not available.
18
+ Pass the \`result_id\` returned by \`run_exam_for_mirror\`. The full result is retrieved from the local file store server-side — no need to pass the large results payload through the LLM context.
38
19
 
39
20
  Results are sanitized server-side to redact sensitive data (OAuth tokens, client secrets, passwords, etc.) before being persisted.
40
21
 
@@ -54,109 +35,85 @@ Typical workflow:
54
35
  format: 'uuid',
55
36
  description: PARAM_DESCRIPTIONS.result_id,
56
37
  },
57
- results: {
58
- type: 'array',
59
- items: {
60
- type: 'object',
61
- properties: {
62
- exam_id: { type: 'string', description: PARAM_DESCRIPTIONS.exam_id },
63
- status: { type: 'string', description: PARAM_DESCRIPTIONS.status },
64
- data: {
65
- type: 'object',
66
- additionalProperties: true,
67
- description: PARAM_DESCRIPTIONS.data,
68
- },
69
- },
70
- required: ['exam_id', 'status'],
71
- },
72
- description: PARAM_DESCRIPTIONS.results,
73
- },
74
38
  },
75
- required: ['mirror_id'],
39
+ required: ['mirror_id', 'result_id'],
76
40
  },
77
41
  handler: async (args) => {
78
42
  const validatedArgs = SaveResultsForMirrorSchema.parse(args);
79
43
  const client = clientFactory();
80
44
  try {
81
- let results = validatedArgs.results;
82
- let runtimeId = validatedArgs.runtime_id;
83
- // If result_id is provided, retrieve from the store
84
- if (validatedArgs.result_id) {
85
- const stored = examResultStore.get(validatedArgs.result_id);
86
- if (!stored) {
87
- return {
88
- content: [
89
- {
90
- type: 'text',
91
- text: `No stored result found for result_id "${validatedArgs.result_id}". The result file may have been cleaned up or the /tmp directory cleared. Pass the results array directly instead.`,
92
- },
93
- ],
94
- isError: true,
95
- };
96
- }
97
- // Extract exam_result lines from stored data.
98
- //
99
- // The real proctor API returns a deeply nested structure:
100
- // line.data = {
101
- // mirror_id, server_slug, exam_id, ...,
102
- // result: { ← envelope
103
- // exam_id, machine_id, status,
104
- // result: { ← actual payload
105
- // input: {...}, output: {...}, processedBy: {...}
106
- // },
107
- // error, logs
108
- // }
109
- // }
110
- //
111
- // The PulseMCP API expects the actual payload { input, output,
112
- // processedBy } at the top level of the saved results column.
113
- // We must unwrap through data.result.result to reach it.
114
- results = stored.lines
115
- .filter((line) => line.type === 'exam_result')
116
- .map((line) => {
117
- const data = line.data;
118
- // Unwrap nested result objects to find the exam payload
119
- // containing { input, output, processedBy }.
120
- let resultData = data;
121
- // Level 1: data.result (envelope with exam_id, machine_id, logs, etc.)
122
- if (resultData?.result &&
45
+ const stored = examResultStore.get(validatedArgs.result_id);
46
+ if (!stored) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: `No stored result found for result_id "${validatedArgs.result_id}". The result file may have been cleaned up or the /tmp directory cleared. Re-run run_exam_for_mirror to generate a new result.`,
52
+ },
53
+ ],
54
+ isError: true,
55
+ };
56
+ }
57
+ // Extract exam_result lines from stored data.
58
+ //
59
+ // The real proctor API returns a deeply nested structure:
60
+ // line.data = {
61
+ // mirror_id, server_slug, exam_id, ...,
62
+ // result: { ← envelope
63
+ // exam_id, machine_id, status,
64
+ // result: { ← actual payload
65
+ // input: {...}, output: {...}, processedBy: {...}
66
+ // },
67
+ // error, logs
68
+ // }
69
+ // }
70
+ //
71
+ // The PulseMCP API expects the actual payload { input, output,
72
+ // processedBy } at the top level of the saved results column.
73
+ // We must unwrap through data.result.result to reach it.
74
+ const results = stored.lines
75
+ .filter((line) => line.type === 'exam_result')
76
+ .map((line) => {
77
+ const data = line.data;
78
+ // Unwrap nested result objects to find the exam payload
79
+ // containing { input, output, processedBy }.
80
+ let resultData = data;
81
+ // Level 1: data.result (envelope with exam_id, machine_id, logs, etc.)
82
+ if (resultData?.result &&
83
+ typeof resultData.result === 'object' &&
84
+ !Array.isArray(resultData.result)) {
85
+ resultData = resultData.result;
86
+ // Level 2: data.result.result (actual payload with input, output, processedBy)
87
+ if (resultData.result &&
123
88
  typeof resultData.result === 'object' &&
124
89
  !Array.isArray(resultData.result)) {
125
90
  resultData = resultData.result;
126
- // Level 2: data.result.result (actual payload with input, output, processedBy)
127
- if (resultData.result &&
128
- typeof resultData.result === 'object' &&
129
- !Array.isArray(resultData.result)) {
130
- resultData = resultData.result;
131
- }
132
91
  }
133
- return {
134
- exam_id: extractExamId(line),
135
- status: extractStatus(line),
136
- ...(resultData ? { data: resultData } : {}),
137
- };
138
- });
139
- if (!runtimeId) {
140
- runtimeId = stored.runtime_id;
141
92
  }
142
- }
143
- if (!results || results.length === 0) {
93
+ return {
94
+ exam_id: extractExamId(line),
95
+ status: extractStatus(line),
96
+ ...(resultData ? { data: resultData } : {}),
97
+ };
98
+ });
99
+ if (results.length === 0) {
144
100
  return {
145
101
  content: [
146
102
  {
147
103
  type: 'text',
148
- text: 'No exam results to save. Either provide a result_id from run_exam_for_mirror or pass results directly.',
104
+ text: 'No exam results found in the stored result. The stored data may not contain any exam_result lines.',
149
105
  },
150
106
  ],
151
107
  isError: true,
152
108
  };
153
109
  }
110
+ const runtimeId = validatedArgs.runtime_id || stored.runtime_id;
154
111
  if (!runtimeId) {
155
112
  return {
156
113
  content: [
157
114
  {
158
115
  type: 'text',
159
- text: 'runtime_id is required. Provide it directly or use a result_id which includes the runtime_id.',
116
+ text: 'runtime_id is required. Provide it directly or ensure the stored result includes it.',
160
117
  },
161
118
  ],
162
119
  isError: true,
@@ -170,9 +127,7 @@ Typical workflow:
170
127
  let content = `**Proctor Results Saved**\n\n`;
171
128
  content += `Mirror ID: ${validatedArgs.mirror_id}\n`;
172
129
  content += `Runtime: ${runtimeId}\n`;
173
- if (validatedArgs.result_id) {
174
- content += `Result ID: ${validatedArgs.result_id}\n`;
175
- }
130
+ content += `Result ID: ${validatedArgs.result_id}\n`;
176
131
  content += '\n';
177
132
  if (response.saved.length > 0) {
178
133
  content += `**Successfully Saved (${response.saved.length}):**\n`;
@@ -193,7 +148,7 @@ Typical workflow:
193
148
  }
194
149
  }
195
150
  // Clean up stored result after successful save (all results persisted)
196
- if (validatedArgs.result_id && response.errors.length === 0) {
151
+ if (response.errors.length === 0) {
197
152
  examResultStore.delete(validatedArgs.result_id);
198
153
  }
199
154
  return { content: [{ type: 'text', text: content.trim() }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulsemcp-cms-admin-mcp-server",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "Local implementation of PulseMCP CMS Admin MCP server",
5
5
  "mcpName": "com.pulsemcp.servers/pulsemcp-cms-admin",
6
6
  "main": "build/index.js",
@@ -17,30 +17,7 @@ export declare function saveResultsForMirror(_server: Server, clientFactory: Cli
17
17
  result_id: {
18
18
  type: string;
19
19
  format: string;
20
- description: "The UUID returned by run_exam_for_mirror. When provided, the server retrieves the full result from the local file store — no need to pass the results array. This is the preferred approach.";
21
- };
22
- results: {
23
- type: string;
24
- items: {
25
- type: string;
26
- properties: {
27
- exam_id: {
28
- type: string;
29
- description: "The exam identifier (e.g., \"auth-check\", \"init-tools-list\")";
30
- };
31
- status: {
32
- type: string;
33
- description: "The result status (e.g., \"pass\", \"fail\", \"error\", \"skip\")";
34
- };
35
- data: {
36
- type: string;
37
- additionalProperties: boolean;
38
- description: "Optional detailed result data. Sensitive fields (tokens, secrets, passwords) will be automatically redacted before storage.";
39
- };
40
- };
41
- required: string[];
42
- };
43
- description: "Array of exam results to save. Each result must include exam_id, status, and optional data. Only needed if result_id is not provided.";
20
+ description: "The UUID returned by run_exam_for_mirror. The server retrieves the full result from the local file store automatically.";
44
21
  };
45
22
  };
46
23
  required: string[];
@@ -3,38 +3,19 @@ import { examResultStore, extractExamId, extractStatus } from '../exam-result-st
3
3
  const PARAM_DESCRIPTIONS = {
4
4
  mirror_id: 'The ID of the unofficial mirror to save results for',
5
5
  runtime_id: 'The runtime ID that was used to run the exams',
6
- result_id: 'The UUID returned by run_exam_for_mirror. When provided, the server retrieves the full result from the local file store — no need to pass the results array. This is the preferred approach.',
7
- results: 'Array of exam results to save. Each result must include exam_id, status, and optional data. Only needed if result_id is not provided.',
8
- exam_id: 'The exam identifier (e.g., "auth-check", "init-tools-list")',
9
- status: 'The result status (e.g., "pass", "fail", "error", "skip")',
10
- data: 'Optional detailed result data. Sensitive fields (tokens, secrets, passwords) will be automatically redacted before storage.',
6
+ result_id: 'The UUID returned by run_exam_for_mirror. The server retrieves the full result from the local file store automatically.',
11
7
  };
12
- const ResultSchema = z.object({
13
- exam_id: z.string().describe(PARAM_DESCRIPTIONS.exam_id),
14
- status: z.string().describe(PARAM_DESCRIPTIONS.status),
15
- data: z.record(z.unknown()).optional().describe(PARAM_DESCRIPTIONS.data),
16
- });
17
- const SaveResultsForMirrorSchema = z
18
- .object({
8
+ const SaveResultsForMirrorSchema = z.object({
19
9
  mirror_id: z.number().describe(PARAM_DESCRIPTIONS.mirror_id),
20
10
  runtime_id: z.string().optional().describe(PARAM_DESCRIPTIONS.runtime_id),
21
- result_id: z.string().uuid().optional().describe(PARAM_DESCRIPTIONS.result_id),
22
- results: z.array(ResultSchema).optional().describe(PARAM_DESCRIPTIONS.results),
23
- })
24
- .refine((data) => data.result_id || (data.results && data.results.length > 0), {
25
- message: 'Either result_id or a non-empty results array must be provided',
26
- })
27
- .refine((data) => !(data.result_id && data.results && data.results.length > 0), {
28
- message: 'Provide either result_id or results, not both. Use result_id (preferred) to retrieve from the store, or results for direct submission.',
11
+ result_id: z.string().uuid().describe(PARAM_DESCRIPTIONS.result_id),
29
12
  });
30
13
  export function saveResultsForMirror(_server, clientFactory) {
31
14
  return {
32
15
  name: 'save_results_for_mirror',
33
16
  description: `Save proctor exam results for an unofficial mirror.
34
17
 
35
- **Preferred**: Pass the \`result_id\` returned by \`run_exam_for_mirror\`. The full result is retrieved from the local file store server-side — no need to pass the large results payload through the LLM context.
36
-
37
- **Fallback**: Pass results directly (as before) if result_id is not available.
18
+ Pass the \`result_id\` returned by \`run_exam_for_mirror\`. The full result is retrieved from the local file store server-side — no need to pass the large results payload through the LLM context.
38
19
 
39
20
  Results are sanitized server-side to redact sensitive data (OAuth tokens, client secrets, passwords, etc.) before being persisted.
40
21
 
@@ -54,109 +35,85 @@ Typical workflow:
54
35
  format: 'uuid',
55
36
  description: PARAM_DESCRIPTIONS.result_id,
56
37
  },
57
- results: {
58
- type: 'array',
59
- items: {
60
- type: 'object',
61
- properties: {
62
- exam_id: { type: 'string', description: PARAM_DESCRIPTIONS.exam_id },
63
- status: { type: 'string', description: PARAM_DESCRIPTIONS.status },
64
- data: {
65
- type: 'object',
66
- additionalProperties: true,
67
- description: PARAM_DESCRIPTIONS.data,
68
- },
69
- },
70
- required: ['exam_id', 'status'],
71
- },
72
- description: PARAM_DESCRIPTIONS.results,
73
- },
74
38
  },
75
- required: ['mirror_id'],
39
+ required: ['mirror_id', 'result_id'],
76
40
  },
77
41
  handler: async (args) => {
78
42
  const validatedArgs = SaveResultsForMirrorSchema.parse(args);
79
43
  const client = clientFactory();
80
44
  try {
81
- let results = validatedArgs.results;
82
- let runtimeId = validatedArgs.runtime_id;
83
- // If result_id is provided, retrieve from the store
84
- if (validatedArgs.result_id) {
85
- const stored = examResultStore.get(validatedArgs.result_id);
86
- if (!stored) {
87
- return {
88
- content: [
89
- {
90
- type: 'text',
91
- text: `No stored result found for result_id "${validatedArgs.result_id}". The result file may have been cleaned up or the /tmp directory cleared. Pass the results array directly instead.`,
92
- },
93
- ],
94
- isError: true,
95
- };
96
- }
97
- // Extract exam_result lines from stored data.
98
- //
99
- // The real proctor API returns a deeply nested structure:
100
- // line.data = {
101
- // mirror_id, server_slug, exam_id, ...,
102
- // result: { ← envelope
103
- // exam_id, machine_id, status,
104
- // result: { ← actual payload
105
- // input: {...}, output: {...}, processedBy: {...}
106
- // },
107
- // error, logs
108
- // }
109
- // }
110
- //
111
- // The PulseMCP API expects the actual payload { input, output,
112
- // processedBy } at the top level of the saved results column.
113
- // We must unwrap through data.result.result to reach it.
114
- results = stored.lines
115
- .filter((line) => line.type === 'exam_result')
116
- .map((line) => {
117
- const data = line.data;
118
- // Unwrap nested result objects to find the exam payload
119
- // containing { input, output, processedBy }.
120
- let resultData = data;
121
- // Level 1: data.result (envelope with exam_id, machine_id, logs, etc.)
122
- if (resultData?.result &&
45
+ const stored = examResultStore.get(validatedArgs.result_id);
46
+ if (!stored) {
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: `No stored result found for result_id "${validatedArgs.result_id}". The result file may have been cleaned up or the /tmp directory cleared. Re-run run_exam_for_mirror to generate a new result.`,
52
+ },
53
+ ],
54
+ isError: true,
55
+ };
56
+ }
57
+ // Extract exam_result lines from stored data.
58
+ //
59
+ // The real proctor API returns a deeply nested structure:
60
+ // line.data = {
61
+ // mirror_id, server_slug, exam_id, ...,
62
+ // result: { ← envelope
63
+ // exam_id, machine_id, status,
64
+ // result: { ← actual payload
65
+ // input: {...}, output: {...}, processedBy: {...}
66
+ // },
67
+ // error, logs
68
+ // }
69
+ // }
70
+ //
71
+ // The PulseMCP API expects the actual payload { input, output,
72
+ // processedBy } at the top level of the saved results column.
73
+ // We must unwrap through data.result.result to reach it.
74
+ const results = stored.lines
75
+ .filter((line) => line.type === 'exam_result')
76
+ .map((line) => {
77
+ const data = line.data;
78
+ // Unwrap nested result objects to find the exam payload
79
+ // containing { input, output, processedBy }.
80
+ let resultData = data;
81
+ // Level 1: data.result (envelope with exam_id, machine_id, logs, etc.)
82
+ if (resultData?.result &&
83
+ typeof resultData.result === 'object' &&
84
+ !Array.isArray(resultData.result)) {
85
+ resultData = resultData.result;
86
+ // Level 2: data.result.result (actual payload with input, output, processedBy)
87
+ if (resultData.result &&
123
88
  typeof resultData.result === 'object' &&
124
89
  !Array.isArray(resultData.result)) {
125
90
  resultData = resultData.result;
126
- // Level 2: data.result.result (actual payload with input, output, processedBy)
127
- if (resultData.result &&
128
- typeof resultData.result === 'object' &&
129
- !Array.isArray(resultData.result)) {
130
- resultData = resultData.result;
131
- }
132
91
  }
133
- return {
134
- exam_id: extractExamId(line),
135
- status: extractStatus(line),
136
- ...(resultData ? { data: resultData } : {}),
137
- };
138
- });
139
- if (!runtimeId) {
140
- runtimeId = stored.runtime_id;
141
92
  }
142
- }
143
- if (!results || results.length === 0) {
93
+ return {
94
+ exam_id: extractExamId(line),
95
+ status: extractStatus(line),
96
+ ...(resultData ? { data: resultData } : {}),
97
+ };
98
+ });
99
+ if (results.length === 0) {
144
100
  return {
145
101
  content: [
146
102
  {
147
103
  type: 'text',
148
- text: 'No exam results to save. Either provide a result_id from run_exam_for_mirror or pass results directly.',
104
+ text: 'No exam results found in the stored result. The stored data may not contain any exam_result lines.',
149
105
  },
150
106
  ],
151
107
  isError: true,
152
108
  };
153
109
  }
110
+ const runtimeId = validatedArgs.runtime_id || stored.runtime_id;
154
111
  if (!runtimeId) {
155
112
  return {
156
113
  content: [
157
114
  {
158
115
  type: 'text',
159
- text: 'runtime_id is required. Provide it directly or use a result_id which includes the runtime_id.',
116
+ text: 'runtime_id is required. Provide it directly or ensure the stored result includes it.',
160
117
  },
161
118
  ],
162
119
  isError: true,
@@ -170,9 +127,7 @@ Typical workflow:
170
127
  let content = `**Proctor Results Saved**\n\n`;
171
128
  content += `Mirror ID: ${validatedArgs.mirror_id}\n`;
172
129
  content += `Runtime: ${runtimeId}\n`;
173
- if (validatedArgs.result_id) {
174
- content += `Result ID: ${validatedArgs.result_id}\n`;
175
- }
130
+ content += `Result ID: ${validatedArgs.result_id}\n`;
176
131
  content += '\n';
177
132
  if (response.saved.length > 0) {
178
133
  content += `**Successfully Saved (${response.saved.length}):**\n`;
@@ -193,7 +148,7 @@ Typical workflow:
193
148
  }
194
149
  }
195
150
  // Clean up stored result after successful save (all results persisted)
196
- if (validatedArgs.result_id && response.errors.length === 0) {
151
+ if (response.errors.length === 0) {
197
152
  examResultStore.delete(validatedArgs.result_id);
198
153
  }
199
154
  return { content: [{ type: 'text', text: content.trim() }] };