coursecode 0.1.43 → 0.1.46

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.
@@ -679,7 +679,12 @@ Runs **in Node.js** during build (via `vite.framework-dev.config.js` `closeBundl
679
679
 
680
680
  ### MCP `coursecode_lint` — Build-Time Only
681
681
 
682
- The MCP `coursecode_lint` tool runs the build linter (config validation, CSS class verification, structure checks). It does NOT include runtime errors. For runtime errors and contrast warnings, use `coursecode_errors` (lightweight just errors and console logs) or `coursecode_state` (full state snapshot including errors).
682
+ The MCP `coursecode_lint` tool runs the static/build-time linter (config validation, CSS class verification, structure checks). It does **not** inspect the running preview and does **not** include live runtime, browser console, or Vite build-watch diagnostics.
683
+
684
+ Use:
685
+ - `coursecode_lint` for static preflight validation after source/config edits
686
+ - `coursecode_errors` for the live "what is broken right now?" preview rollup
687
+ - `coursecode_state` when you also need current slide, TOC, engagement, LMS state, and diagnostics
683
688
 
684
689
  ### Shared Rules (`lib/validation-rules.js`)
685
690
 
@@ -861,16 +866,18 @@ The MCP server runs a **persistent headless Chrome** internally via `puppeteer-c
861
866
 
862
867
  ### Preview Server Ownership
863
868
 
864
- The MCP does **not** start or manage the preview server. The preview must be running before using runtime tools:
869
+ The MCP does **not** start or manage the preview server. Runtime tools connect to an already-running preview server.
865
870
 
866
- - **Human**: run `coursecode preview` (or `npm run preview`) in a terminal
867
- - **AI agent**: use your terminal/command execution tool to run `npm run preview`
871
+ - If preview is already running for the current project, use it. Do **not** start a second preview server.
872
+ - If preview is not running, start it in a terminal with `coursecode preview`.
873
+ - For framework development from this repo, use `npm run preview`.
874
+ - AI agents may start preview only via their terminal/command execution tool, and only after confirming preview is not already running or after a runtime MCP tool reports that preview is not running.
868
875
 
869
876
  If the preview is not running, runtime tools fail fast with a clear error message.
870
877
 
871
878
  ### Setup
872
879
 
873
- 1. Start the preview server externally (see above)
880
+ 1. Make sure the preview server is running externally (see above)
874
881
  2. Add to IDE MCP config:
875
882
 
876
883
  ```json
@@ -885,10 +892,12 @@ If the preview is not running, runtime tools fail fast with a clear error messag
885
892
 
886
893
  ### Runtime Tools (require preview server)
887
894
 
895
+ Tool results include machine-readable `structuredContent` plus text content for compatibility. Tool failures use structured error payloads with stable `code`, `message`, `hint`, and optional `details` fields so AI clients can recover without parsing prose.
896
+
888
897
  | Tool | Purpose | Returns |
889
898
  |------|---------|--------|
890
- | `coursecode_state` | Full course snapshot | `{slide, toc, interactions, engagement, lmsState, apiLog, errors, frameworkLogs, consoleLogs}` |
891
- | `coursecode_errors` | Errors + console logs only | `{errors, consoleLogs, count, clean}` — same error sources as `coursecode_state`, without the state payload |
899
+ | `coursecode_state` | Full course snapshot + live diagnostics | `{slide, toc, interactions, engagement, lmsState, apiLog, diagnostics, issues, errors, frameworkLogs, consoleLogs}` |
900
+ | `coursecode_errors` | Live diagnostic rollup only | `{build, runtime, framework, console, issues, errors, count, clean}` — same diagnostic sources as `coursecode_state`, without the state payload |
892
901
  | `coursecode_navigate` | Go to slide by ID | `{slide, interactions, engagement, accessibility}` |
893
902
  | `coursecode_interact` | Set response + evaluate | `{interactionId, response}` → `{correct, score, feedback}` |
894
903
  | `coursecode_screenshot` | Visual capture (JPEG) | Optional `slideId` to navigate first, `fullPage` for scroll capture |
@@ -933,7 +942,7 @@ MCP Server (IDE) ──puppeteer──▶ Headless Chrome ──HTTP──▶ Pr
933
942
  └── Course iframe (CourseCodeAutomation API)
934
943
  ```
935
944
 
936
- - **Preview not running?** → Tools return clear error: "Start preview server first"
945
+ - **Preview not running?** → Tools return a clear error. Start preview externally in a terminal, then retry.
937
946
  - **Chrome not found?** → Install Google Chrome or set `CHROME_PATH` env var
938
947
 
939
948
  ### Pre-Release Responsive Checks (Framework)
@@ -297,7 +297,8 @@ Once connected, your AI assistant gains these capabilities:
297
297
  | `coursecode_screenshot` | Take a screenshot of any slide |
298
298
  | `coursecode_interact` | Answer an interaction and check if it's correct |
299
299
  | `coursecode_reset` | Clear progress and restart the course |
300
- | `coursecode_lint` | Check for errors (bad CSS classes, missing components, config issues) |
300
+ | `coursecode_errors` | Check live preview diagnostics (build, runtime, framework, and console issues) |
301
+ | `coursecode_lint` | Run static preflight checks (bad CSS classes, missing components, config issues) |
301
302
  | `coursecode_component_catalog` | Browse available UI components (tabs, accordion, cards, etc.) |
302
303
  | `coursecode_interaction_catalog` | Browse available interaction types (multiple choice, drag-drop, etc.) |
303
304
  | `coursecode_css_catalog` | Browse available CSS classes by category |
@@ -306,7 +307,7 @@ Once connected, your AI assistant gains these capabilities:
306
307
  | `coursecode_workflow_status` | Get guidance on what to do next based on your project's current state |
307
308
  | `coursecode_build` | Build the course for LMS deployment |
308
309
 
309
- > **Note:** The preview server must be running before using runtime tools like `coursecode_state`, `coursecode_screenshot`, or `coursecode_navigate`. Start it with `coursecode preview` in a terminal.
310
+ > **Note:** The preview server must be running before using runtime tools like `coursecode_state`, `coursecode_errors`, `coursecode_screenshot`, or `coursecode_navigate`. If preview is not already running for this project, start it with `coursecode preview` in a terminal. Do not start a second preview server if one is already running.
310
311
 
311
312
  ### How the Workflow Changes
312
313
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  import interactionRegistry from '../managers/interaction-registry.js';
8
8
  import { courseConfig } from '../../../course/course-config.js';
9
- import { recordInteractionResult } from '../components/interactions/interaction-base.js';
9
+ import { getInteractionState, recordInteractionResult } from '../components/interactions/interaction-base.js';
10
10
  import { logger } from '../utilities/logger.js';
11
11
 
12
12
  /**
@@ -18,11 +18,26 @@ export function createInteractionMethods(logTrace) {
18
18
  return {
19
19
  listInteractions() {
20
20
  const interactions = interactionRegistry.getAll();
21
- const simplifiedList = interactions.map(i => ({
22
- id: i.id,
23
- type: i.type,
24
- description: i.description
25
- }));
21
+ const simplifiedList = interactions.map(i => {
22
+ const savedState = getInteractionState(i.id);
23
+ let response = savedState?.response;
24
+
25
+ if (response === undefined && typeof i.instance?.getResponse === 'function') {
26
+ try {
27
+ response = i.instance.getResponse();
28
+ } catch {
29
+ response = undefined;
30
+ }
31
+ }
32
+
33
+ return {
34
+ id: i.id,
35
+ type: i.type,
36
+ description: i.description,
37
+ hasResponse: response !== null && response !== undefined && response !== '',
38
+ isChecked: savedState?.submitted === true
39
+ };
40
+ });
26
41
  logTrace('listInteractions', { count: simplifiedList.length });
27
42
  return simplifiedList;
28
43
  },
@@ -33,10 +48,23 @@ export function createInteractionMethods(logTrace) {
33
48
  throw new Error(`CourseCodeAutomation: Interaction "${interactionId}" not found on the current slide`);
34
49
  }
35
50
  logTrace('getInteractionMetadata', { interactionId });
51
+ const savedState = getInteractionState(entry.id);
52
+ let response = savedState?.response;
53
+
54
+ if (response === undefined && typeof entry.instance?.getResponse === 'function') {
55
+ try {
56
+ response = entry.instance.getResponse();
57
+ } catch {
58
+ response = undefined;
59
+ }
60
+ }
61
+
36
62
  return {
37
63
  id: entry.id,
38
64
  type: entry.type,
39
- description: entry.description
65
+ description: entry.description,
66
+ hasResponse: response !== null && response !== undefined && response !== '',
67
+ isChecked: savedState?.submitted === true
40
68
  };
41
69
  },
42
70
 
@@ -358,8 +358,8 @@ class HeadlessBrowser {
358
358
 
359
359
  // Navigate to specific slide if requested
360
360
  if (slideId) {
361
- await this.evaluate((id) => {
362
- window.CourseCodeAutomation.goToSlide(id);
361
+ await this.evaluate(async (id) => {
362
+ await window.CourseCodeAutomation.goToSlide(id);
363
363
  }, slideId);
364
364
  // Wait for slide transition
365
365
  await new Promise(resolve => setTimeout(resolve, 500));
@@ -20,7 +20,7 @@ export const TOOLS = [
20
20
  // --- Runtime Tools (require headless browser) ---
21
21
  {
22
22
  name: 'coursecode_state',
23
- description: `Get course state, runtime errors, and warnings in one call. This is the primary tool for checking errors.
23
+ description: `Get course state and live preview diagnostics in one call.
24
24
 
25
25
  Returns:
26
26
  - slide: current slide ID (string)
@@ -29,12 +29,15 @@ Returns:
29
29
  - engagement: slide engagement {complete, percentage, requirements}
30
30
  - lmsState: LMS data {score, completion, success, bookmark, format, objectives, state}
31
31
  - apiLog: last 20 LMS API calls [{timestamp, method, args, result}]
32
- - errors: runtime errors/warnings [{type, message, hint, isWarning}]
32
+ - diagnostics: live issue rollup with build, runtime, framework, console, issues, count, clean
33
+ - issues/errors: flat live issue list [{source, severity, isWarning, type, message, hint?}]
34
+ - runtimeErrors: runtime/debug-panel errors and warnings only
35
+ - buildErrors/buildWarnings: backward-compatible build-watch aliases
33
36
  - frameworkLogs: structured framework log events [{level, domain, operation, message, stack?, timestamp}]
34
37
  - consoleLogs: browser console warnings/errors [{type, text, time}]
35
38
 
36
39
  Use this first to understand the course state before taking actions.
37
- For error checking only (after file edits), prefer coursecode_errors — same error sources, smaller payload.
40
+ For checking what is broken after file edits, prefer coursecode_errors — same live diagnostic sources, smaller payload.
38
41
  Requires preview server to be running.`,
39
42
  inputSchema: {
40
43
  type: 'object',
@@ -48,15 +51,23 @@ Requires preview server to be running.`,
48
51
  },
49
52
  {
50
53
  name: 'coursecode_errors',
51
- description: `Get runtime errors and warnings from the live preview. Uses the same error sources as coursecode_state (preview server errors + browser console) but without the heavyweight state payload.
54
+ description: `Get all current live preview diagnostics without the heavyweight course state payload.
55
+
56
+ This is the primary "what is broken right now?" tool after edits. It aggregates:
57
+ - build: Vite/build-watch errors and warnings from the running preview server
58
+ - runtime: stub LMS/debug-panel errors and warnings
59
+ - framework: CourseCode logger warnings/errors
60
+ - console: browser console warnings/errors
61
+
62
+ This is different from coursecode_lint, which is a static/build-time linter and does not inspect the running preview.
52
63
 
53
64
  Returns:
54
- - errors: [{type, message, hint?, isWarning?}] — preview server errors and warnings
55
- - consoleLogs: [{type, text, time}] browser console warnings/errors
56
- - count: total number of errors + console logs
57
- - clean: true if no errors, warnings, or console issues
65
+ - build/runtime/framework/console: grouped diagnostics by source
66
+ - issues/errors: flat list [{source, severity, isWarning, type, message, hint?}] for agents
67
+ - count: total issue count
68
+ - clean: true if no live issues were found
69
+ - runtimeErrors/frameworkLogs/consoleLogs: source-specific aliases
58
70
 
59
- Use after making file changes to check for breakage without the overhead of coursecode_state.
60
71
  Requires preview server to be running.`,
61
72
  inputSchema: {
62
73
  type: 'object',
@@ -378,16 +389,16 @@ Use to discover available interactions before creating assessments.`,
378
389
  },
379
390
  {
380
391
  name: 'coursecode_lint',
381
- description: `Run the course linter and get structured results.
392
+ description: `Run the static course linter and get structured results.
382
393
 
383
- Always runs build-time lint (config, CSS classes, structure). Does NOT include runtime errors use coursecode_state for runtime errors and contrast warnings when the preview server is running.
394
+ This is a preflight/static validation tool for config, source structure, CSS class names, and schema rules. It does NOT inspect the running preview and does NOT include live runtime, browser console, or Vite build-watch diagnostics. Use coursecode_errors for the live "what is broken right now?" view when preview is running.
384
395
 
385
396
  Returns:
386
397
  - errors: [{slideId?, rule, message, severity, hint?}]
387
398
  - warnings: [{slideId?, rule, message, severity, hint?}]
388
399
  - passed: boolean
389
400
 
390
- Build-time rules (always checked):
401
+ Static rules (always checked):
391
402
  - undefined-css-class: hallucinated or stale class names (with fix suggestions)
392
403
  - unknown-component: unregistered data-component types
393
404
  - requirement-missing-component: engagement requirement without matching component
@@ -396,7 +407,7 @@ Build-time rules (always checked):
396
407
  - assessment-id-mismatch: config ID doesn't match assessment ID
397
408
  - invalid-gating: bad gating condition configuration
398
409
 
399
- Use AFTER making changes to validate the course.`,
410
+ Use after making source/config changes as a fast static check. Then use coursecode_errors against the running preview for live diagnostics.`,
400
411
  inputSchema: {
401
412
  type: 'object',
402
413
  properties: {},
@@ -764,12 +775,16 @@ All runtime tools (state, navigate, interact, screenshot, viewport, reset) execu
764
775
 
765
776
  ### Preview Server Ownership
766
777
  - The MCP does NOT start or manage the preview server
767
- - The preview must be started externally: run \`coursecode preview\` in a terminal (human) or via a terminal/command execution tool (AI agent)
768
- - If preview is not running, runtime tools will fail with a clear error message
778
+ - Before using runtime tools, check whether preview is already running for this project
779
+ - If preview is already running, use it; do not start a second preview server
780
+ - If preview is not running, start it externally in a terminal: run \`coursecode preview\` (or \`npm run preview\` for framework development)
781
+ - AI agents may start preview only via a terminal/command execution tool, and only after a runtime tool reports that preview is not running
769
782
  - The headless browser auto-reconnects when Vite rebuilds (file changes)
770
783
 
771
784
  ### Navigation API
772
- - coursecode_state → get slim TOC with slide IDs, current slide, interactions, engagement, lmsState, apiLog, errors, frameworkLogs, consoleLogs
785
+ - coursecode_state → get slim TOC with slide IDs, current slide, interactions, engagement, lmsState, apiLog, diagnostics, frameworkLogs, consoleLogs
786
+ - coursecode_errors → get live diagnostics only (build/runtime/framework/console); use this after edits
787
+ - coursecode_lint → run static preflight lint only; it is not a live preview diagnostic
773
788
  - coursecode_navigate(slideId) → go to any slide instantly by ID
774
789
  - coursecode_viewport(breakpoint or {width,height}) → set viewport for responsive testing (persists until changed)
775
790
  - coursecode_screenshot(slideId) → navigate + capture in one call (quality modes only, never changes viewport)
package/lib/mcp-server.js CHANGED
@@ -37,6 +37,178 @@ import { TOOLS, buildInstructions, getWorkflowStatusWithInstructions } from './m
37
37
 
38
38
  const DEFAULT_PORT = 4173;
39
39
 
40
+ class McpToolError extends Error {
41
+ constructor(code, message, options = {}) {
42
+ super(message);
43
+ this.name = 'McpToolError';
44
+ this.code = code;
45
+ this.hint = options.hint;
46
+ this.details = options.details;
47
+ }
48
+ }
49
+
50
+ function normalizePort(port) {
51
+ const parsed = parseInt(port ?? DEFAULT_PORT, 10);
52
+ return Number.isFinite(parsed) ? parsed : DEFAULT_PORT;
53
+ }
54
+
55
+ function makeToolResult(result) {
56
+ return {
57
+ content: [{
58
+ type: 'text',
59
+ text: JSON.stringify(result, null, 2)
60
+ }],
61
+ structuredContent: result
62
+ };
63
+ }
64
+
65
+ function normalizeToolError(error) {
66
+ if (error instanceof McpToolError) {
67
+ return {
68
+ code: error.code,
69
+ message: error.message,
70
+ ...(error.hint ? { hint: error.hint } : {}),
71
+ ...(error.details ? { details: error.details } : {})
72
+ };
73
+ }
74
+
75
+ const message = error?.message || String(error);
76
+ if (/Preview server not running/i.test(message)) {
77
+ return {
78
+ code: 'preview_not_running',
79
+ message,
80
+ hint: 'Start the CourseCode preview server for this project, then retry the MCP tool call.'
81
+ };
82
+ }
83
+
84
+ if (/Slide ".*" not found/i.test(message)) {
85
+ return {
86
+ code: 'invalid_slide_id',
87
+ message,
88
+ hint: 'Call coursecode_state to get valid slide IDs, then retry with one of those IDs.'
89
+ };
90
+ }
91
+
92
+ if (/is required|Provide either/i.test(message)) {
93
+ return {
94
+ code: 'invalid_arguments',
95
+ message,
96
+ hint: 'Check the tool input schema and retry with the required arguments.'
97
+ };
98
+ }
99
+
100
+ if (/Unknown tool:/i.test(message)) {
101
+ return {
102
+ code: 'unknown_tool',
103
+ message,
104
+ hint: 'Call tools/list and retry with one of the advertised CourseCode tool names.'
105
+ };
106
+ }
107
+
108
+ return {
109
+ code: 'tool_failed',
110
+ message,
111
+ hint: 'Inspect the message and retry after correcting the underlying issue.'
112
+ };
113
+ }
114
+
115
+ function makeToolErrorResult(error) {
116
+ const normalized = normalizeToolError(error);
117
+ const result = {
118
+ success: false,
119
+ error: normalized,
120
+ code: normalized.code,
121
+ message: normalized.message,
122
+ ...(normalized.hint ? { hint: normalized.hint } : {}),
123
+ ...(normalized.details ? { details: normalized.details } : {})
124
+ };
125
+
126
+ return {
127
+ content: [{
128
+ type: 'text',
129
+ text: JSON.stringify(result, null, 2)
130
+ }],
131
+ structuredContent: result,
132
+ isError: true
133
+ };
134
+ }
135
+
136
+ function normalizeIssue(source, severity, issue) {
137
+ const isWarning = severity === 'warning' || issue?.isWarning === true || issue?.level === 'warn';
138
+ return {
139
+ source,
140
+ severity: isWarning ? 'warning' : 'error',
141
+ isWarning,
142
+ type: issue?.type || issue?.domain || source,
143
+ message: issue?.message || issue?.text || String(issue),
144
+ ...(issue?.hint ? { hint: issue.hint } : {}),
145
+ ...(issue?.operation ? { operation: issue.operation } : {}),
146
+ ...(issue?.time ? { time: issue.time } : {}),
147
+ ...(issue?.timestamp ? { timestamp: issue.timestamp } : {})
148
+ };
149
+ }
150
+
151
+ async function getLiveDiagnostics(port, frameworkLogs = []) {
152
+ const diagnostics = {
153
+ build: { errors: [], warnings: [] },
154
+ runtime: { errors: [], warnings: [] },
155
+ framework: { errors: [], warnings: [] },
156
+ console: { errors: [], warnings: [] },
157
+ issues: []
158
+ };
159
+
160
+ try {
161
+ const buildResp = await fetch(`http://localhost:${port}/__mcp/errors`);
162
+ if (buildResp.ok) {
163
+ const buildData = await buildResp.json();
164
+ diagnostics.build.errors = buildData.errors || [];
165
+ diagnostics.build.warnings = buildData.warnings || [];
166
+ }
167
+ } catch {
168
+ // Preview build diagnostics unavailable — leave empty.
169
+ }
170
+
171
+ try {
172
+ const errResp = await fetch(`http://localhost:${port}/__lms/errors`);
173
+ if (errResp.ok) {
174
+ const errData = await errResp.json();
175
+ diagnostics.runtime.errors = errData.errors || [];
176
+ diagnostics.runtime.warnings = errData.warnings || [];
177
+ }
178
+ } catch {
179
+ // Runtime diagnostics unavailable — leave empty.
180
+ }
181
+
182
+ for (const log of frameworkLogs || []) {
183
+ if (log.level === 'warn') diagnostics.framework.warnings.push(log);
184
+ else if (log.level === 'error' || log.level === 'fatal') diagnostics.framework.errors.push(log);
185
+ }
186
+
187
+ const consoleLogs = headless.getConsoleLogs();
188
+ for (const log of consoleLogs) {
189
+ // logger.warn/error entries are already represented as structured
190
+ // framework diagnostics, so do not double-count their console echo.
191
+ if (/^\[(WARN|ERROR|FATAL)\]/.test(log.text || '')) continue;
192
+ if (log.type === 'warning') diagnostics.console.warnings.push(log);
193
+ else diagnostics.console.errors.push(log);
194
+ }
195
+
196
+ diagnostics.issues = [
197
+ ...diagnostics.build.errors.map(issue => normalizeIssue('build', 'error', issue)),
198
+ ...diagnostics.build.warnings.map(issue => normalizeIssue('build', 'warning', issue)),
199
+ ...diagnostics.runtime.errors.map(issue => normalizeIssue('runtime', 'error', issue)),
200
+ ...diagnostics.runtime.warnings.map(issue => normalizeIssue('runtime', 'warning', issue)),
201
+ ...diagnostics.framework.errors.map(issue => normalizeIssue('framework', 'error', issue)),
202
+ ...diagnostics.framework.warnings.map(issue => normalizeIssue('framework', 'warning', issue)),
203
+ ...diagnostics.console.errors.map(issue => normalizeIssue('console', 'error', issue)),
204
+ ...diagnostics.console.warnings.map(issue => normalizeIssue('console', 'warning', issue))
205
+ ];
206
+
207
+ diagnostics.count = diagnostics.issues.length;
208
+ diagnostics.clean = diagnostics.count === 0;
209
+ return diagnostics;
210
+ }
211
+
40
212
  /**
41
213
  * Ensure headless browser is connected to preview server.
42
214
  * Fails fast if preview is not running — humans own the preview lifecycle.
@@ -60,7 +232,7 @@ async function ensureHeadless(port) {
60
232
  * Create and run the MCP server
61
233
  */
62
234
  export async function startMcpServer(options = {}) {
63
- const port = options.port || DEFAULT_PORT;
235
+ const port = normalizePort(options.port);
64
236
 
65
237
  // Build dynamic instructions for current authoring stage
66
238
  const instructions = await buildInstructions(port);
@@ -68,10 +240,10 @@ export async function startMcpServer(options = {}) {
68
240
  const server = new Server(
69
241
  {
70
242
  name: 'coursecode',
71
- version: '2.0.0',
72
- instructions
243
+ version: '2.0.0'
73
244
  },
74
245
  {
246
+ instructions,
75
247
  capabilities: {
76
248
  tools: {}
77
249
  }
@@ -106,29 +278,32 @@ export async function startMcpServer(options = {}) {
106
278
  lmsState: api.getLmsState()
107
279
  };
108
280
  });
109
- // Read API log and error log from preview server (same data user sees in debug panel)
281
+ // Read API log plus a unified diagnostic rollup from the preview server/headless page.
110
282
  try {
111
- const [logResp, errResp] = await Promise.all([
112
- fetch(`http://localhost:${port}/__lms/log`),
113
- fetch(`http://localhost:${port}/__lms/errors`)
114
- ]);
283
+ const logResp = await fetch(`http://localhost:${port}/__lms/log`);
115
284
  result.apiLog = logResp.ok ? (await logResp.json()).entries?.slice(0, 20) || [] : [];
116
- if (errResp.ok) {
117
- const errData = await errResp.json();
118
- result.errors = [...(errData.errors || []), ...(errData.warnings || [])];
119
- } else {
120
- result.errors = [];
121
- }
122
285
  } catch {
123
286
  result.apiLog = [];
124
- result.errors = [];
125
287
  }
126
- // Append console errors/warnings captured from the page
127
- result.consoleLogs = headless.getConsoleLogs();
288
+ result.diagnostics = await getLiveDiagnostics(port, result.frameworkLogs);
289
+ result.issues = result.diagnostics.issues;
290
+ result.errors = result.diagnostics.issues;
291
+ result.runtimeErrors = [
292
+ ...result.diagnostics.runtime.errors,
293
+ ...result.diagnostics.runtime.warnings
294
+ ];
295
+ result.buildErrors = result.diagnostics.build.errors;
296
+ result.buildWarnings = result.diagnostics.build.warnings;
297
+ result.consoleLogs = [
298
+ ...result.diagnostics.console.errors,
299
+ ...result.diagnostics.console.warnings
300
+ ];
128
301
  break;
129
302
 
130
303
  case 'coursecode_navigate':
131
- if (!args?.slideId) throw new Error('slideId is required');
304
+ if (!args?.slideId) throw new McpToolError('missing_required_argument', 'slideId is required', {
305
+ hint: 'Call coursecode_state to get valid slide IDs, then pass one as slideId.'
306
+ });
132
307
  await ensureHeadless(port);
133
308
  // Apply accessibility preferences before navigation
134
309
  if (args.theme || args.highContrast !== undefined) {
@@ -147,11 +322,14 @@ export async function startMcpServer(options = {}) {
147
322
  return toc.some(item => item.id === slideId);
148
323
  }, args.slideId);
149
324
  if (!validSlide) {
150
- throw new Error(`Slide "${args.slideId}" not found. Use coursecode_state to get valid slide IDs.`);
325
+ throw new McpToolError('invalid_slide_id', `Slide "${args.slideId}" not found.`, {
326
+ hint: 'Call coursecode_state to get valid slide IDs, then retry with one of those IDs.',
327
+ details: { slideId: args.slideId }
328
+ });
151
329
  }
152
330
  }
153
- await headless.evaluate((slideId) => {
154
- window.CourseCodeAutomation.goToSlide(slideId);
331
+ await headless.evaluate(async (slideId) => {
332
+ await window.CourseCodeAutomation.goToSlide(slideId);
155
333
  }, args.slideId);
156
334
  // State updates asynchronously after navigation
157
335
  await new Promise(resolve => setTimeout(resolve, 50));
@@ -167,8 +345,12 @@ export async function startMcpServer(options = {}) {
167
345
  break;
168
346
 
169
347
  case 'coursecode_interact':
170
- if (!args?.interactionId) throw new Error('interactionId is required');
171
- if (args.response === undefined) throw new Error('response is required');
348
+ if (!args?.interactionId) throw new McpToolError('missing_required_argument', 'interactionId is required', {
349
+ hint: 'Call coursecode_state on the target slide to list valid interaction IDs.'
350
+ });
351
+ if (args.response === undefined) throw new McpToolError('missing_required_argument', 'response is required', {
352
+ hint: 'Pass a response value matching the interaction type.'
353
+ });
172
354
  await ensureHeadless(port);
173
355
  result = await headless.evaluate(({ interactionId, response }) => {
174
356
  const api = window.CourseCodeAutomation;
@@ -204,7 +386,10 @@ export async function startMcpServer(options = {}) {
204
386
  return toc.some(item => item.id === slideId);
205
387
  }, args.slideId);
206
388
  if (!validSlide) {
207
- throw new Error(`Slide "${args.slideId}" not found. Use coursecode_state to get valid slide IDs.`);
389
+ throw new McpToolError('invalid_slide_id', `Slide "${args.slideId}" not found.`, {
390
+ hint: 'Call coursecode_state to get valid slide IDs, then retry with one of those IDs.',
391
+ details: { slideId: args.slideId }
392
+ });
208
393
  }
209
394
  }
210
395
  result = await headless.screenshot({
@@ -228,30 +413,33 @@ export async function startMcpServer(options = {}) {
228
413
  } else if (args?.width && args?.height) {
229
414
  result = await headless.setViewport({ width: args.width, height: args.height });
230
415
  } else {
231
- throw new Error('Provide either a breakpoint name or both width and height.');
416
+ throw new McpToolError('missing_required_argument', 'Provide either a breakpoint name or both width and height.', {
417
+ hint: 'Use breakpoint: "desktop" or pass explicit width and height numbers.'
418
+ });
232
419
  }
233
420
  break;
234
421
 
235
422
  case 'coursecode_errors': {
236
- // Same error-gathering mechanism as coursecode_state,
237
- // but without the heavyweight state payload (TOC, interactions, etc.)
423
+ // Live diagnostic rollup without the heavyweight state payload (TOC, interactions, etc.).
238
424
  await ensureHeadless(port);
239
- let errors = [];
240
- try {
241
- const errResp = await fetch(`http://localhost:${port}/__lms/errors`);
242
- if (errResp.ok) {
243
- const errData = await errResp.json();
244
- errors = [...(errData.errors || []), ...(errData.warnings || [])];
245
- }
246
- } catch {
247
- // Preview server unreachable — errors array stays empty
248
- }
249
- const consoleLogs = headless.getConsoleLogs();
425
+ const frameworkLogs = await headless.evaluate(() => {
426
+ return window.CourseCodeAutomation.getFrameworkLogs();
427
+ });
428
+ const diagnostics = await getLiveDiagnostics(port, frameworkLogs);
250
429
  result = {
251
- errors,
252
- consoleLogs,
253
- count: errors.length + consoleLogs.length,
254
- clean: errors.length === 0 && consoleLogs.length === 0
430
+ ...diagnostics,
431
+ // Convenience aliases for agents that expect a flat list.
432
+ issues: diagnostics.issues,
433
+ errors: diagnostics.issues,
434
+ runtimeErrors: [
435
+ ...diagnostics.runtime.errors,
436
+ ...diagnostics.runtime.warnings
437
+ ],
438
+ frameworkLogs,
439
+ consoleLogs: [
440
+ ...diagnostics.console.errors,
441
+ ...diagnostics.console.warnings
442
+ ]
255
443
  };
256
444
  break;
257
445
  }
@@ -295,23 +483,15 @@ export async function startMcpServer(options = {}) {
295
483
 
296
484
 
297
485
  default:
298
- throw new Error(`Unknown tool: ${name}`);
486
+ throw new McpToolError('unknown_tool', `Unknown tool: ${name}`, {
487
+ hint: 'Call tools/list and retry with one of the advertised CourseCode tool names.',
488
+ details: { name }
489
+ });
299
490
  }
300
-
301
- return {
302
- content: [{
303
- type: 'text',
304
- text: JSON.stringify(result, null, 2)
305
- }]
306
- };
491
+
492
+ return makeToolResult(result);
307
493
  } catch (error) {
308
- return {
309
- content: [{
310
- type: 'text',
311
- text: `Error: ${error.message}`
312
- }],
313
- isError: true
314
- };
494
+ return makeToolErrorResult(error);
315
495
  }
316
496
  });
317
497
 
@@ -355,6 +535,7 @@ export async function startMcpServer(options = {}) {
355
535
  if (process.argv[1]?.endsWith('mcp-server.js')) {
356
536
  const args = process.argv.slice(2);
357
537
  const portArg = args.find(a => a.startsWith('--port='));
358
- const port = portArg ? parseInt(portArg.split('=')[1], 10) : DEFAULT_PORT;
538
+ const portValue = portArg ? portArg.split('=')[1] : args[args.indexOf('--port') + 1];
539
+ const port = normalizePort(portValue);
359
540
  startMcpServer({ port });
360
541
  }
@@ -269,6 +269,11 @@ export async function previewServer(options = {}) {
269
269
  viteProcess.stdout.on('data', (data) => {
270
270
  const output = data.toString();
271
271
  process.stdout.write(output);
272
+ if (output.includes('Building...')) {
273
+ buildState.errors = [];
274
+ buildState.warnings = [];
275
+ buildState.lastBuildSuccess = false;
276
+ }
272
277
  if (output.includes('Build complete')) {
273
278
  buildState.lastBuildTime = new Date().toISOString();
274
279
  buildState.lastBuildSuccess = true;
package/lib/upgrade.js CHANGED
@@ -83,6 +83,36 @@ function copyFileWithBackup(src, dest) {
83
83
  return { updated: true, backup: null };
84
84
  }
85
85
 
86
+ export function addMissingRuntimeDependencies(projectPkg, templatePkg) {
87
+ const requiredDeps = templatePkg.dependencies || {};
88
+ const existingDeps = projectPkg.dependencies || {};
89
+ const existingDevDeps = projectPkg.devDependencies || {};
90
+ const added = [];
91
+
92
+ for (const [name, version] of Object.entries(requiredDeps)) {
93
+ if (existingDeps[name]) {
94
+ continue;
95
+ }
96
+
97
+ existingDeps[name] = existingDevDeps[name] || version;
98
+ delete existingDevDeps[name];
99
+ added.push(name);
100
+ }
101
+
102
+ if (added.length > 0) {
103
+ projectPkg.dependencies = Object.fromEntries(
104
+ Object.entries(existingDeps).sort(([a], [b]) => a.localeCompare(b))
105
+ );
106
+ if (projectPkg.devDependencies) {
107
+ projectPkg.devDependencies = Object.fromEntries(
108
+ Object.entries(existingDevDeps).sort(([a], [b]) => a.localeCompare(b))
109
+ );
110
+ }
111
+ }
112
+
113
+ return added;
114
+ }
115
+
86
116
  export async function upgrade(options = {}) {
87
117
  const cwd = process.cwd();
88
118
  const rcPath = path.join(cwd, '.coursecoderc.json');
@@ -109,6 +139,7 @@ export async function upgrade(options = {}) {
109
139
  // Get CLI version (this is the version we'll upgrade to)
110
140
  const cliPkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
111
141
  const targetVersion = cliPkg.version;
142
+ const templatePkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'template', 'package.json'), 'utf-8'));
112
143
 
113
144
  console.log(`
114
145
  📦 CourseCode Upgrade
@@ -138,6 +169,7 @@ export async function upgrade(options = {}) {
138
169
  - framework/ (replace entirely)
139
170
  - schemas/ (replace entirely)
140
171
  - lib/manifest/ (replace entirely)
172
+ - package.json (add missing runtime dependencies)
141
173
  - .coursecoderc.json (update version)`;
142
174
 
143
175
  if (options.configs) {
@@ -150,7 +182,6 @@ export async function upgrade(options = {}) {
150
182
 
151
183
  Would NOT touch:
152
184
  - course/ (your content)${!options.configs ? '\n - vite.config.js\n - eslint.config.js' : ''}
153
- - package.json
154
185
  - .env
155
186
  `;
156
187
  console.log(dryRunMsg);
@@ -231,6 +262,18 @@ export async function upgrade(options = {}) {
231
262
  rcConfig.upgradedFrom = currentVersion;
232
263
  fs.writeFileSync(rcPath, JSON.stringify(rcConfig, null, 2));
233
264
 
265
+ // Add any runtime dependencies introduced by the new framework while preserving
266
+ // the project's existing version ranges.
267
+ const pkgPath = path.join(cwd, 'package.json');
268
+ let addedRuntimeDeps = [];
269
+ if (fs.existsSync(pkgPath)) {
270
+ const projectPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
271
+ addedRuntimeDeps = addMissingRuntimeDependencies(projectPkg, templatePkg);
272
+ if (addedRuntimeDeps.length > 0) {
273
+ fs.writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
274
+ }
275
+ }
276
+
234
277
  let successMsg = `
235
278
  ✅ Upgrade complete!
236
279
 
@@ -238,6 +281,15 @@ export async function upgrade(options = {}) {
238
281
 
239
282
  Your course/ directory was not modified.`;
240
283
 
284
+ if (addedRuntimeDeps.length > 0) {
285
+ successMsg += `
286
+
287
+ Runtime dependencies added to package.json:
288
+ - ${addedRuntimeDeps.join('\n - ')}
289
+
290
+ Run npm install to update node_modules and your lockfile.`;
291
+ }
292
+
241
293
  if (configUpdates.length > 0) {
242
294
  successMsg += `
243
295
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.43",
3
+ "version": "0.1.46",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -190,6 +190,7 @@ function scanDistFiles() {
190
190
  function scormPostBuild(isDev) {
191
191
  return {
192
192
  name: 'scorm-post-build',
193
+ enforce: 'post',
193
194
  buildStart() {
194
195
  if (isDev) console.log('🔨 Building...');
195
196
  },
@@ -251,7 +252,7 @@ function scormPostBuild(isDev) {
251
252
  console.log('\n📦 Creating external hosting package archives...');
252
253
  await createExternalPackagesForClients({ rootDir: ROOT_DIR, config });
253
254
  }
254
- console.log('\n Package built successfully');
255
+ console.log('\n Package archive created');
255
256
  } else {
256
257
  console.log('✅ Build complete\n');
257
258
  }