@epic-web/workshop-mcp 6.60.0 → 6.61.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.
@@ -110,6 +110,60 @@ export declare const toolDocs: {
110
110
  openWorldHint: false;
111
111
  };
112
112
  };
113
+ list_saved_playgrounds: {
114
+ title: string;
115
+ summary: string;
116
+ inputs: {
117
+ name: string;
118
+ type: string;
119
+ required: true;
120
+ description: string;
121
+ examples: string[];
122
+ }[];
123
+ returns: string;
124
+ examples: {
125
+ description: string;
126
+ params: string;
127
+ }[];
128
+ nextSteps: string[];
129
+ errorNextSteps: string[];
130
+ annotations: {
131
+ readOnlyHint: true;
132
+ destructiveHint: false;
133
+ idempotentHint: true;
134
+ openWorldHint: false;
135
+ };
136
+ };
137
+ set_saved_playground: {
138
+ title: string;
139
+ summary: string;
140
+ inputs: ({
141
+ name: string;
142
+ type: string;
143
+ required: true;
144
+ description: string;
145
+ examples: string[];
146
+ } | {
147
+ name: string;
148
+ type: string;
149
+ required: false;
150
+ description: string;
151
+ examples: string[];
152
+ })[];
153
+ returns: string;
154
+ examples: {
155
+ description: string;
156
+ params: string;
157
+ }[];
158
+ nextSteps: string[];
159
+ errorNextSteps: string[];
160
+ annotations: {
161
+ readOnlyHint: false;
162
+ destructiveHint: false;
163
+ idempotentHint: false;
164
+ openWorldHint: false;
165
+ };
166
+ };
113
167
  update_progress: {
114
168
  title: string;
115
169
  summary: string;
@@ -5,6 +5,7 @@ Quick start
5
5
  - Use \`set_playground\` to move to a step, then \`open_exercise_step_files\` to open relevant files.
6
6
 
7
7
  Default behavior
8
+ - Use \`list_saved_playgrounds\` and \`set_saved_playground\` to restore saved copies when persistence is enabled.
8
9
  - \`workshopDirectory\` is required and must be an absolute path to the workshop root.
9
10
  - Passing a \`/playground\` path is normalized to the workshop root.
10
11
  - The user's work-in-progress lives in the \`playground\` directory.
@@ -141,6 +142,88 @@ export const toolDocs = {
141
142
  openWorldHint: false,
142
143
  },
143
144
  },
145
+ list_saved_playgrounds: {
146
+ title: 'List Saved Playgrounds',
147
+ summary: 'List saved playground copies when playground persistence is enabled.',
148
+ inputs: [
149
+ {
150
+ name: 'workshopDirectory',
151
+ type: 'string',
152
+ required: true,
153
+ description: 'Absolute path to the workshop root.',
154
+ examples: ['/Users/alice/workshops/react-fundamentals'],
155
+ },
156
+ ],
157
+ returns: '{ savedPlaygrounds: [{ id, appName, displayName, createdAt, createdAtMs, fullPath }] }',
158
+ examples: [
159
+ {
160
+ description: 'List saved playgrounds',
161
+ params: '{ "workshopDirectory": "/Users/alice/workshops/react" }',
162
+ },
163
+ ],
164
+ nextSteps: [
165
+ 'Call `set_saved_playground` with a savedPlaygroundId to restore a copy.',
166
+ ],
167
+ errorNextSteps: [
168
+ 'Enable playground persistence in Preferences and set the playground at least once.',
169
+ ],
170
+ annotations: {
171
+ readOnlyHint: true,
172
+ destructiveHint: false,
173
+ idempotentHint: true,
174
+ openWorldHint: false,
175
+ },
176
+ },
177
+ set_saved_playground: {
178
+ title: 'Set Saved Playground',
179
+ summary: 'Restore the playground from a saved copy.',
180
+ inputs: [
181
+ {
182
+ name: 'workshopDirectory',
183
+ type: 'string',
184
+ required: true,
185
+ description: 'Absolute path to the workshop root.',
186
+ examples: ['/Users/alice/workshops/react-fundamentals'],
187
+ },
188
+ {
189
+ name: 'savedPlaygroundId',
190
+ type: 'string',
191
+ required: false,
192
+ description: 'Saved playground id (directory name). Omit to restore the most recent saved copy.',
193
+ examples: ['2026.01.18_11.12.00_01.01.problem'],
194
+ },
195
+ {
196
+ name: 'latest',
197
+ type: 'boolean',
198
+ required: false,
199
+ description: 'Use the most recent saved playground when true.',
200
+ examples: ['true'],
201
+ },
202
+ ],
203
+ returns: '{ savedPlayground: { id, appName, displayName, createdAt, fullPath } }',
204
+ examples: [
205
+ {
206
+ description: 'Restore the most recent saved playground',
207
+ params: '{ "workshopDirectory": "/Users/alice/workshops/react" }',
208
+ },
209
+ {
210
+ description: 'Restore a specific saved playground',
211
+ params: '{ "workshopDirectory": "/Users/alice/workshops/react", "savedPlaygroundId": "2026.01.18_11.12.00_01.01.problem" }',
212
+ },
213
+ ],
214
+ nextSteps: [
215
+ 'Open relevant files with `open_exercise_step_files` or `open_file`.',
216
+ ],
217
+ errorNextSteps: [
218
+ 'Call `list_saved_playgrounds` to get valid savedPlaygroundId values.',
219
+ ],
220
+ annotations: {
221
+ readOnlyHint: false,
222
+ destructiveHint: false,
223
+ idempotentHint: false,
224
+ openWorldHint: false,
225
+ },
226
+ },
144
227
  update_progress: {
145
228
  title: 'Update Progress',
146
229
  summary: 'Mark an Epic lesson as complete or incomplete for the current user.',
package/dist/tools.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { invariant } from '@epic-web/invariant';
3
- import { getAppByName, getApps, getExercise, getExerciseApp, getExercises, getPlaygroundApp, getPlaygroundAppName, isExerciseStepApp, isProblemApp, setPlayground, } from '@epic-web/workshop-utils/apps.server';
3
+ import { getAppByName, getAppDisplayName, getApps, getExercise, getExerciseApp, getExercises, getPlaygroundApp, getPlaygroundAppName, getSavedPlaygrounds, isExerciseStepApp, isProblemApp, setPlayground, } from '@epic-web/workshop-utils/apps.server';
4
4
  import { deleteCache } from '@epic-web/workshop-utils/cache.server';
5
5
  import { getWorkshopConfig } from '@epic-web/workshop-utils/config.server';
6
- import { getAuthInfo, logout, setAuthInfo, } from '@epic-web/workshop-utils/db.server';
6
+ import { getAuthInfo, getPreferences, logout, setAuthInfo, } from '@epic-web/workshop-utils/db.server';
7
7
  import { getDiffFiles } from '@epic-web/workshop-utils/diff.server';
8
8
  import { getProgress, getUserInfo, updateProgress, } from '@epic-web/workshop-utils/epic-api.server';
9
9
  import { launchEditor } from '@epic-web/workshop-utils/launch-editor.server';
@@ -62,6 +62,15 @@ function createToolErrorResult(toolName, error) {
62
62
  });
63
63
  return { ...response, isError: true };
64
64
  }
65
+ function formatSavedPlaygroundTimestamp(createdAt) {
66
+ const createdAtDate = new Date(createdAt);
67
+ if (Number.isNaN(createdAtDate.getTime()))
68
+ return createdAt;
69
+ return new Intl.DateTimeFormat(undefined, {
70
+ dateStyle: 'medium',
71
+ timeStyle: 'short',
72
+ }).format(createdAtDate);
73
+ }
65
74
  function parseResourceText(resource) {
66
75
  if (typeof resource.text === 'string') {
67
76
  try {
@@ -308,6 +317,82 @@ export function initTools(server) {
308
317
  },
309
318
  });
310
319
  });
320
+ registerTool(server, 'list_saved_playgrounds', {
321
+ workshopDirectory: workshopDirectoryInputSchema,
322
+ }, async ({ workshopDirectory }) => {
323
+ await handleWorkshopDirectory(workshopDirectory);
324
+ const persistEnabled = (await getPreferences())?.playground?.persist ?? false;
325
+ invariant(persistEnabled, 'Playground persistence is disabled. Enable it in Preferences to use saved playgrounds.');
326
+ const [savedPlaygrounds, apps] = await Promise.all([
327
+ getSavedPlaygrounds(),
328
+ getApps(),
329
+ ]);
330
+ const savedPlaygroundEntries = savedPlaygrounds.map((entry) => {
331
+ const matchingApp = apps.find((app) => app.name === entry.appName);
332
+ const displayName = matchingApp
333
+ ? getAppDisplayName(matchingApp, apps)
334
+ : entry.appName;
335
+ return { ...entry, displayName };
336
+ });
337
+ const details = savedPlaygroundEntries.slice(0, 5).map((entry) => {
338
+ const timestamp = formatSavedPlaygroundTimestamp(entry.createdAt);
339
+ return `${entry.displayName} (${entry.appName}) — ${timestamp} — ${entry.id}`;
340
+ });
341
+ const summary = savedPlaygroundEntries.length
342
+ ? `${savedPlaygroundEntries.length} saved playgrounds found.`
343
+ : 'No saved playgrounds found.';
344
+ return createToolResponse({
345
+ toolName: 'list_saved_playgrounds',
346
+ summary,
347
+ details: details.length ? details : undefined,
348
+ structuredContent: {
349
+ savedPlaygrounds: savedPlaygroundEntries,
350
+ },
351
+ });
352
+ });
353
+ registerTool(server, 'set_saved_playground', {
354
+ workshopDirectory: workshopDirectoryInputSchema,
355
+ savedPlaygroundId: z
356
+ .string()
357
+ .optional()
358
+ .describe('Saved playground id to restore (directory name).'),
359
+ latest: z
360
+ .boolean()
361
+ .optional()
362
+ .default(false)
363
+ .describe('Use the most recent saved playground when true.'),
364
+ }, async ({ workshopDirectory, savedPlaygroundId, latest }) => {
365
+ await handleWorkshopDirectory(workshopDirectory);
366
+ const persistEnabled = (await getPreferences())?.playground?.persist ?? false;
367
+ invariant(persistEnabled, 'Playground persistence is disabled. Enable it in Preferences to use saved playgrounds.');
368
+ const [savedPlaygrounds, apps] = await Promise.all([
369
+ getSavedPlaygrounds(),
370
+ getApps(),
371
+ ]);
372
+ invariant(savedPlaygrounds.length, 'No saved playgrounds found.');
373
+ const useLatest = latest || !savedPlaygroundId;
374
+ const selected = savedPlaygroundId
375
+ ? savedPlaygrounds.find((entry) => entry.id === savedPlaygroundId)
376
+ : useLatest
377
+ ? savedPlaygrounds[0]
378
+ : undefined;
379
+ invariant(selected, `Saved playground not found: ${savedPlaygroundId}`);
380
+ await setPlayground(selected.fullPath);
381
+ const matchingApp = apps.find((app) => app.name === selected.appName);
382
+ const displayName = matchingApp
383
+ ? getAppDisplayName(matchingApp, apps)
384
+ : selected.appName;
385
+ return createToolResponse({
386
+ toolName: 'set_saved_playground',
387
+ summary: `Playground set from saved copy: ${displayName}.`,
388
+ structuredContent: {
389
+ savedPlayground: {
390
+ ...selected,
391
+ displayName,
392
+ },
393
+ },
394
+ });
395
+ });
311
396
  registerTool(server, 'update_progress', {
312
397
  workshopDirectory: workshopDirectoryInputSchema,
313
398
  epicLessonSlug: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-mcp",
3
- "version": "6.60.0",
3
+ "version": "6.61.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@epic-web/invariant": "^1.0.0",
38
- "@epic-web/workshop-utils": "6.60.0",
38
+ "@epic-web/workshop-utils": "6.61.0",
39
39
  "@mcp-ui/server": "^5.13.1",
40
40
  "@modelcontextprotocol/sdk": "^1.21.1",
41
41
  "@sentry/node": "^10.25.0",