@epic-web/workshop-mcp 6.38.0 → 6.39.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/dist/esm/index.js +5 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/resources.d.ts +17 -0
- package/dist/esm/resources.d.ts.map +1 -1
- package/dist/esm/resources.js +195 -75
- package/dist/esm/resources.js.map +1 -1
- package/dist/esm/tools.d.ts.map +1 -1
- package/dist/esm/tools.js +391 -78
- package/dist/esm/tools.js.map +1 -1
- package/dist/esm/utils.d.ts +2 -0
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +3 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -2
package/dist/esm/tools.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { invariant } from '@epic-web/invariant';
|
|
2
|
-
import { getApps, getExerciseApp, getPlaygroundAppName, isExerciseStepApp, isProblemApp, setPlayground, } from '@epic-web/workshop-utils/apps.server';
|
|
3
|
+
import { getAppByName, getApps, getExercise, getExerciseApp, getExercises, getPlaygroundApp, getPlaygroundAppName, isExerciseStepApp, isProblemApp, setPlayground, } from '@epic-web/workshop-utils/apps.server';
|
|
3
4
|
import { deleteCache } from '@epic-web/workshop-utils/cache.server';
|
|
4
5
|
import { getWorkshopConfig } from '@epic-web/workshop-utils/config.server';
|
|
5
6
|
import { getAuthInfo, logout, setAuthInfo, } from '@epic-web/workshop-utils/db.server';
|
|
7
|
+
import { getDiffFiles } from '@epic-web/workshop-utils/diff.server';
|
|
6
8
|
import { getProgress, getUserInfo, updateProgress, } from '@epic-web/workshop-utils/epic-api.server';
|
|
9
|
+
import { launchEditor } from '@epic-web/workshop-utils/launch-editor.server';
|
|
10
|
+
// @ts-ignore 🤷♂️ tshy doesn't like this
|
|
11
|
+
import { createUIResource } from '@mcp-ui/server';
|
|
7
12
|
import * as client from 'openid-client';
|
|
8
13
|
import { z } from 'zod';
|
|
9
14
|
import { quizMe, quizMeInputSchema } from './prompts.js';
|
|
10
|
-
import { diffBetweenAppsResource, exerciseContextResource, exerciseStepProgressDiffResource, userAccessResource, userInfoResource, userProgressResource, workshopContextResource, } from './resources.js';
|
|
11
|
-
import { handleWorkshopDirectory, workshopDirectoryInputSchema, } from './utils.js';
|
|
15
|
+
import { diffBetweenAppsResource, exerciseContextResource, exerciseStepContextResource, exerciseStepProgressDiffResource, userAccessResource, userInfoResource, userProgressResource, workshopContextResource, } from './resources.js';
|
|
16
|
+
import { handleWorkshopDirectory, readInWorkshop, safeReadFile, workshopDirectoryInputSchema, } from './utils.js';
|
|
12
17
|
// not enough support for this yet
|
|
13
18
|
const clientSupportsEmbeddedResources = false;
|
|
14
19
|
export function initTools(server) {
|
|
@@ -153,56 +158,58 @@ An error will be returned if no app is found for the given arguments.
|
|
|
153
158
|
.describe('The type of app to set the playground to'),
|
|
154
159
|
},
|
|
155
160
|
}, async ({ workshopDirectory, exerciseNumber, stepNumber, type }) => {
|
|
156
|
-
|
|
161
|
+
await handleWorkshopDirectory(workshopDirectory);
|
|
157
162
|
const authInfo = await getAuthInfo();
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (nextProgress
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
nextProgress.type === '
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
nextProgress.type === 'workshop-
|
|
203
|
-
|
|
163
|
+
if (!exerciseNumber) {
|
|
164
|
+
if (authInfo) {
|
|
165
|
+
const progress = await getProgress();
|
|
166
|
+
const scoreProgress = (a) => {
|
|
167
|
+
if (a.type === 'workshop-instructions')
|
|
168
|
+
return 0;
|
|
169
|
+
if (a.type === 'workshop-finished')
|
|
170
|
+
return 10000;
|
|
171
|
+
if (a.type === 'instructions')
|
|
172
|
+
return a.exerciseNumber * 100;
|
|
173
|
+
if (a.type === 'step')
|
|
174
|
+
return a.exerciseNumber * 100 + a.stepNumber;
|
|
175
|
+
if (a.type === 'finished')
|
|
176
|
+
return a.exerciseNumber * 100 + 100;
|
|
177
|
+
if (a.type === 'unknown')
|
|
178
|
+
return 100000;
|
|
179
|
+
return -1;
|
|
180
|
+
};
|
|
181
|
+
const sortedProgress = progress.sort((a, b) => {
|
|
182
|
+
return scoreProgress(a) - scoreProgress(b);
|
|
183
|
+
});
|
|
184
|
+
const nextProgress = sortedProgress.find((p) => !p.epicCompletedAt);
|
|
185
|
+
if (nextProgress) {
|
|
186
|
+
if (nextProgress.type === 'step') {
|
|
187
|
+
const exerciseApp = await getExerciseApp({
|
|
188
|
+
exerciseNumber: nextProgress.exerciseNumber.toString(),
|
|
189
|
+
stepNumber: nextProgress.stepNumber.toString(),
|
|
190
|
+
type: 'problem',
|
|
191
|
+
});
|
|
192
|
+
invariant(exerciseApp, 'No exercise app found');
|
|
193
|
+
await setPlayground(exerciseApp.fullPath);
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: `Playground set to ${exerciseApp.exerciseNumber}.${exerciseApp.stepNumber}.${exerciseApp.type}`,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (nextProgress.type === 'instructions' ||
|
|
204
|
+
nextProgress.type === 'finished') {
|
|
205
|
+
throw new Error(`The user needs to mark the ${nextProgress.exerciseNumber} ${nextProgress.type === 'instructions' ? 'instructions' : 'finished'} as complete before they can continue. Have them watch the video at ${nextProgress.epicLessonUrl}, then mark it as complete.`);
|
|
206
|
+
}
|
|
207
|
+
if (nextProgress.type === 'workshop-instructions' ||
|
|
208
|
+
nextProgress.type === 'workshop-finished') {
|
|
209
|
+
throw new Error(`The user needs to mark the ${nextProgress.exerciseNumber} ${nextProgress.type === 'workshop-instructions' ? 'Workshop instructions' : 'Workshop finished'} as complete before they can continue. Have them watch the video at ${nextProgress.epicLessonUrl}, then mark it as complete.`);
|
|
210
|
+
}
|
|
211
|
+
throw new Error(`The user needs to mark ${nextProgress.epicLessonSlug} as complete before they can continue. Have them watch the video at ${nextProgress.epicLessonUrl}, then mark it as complete.`);
|
|
204
212
|
}
|
|
205
|
-
throw new Error(`The user needs to mark ${nextProgress.epicLessonSlug} as complete before they can continue. Have them watch the video at ${nextProgress.epicLessonUrl}, then mark it as complete.`);
|
|
206
213
|
}
|
|
207
214
|
}
|
|
208
215
|
const apps = await getApps();
|
|
@@ -230,17 +237,12 @@ An error will be returned if no app is found for the given arguments.
|
|
|
230
237
|
}
|
|
231
238
|
invariant(desiredApp, `No app found for values derived by the arguments: ${exerciseNumber}.${stepNumber}.${type}`);
|
|
232
239
|
await setPlayground(desiredApp.fullPath);
|
|
233
|
-
const exerciseContext = await exerciseContextResource.getResource({
|
|
234
|
-
workshopDirectory,
|
|
235
|
-
exerciseNumber: desiredApp.exerciseNumber,
|
|
236
|
-
});
|
|
237
240
|
return {
|
|
238
241
|
content: [
|
|
239
242
|
{
|
|
240
243
|
type: 'text',
|
|
241
244
|
text: `Playground set to ${desiredApp.name}.`,
|
|
242
245
|
},
|
|
243
|
-
getEmbeddedResourceContent(exerciseContext),
|
|
244
246
|
],
|
|
245
247
|
};
|
|
246
248
|
});
|
|
@@ -352,28 +354,6 @@ If the user asks for the diff for 2.3, then use 02.03.problem for app1 and 02.03
|
|
|
352
354
|
server.registerTool('get_exercise_step_progress_diff', {
|
|
353
355
|
description: `
|
|
354
356
|
Intended to help a student understand what work they still have to complete.
|
|
355
|
-
|
|
356
|
-
This is not a typical diff. It's a diff of the user's work in progress against
|
|
357
|
-
the solution.
|
|
358
|
-
|
|
359
|
-
- Lines starting with \`-\` show code that needs to be removed from the user's solution
|
|
360
|
-
- Lines starting with \`+\` show code that needs to be added to the user's solution
|
|
361
|
-
- If there are differences, the user's work is incomplete
|
|
362
|
-
|
|
363
|
-
Only tell the user they have more work to do if the diff output affects the
|
|
364
|
-
required behavior, API, or user experience. If the differences are only
|
|
365
|
-
stylistic or organizational, explain that things look different, but they are
|
|
366
|
-
still valid and ready to be tested.
|
|
367
|
-
|
|
368
|
-
If there's a diff with significant changes, you should explain what the changes
|
|
369
|
-
are and their significance. Be brief. Let them tell you whether they need you to
|
|
370
|
-
elaborate.
|
|
371
|
-
|
|
372
|
-
The output for this changes over time so it's useful to call multiple times.
|
|
373
|
-
|
|
374
|
-
For additional context, you can use the \`get_exercise_instructions\` tool
|
|
375
|
-
to get the instructions for the current exercise step to help explain the
|
|
376
|
-
significance of changes.
|
|
377
357
|
`.trim(),
|
|
378
358
|
inputSchema: exerciseStepProgressDiffResource.inputSchema,
|
|
379
359
|
}, async ({ workshopDirectory }) => {
|
|
@@ -381,10 +361,100 @@ significance of changes.
|
|
|
381
361
|
const resource = await exerciseStepProgressDiffResource.getResource({
|
|
382
362
|
workshopDirectory,
|
|
383
363
|
});
|
|
364
|
+
return {
|
|
365
|
+
content: [
|
|
366
|
+
createText(getDiffInstructionText()),
|
|
367
|
+
getEmbeddedResourceContent(resource),
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
server.registerTool('get_exercise_step_context', {
|
|
372
|
+
description: `
|
|
373
|
+
Intended to help a student understand what they need to do for a specific
|
|
374
|
+
exercise step.
|
|
375
|
+
|
|
376
|
+
This returns the instructions MDX content for the specified exercise step's
|
|
377
|
+
problem and solution. If the user has the paid version of the workshop, it will also
|
|
378
|
+
include the transcript from each of the videos as well.
|
|
379
|
+
|
|
380
|
+
The output for this will rarely change, so it's unnecessary to call this tool
|
|
381
|
+
more than once for the same exercise step.
|
|
382
|
+
|
|
383
|
+
\`get_exercise_step_context\` is often best when used with the
|
|
384
|
+
\`get_exercise_step_progress_diff\` tool to help a student understand what
|
|
385
|
+
work they still need to do and answer any questions about the exercise step.
|
|
386
|
+
`.trim(),
|
|
387
|
+
inputSchema: exerciseStepContextResource.inputSchema,
|
|
388
|
+
}, async ({ workshopDirectory, exerciseNumber, stepNumber }) => {
|
|
389
|
+
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
390
|
+
const resource = await exerciseStepContextResource.getResource({
|
|
391
|
+
workshopDirectory,
|
|
392
|
+
exerciseNumber,
|
|
393
|
+
stepNumber,
|
|
394
|
+
});
|
|
384
395
|
return {
|
|
385
396
|
content: [getEmbeddedResourceContent(resource)],
|
|
386
397
|
};
|
|
387
398
|
});
|
|
399
|
+
server.registerTool('view_video', {
|
|
400
|
+
description: `
|
|
401
|
+
Intended to help a student view a video.
|
|
402
|
+
|
|
403
|
+
If the user ever asks you to show them a video, use this tool to do so.
|
|
404
|
+
`.trim(),
|
|
405
|
+
inputSchema: {
|
|
406
|
+
videoUrl: z.string().describe(`
|
|
407
|
+
The URL of the video to view. If you don't know the URL to use already, you can get this from the \`get_exercise_step_context\` tool or the \`get_exercise_context\` tool depending on whether you're trying to show the exercise intro/outro video or a specific step's problem/solution video. If you use the \`get_what_is_next\` tool, this will be handled automatically.
|
|
408
|
+
`.trim()),
|
|
409
|
+
},
|
|
410
|
+
}, async ({ videoUrl }) => {
|
|
411
|
+
const url = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
412
|
+
url.searchParams.set('url', videoUrl);
|
|
413
|
+
return {
|
|
414
|
+
content: [
|
|
415
|
+
createUIResource({
|
|
416
|
+
content: {
|
|
417
|
+
type: 'externalUrl',
|
|
418
|
+
iframeUrl: url.toString(),
|
|
419
|
+
},
|
|
420
|
+
uri: `ui://epicshop/epic-video/${videoUrl.toString()}`,
|
|
421
|
+
encoding: 'text',
|
|
422
|
+
}),
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
});
|
|
426
|
+
server.registerTool('open_exercise_step_files', {
|
|
427
|
+
title: 'Open Exercise Step Files',
|
|
428
|
+
description: `
|
|
429
|
+
Call this to open the files for the exercise step the playground is currently set to.
|
|
430
|
+
`.trim(),
|
|
431
|
+
inputSchema: {
|
|
432
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
433
|
+
},
|
|
434
|
+
}, async ({ workshopDirectory }) => {
|
|
435
|
+
await handleWorkshopDirectory(workshopDirectory);
|
|
436
|
+
const playgroundApp = await getPlaygroundApp();
|
|
437
|
+
invariant(playgroundApp, 'The playground app is not currently set. Use the `set_playground` tool to set the playground to an exercise step.');
|
|
438
|
+
const problemApp = await getAppByName(playgroundApp.appName);
|
|
439
|
+
invariant(problemApp, 'Cannot find the problem app for the playground app. This is unexpected. The playground app may need to be reset using the `set_playground` tool.');
|
|
440
|
+
invariant(isProblemApp(problemApp) && problemApp.solutionName, 'The playground app is not set to a problem app with a solution. The playground app may need to be reset using the `set_playground` tool.');
|
|
441
|
+
const solutionApp = await getAppByName(problemApp.solutionName);
|
|
442
|
+
invariant(solutionApp, 'Cannot find the solution app for the problem app. Cannot open the files for a step that does not have both a problem and solution.');
|
|
443
|
+
const diffFiles = await getDiffFiles(problemApp, solutionApp);
|
|
444
|
+
invariant(diffFiles, 'There was a problem generating the diff. Check the terminal output.');
|
|
445
|
+
for (const file of diffFiles) {
|
|
446
|
+
const fullPath = path.join(playgroundApp.fullPath, file.path);
|
|
447
|
+
await launchEditor(fullPath, file.line);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: 'text',
|
|
453
|
+
text: `Opened ${diffFiles.length} file${diffFiles.length === 1 ? '' : 's'}:\n${diffFiles.map((file) => `${file.path}:${file.line}`).join('\n')}`,
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
};
|
|
457
|
+
});
|
|
388
458
|
server.registerTool('get_user_info', {
|
|
389
459
|
description: `
|
|
390
460
|
Intended to help you get information about the current user.
|
|
@@ -471,6 +541,186 @@ retrieve the instructions for how to do so.
|
|
|
471
541
|
}),
|
|
472
542
|
};
|
|
473
543
|
});
|
|
544
|
+
server.registerTool('get_what_is_next', {
|
|
545
|
+
title: 'Get What Is Next',
|
|
546
|
+
description: `
|
|
547
|
+
Intended to help you get the next step that the user needs to complete.
|
|
548
|
+
|
|
549
|
+
This is often useful to know what the user should do next to continue their learning.
|
|
550
|
+
|
|
551
|
+
This could be that they need to login, watch a video, complete an exercise, etc.
|
|
552
|
+
`.trim(),
|
|
553
|
+
inputSchema: {
|
|
554
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
555
|
+
},
|
|
556
|
+
}, async ({ workshopDirectory }) => {
|
|
557
|
+
await handleWorkshopDirectory(workshopDirectory);
|
|
558
|
+
const authInfo = await getAuthInfo();
|
|
559
|
+
if (!authInfo) {
|
|
560
|
+
return {
|
|
561
|
+
content: [
|
|
562
|
+
{
|
|
563
|
+
type: 'text',
|
|
564
|
+
text: 'The user is not logged in. Use the `login` tool to login the user.',
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const progress = await getProgress();
|
|
570
|
+
const scoreProgress = (a) => {
|
|
571
|
+
if (a.type === 'workshop-instructions')
|
|
572
|
+
return 0;
|
|
573
|
+
if (a.type === 'workshop-finished')
|
|
574
|
+
return 10000;
|
|
575
|
+
if (a.type === 'instructions')
|
|
576
|
+
return a.exerciseNumber * 100;
|
|
577
|
+
if (a.type === 'step')
|
|
578
|
+
return a.exerciseNumber * 100 + a.stepNumber;
|
|
579
|
+
if (a.type === 'finished')
|
|
580
|
+
return a.exerciseNumber * 100 + 100;
|
|
581
|
+
if (a.type === 'unknown')
|
|
582
|
+
return 100000;
|
|
583
|
+
return -1;
|
|
584
|
+
};
|
|
585
|
+
const sortedProgress = progress.sort((a, b) => {
|
|
586
|
+
return scoreProgress(a) - scoreProgress(b);
|
|
587
|
+
});
|
|
588
|
+
const nextProgress = sortedProgress.find((p) => !p.epicCompletedAt);
|
|
589
|
+
if (!nextProgress) {
|
|
590
|
+
return {
|
|
591
|
+
content: [
|
|
592
|
+
{
|
|
593
|
+
type: 'text',
|
|
594
|
+
text: `The user has completed the workshop. Congratulate them and invite them to ask you to quiz them on their understanding of the material. A summary of the material is below:`,
|
|
595
|
+
},
|
|
596
|
+
createText(await createWorkshopSummary()),
|
|
597
|
+
],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
invariant(nextProgress.type !== 'unknown', `Next progress type is unknown. This is unexpected. Sorry, we don't know what to do here. Here's a summary of the workshop:\n\n${await createWorkshopSummary()}`);
|
|
601
|
+
if (nextProgress.type === 'workshop-instructions') {
|
|
602
|
+
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
603
|
+
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
604
|
+
return {
|
|
605
|
+
content: [
|
|
606
|
+
createText(`The user has just begun! They need to watch the workshop instructions video and read the instructions to get started. When they say they're done or ready for what's next, mark it as complete using the \`update_progress\` tool with the slug "${nextProgress.epicLessonSlug}" and then call \`get_what_is_next\` again to get the next step. Relevant info is below:`),
|
|
607
|
+
createText(await createWorkshopSummary()),
|
|
608
|
+
createText(`Instructions:\n${await readInWorkshop('exercises', 'README.mdx')}`),
|
|
609
|
+
createUIResource({
|
|
610
|
+
uri: `ui://epicshop/epic-video/${nextProgress.epicLessonUrl}`,
|
|
611
|
+
encoding: 'text',
|
|
612
|
+
content: {
|
|
613
|
+
type: 'externalUrl',
|
|
614
|
+
iframeUrl: embedUrl.toString(),
|
|
615
|
+
},
|
|
616
|
+
}),
|
|
617
|
+
],
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
if (nextProgress.type === 'workshop-finished') {
|
|
621
|
+
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
622
|
+
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
623
|
+
return {
|
|
624
|
+
content: [
|
|
625
|
+
createText(`The user has almost completed the workshop. They just need to watch the workshop finished video and read the finished instructions to get started. When they say they're done or ready for what's next, mark it as complete using the \`update_progress\` tool with the slug "${nextProgress.epicLessonSlug}" and then call \`get_what_is_next\` again to get the next step. Relevant info is below:`),
|
|
626
|
+
createText(`Finished instructions:\n${await readInWorkshop('exercises', 'FINISHED.mdx')}`),
|
|
627
|
+
createUIResource({
|
|
628
|
+
uri: `ui://epicshop/epic-video/${nextProgress.epicLessonUrl}`,
|
|
629
|
+
encoding: 'text',
|
|
630
|
+
content: {
|
|
631
|
+
type: 'externalUrl',
|
|
632
|
+
iframeUrl: embedUrl.toString(),
|
|
633
|
+
},
|
|
634
|
+
}),
|
|
635
|
+
],
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
const ex = nextProgress.exerciseNumber.toString().padStart(2, '0');
|
|
639
|
+
if (nextProgress.type === 'instructions') {
|
|
640
|
+
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
641
|
+
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
642
|
+
const exercise = await getExercise(nextProgress.exerciseNumber);
|
|
643
|
+
return {
|
|
644
|
+
content: [
|
|
645
|
+
createText(`The user needs to complete the intro for exercise ${ex}. When they say they're done or ready for what's next, mark it as complete using the \`update_progress\` tool with the slug "${nextProgress.epicLessonSlug}" and then call \`get_what_is_next\` again to get the next step. Relevant info is below:`),
|
|
646
|
+
createText(`Exercise instructions:\n${await readReadme(exercise?.fullPath)}`),
|
|
647
|
+
createUIResource({
|
|
648
|
+
uri: `ui://epicshop/epic-video/${nextProgress.epicLessonUrl}`,
|
|
649
|
+
encoding: 'text',
|
|
650
|
+
content: {
|
|
651
|
+
type: 'externalUrl',
|
|
652
|
+
iframeUrl: embedUrl.toString(),
|
|
653
|
+
},
|
|
654
|
+
}),
|
|
655
|
+
],
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (nextProgress.type === 'finished') {
|
|
659
|
+
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
660
|
+
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
661
|
+
const exercise = await getExercise(nextProgress.exerciseNumber);
|
|
662
|
+
return {
|
|
663
|
+
content: [
|
|
664
|
+
createText(`The user is almost finished with exercise ${ex}. They need to complete the outro for exercise ${ex}. Relevant info is below:`),
|
|
665
|
+
createText(`Exercise finished instructions:\n${await readReadme(exercise?.fullPath)}`),
|
|
666
|
+
createUIResource({
|
|
667
|
+
uri: `ui://epicshop/epic-video/${nextProgress.epicLessonUrl}`,
|
|
668
|
+
encoding: 'text',
|
|
669
|
+
content: {
|
|
670
|
+
type: 'externalUrl',
|
|
671
|
+
iframeUrl: embedUrl.toString(),
|
|
672
|
+
},
|
|
673
|
+
}),
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const st = nextProgress.stepNumber.toString().padStart(2, '0');
|
|
678
|
+
if (nextProgress.type === 'step') {
|
|
679
|
+
const exercise = await getExercise(nextProgress.exerciseNumber);
|
|
680
|
+
const problemEmbedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
681
|
+
problemEmbedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
682
|
+
const solutionEmbedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
683
|
+
solutionEmbedUrl.searchParams.set('url', `${nextProgress.epicLessonUrl}/solution`);
|
|
684
|
+
const step = exercise?.steps.find((s) => s.stepNumber === nextProgress.stepNumber);
|
|
685
|
+
invariant(step, `No step found for exercise ${nextProgress.exerciseNumber} step ${nextProgress.stepNumber}`);
|
|
686
|
+
return {
|
|
687
|
+
content: [
|
|
688
|
+
createText(`
|
|
689
|
+
The user is on step ${st} of exercise ${ex}. To complete this step they need to:
|
|
690
|
+
1. Watch the problem video
|
|
691
|
+
2. Review the problem instructions (you can summarize these from the info below)
|
|
692
|
+
3. Set the playground to the problem app (you can help them using the \`set_playground\` tool)
|
|
693
|
+
4. Open the relevant files in their playground environment (you can help them using the \`open_exercise_step_files\` tool)
|
|
694
|
+
5. Run the tests and dev server to validate their work (no tools for this are available yet, but you can use the \`get_exercise_step_progress_diff\` tool to help them understand what work they still need to do as they go)
|
|
695
|
+
6. Watch the solution video
|
|
696
|
+
7. Review the solution instructions (you can summarize these from the info below)
|
|
697
|
+
8. Mark the step as complete (you can help them using the \`update_progress\` tool with the slug "${nextProgress.epicLessonSlug}")
|
|
698
|
+
|
|
699
|
+
Then you can call \`get_what_is_next\` again to get the next step.
|
|
700
|
+
`.trim()),
|
|
701
|
+
createText(`Exercise step problem instructions:\n${await readReadme(step.problem?.fullPath)}`),
|
|
702
|
+
createText(`Exercise step solution instructions:\n${await readReadme(step.solution?.fullPath)}`),
|
|
703
|
+
createUIResource({
|
|
704
|
+
uri: `ui://epicshop/epic-video/${nextProgress.epicLessonUrl}`,
|
|
705
|
+
encoding: 'text',
|
|
706
|
+
content: {
|
|
707
|
+
type: 'externalUrl',
|
|
708
|
+
iframeUrl: problemEmbedUrl.toString(),
|
|
709
|
+
},
|
|
710
|
+
}),
|
|
711
|
+
createUIResource({
|
|
712
|
+
uri: `ui://epicshop/epic-video/${nextProgress.epicLessonUrl}/solution`,
|
|
713
|
+
encoding: 'text',
|
|
714
|
+
content: {
|
|
715
|
+
type: 'externalUrl',
|
|
716
|
+
iframeUrl: solutionEmbedUrl.toString(),
|
|
717
|
+
},
|
|
718
|
+
}),
|
|
719
|
+
],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
throw new Error(`This is unexpected, but I do not know what the next step for the user is. Sorry!`);
|
|
723
|
+
});
|
|
474
724
|
}
|
|
475
725
|
function getEmbeddedResourceContent(resource) {
|
|
476
726
|
if (clientSupportsEmbeddedResources) {
|
|
@@ -489,4 +739,67 @@ function getEmbeddedResourceContent(resource) {
|
|
|
489
739
|
throw new Error(`Unknown resource type: ${resource.type} for ${resource.uri}`);
|
|
490
740
|
}
|
|
491
741
|
}
|
|
742
|
+
function createText(text) {
|
|
743
|
+
if (typeof text === 'string') {
|
|
744
|
+
return { type: 'text', text };
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
return { type: 'text', text: JSON.stringify(text) };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async function createWorkshopSummary() {
|
|
751
|
+
const config = getWorkshopConfig();
|
|
752
|
+
const exercises = await getExercises();
|
|
753
|
+
let summary = `# ${config.title}
|
|
754
|
+
|
|
755
|
+
${config.subtitle}
|
|
756
|
+
|
|
757
|
+
## Exercises
|
|
758
|
+
`;
|
|
759
|
+
for (const exercise of exercises) {
|
|
760
|
+
summary += `
|
|
761
|
+
${exercise.exerciseNumber.toString().padStart(2, '0')}. ${exercise.title}
|
|
762
|
+
${exercise.steps.map((s) => ` ${s.stepNumber.toString().padStart(2, '0')}. ${s.problem?.title ?? s.solution?.title ?? 'No title'}`).join('\n')}`;
|
|
763
|
+
}
|
|
764
|
+
return summary;
|
|
765
|
+
}
|
|
766
|
+
async function readReadme(dirPath) {
|
|
767
|
+
return ((dirPath ? await safeReadFile(path.join(dirPath, 'README.mdx')) : null) ??
|
|
768
|
+
'No instructions');
|
|
769
|
+
}
|
|
770
|
+
function getDiffInstructionText() {
|
|
771
|
+
return `
|
|
772
|
+
Below is the diff between the user's work in progress and the solution.
|
|
773
|
+
Lines starting with \`-\` show code that needs to be removed from the user's solution.
|
|
774
|
+
Lines starting with \`+\` show code that needs to be added to the user's solution.
|
|
775
|
+
|
|
776
|
+
If there are significant differences, the user's work is incomplete.
|
|
777
|
+
|
|
778
|
+
Here's an example of the output you can expect:
|
|
779
|
+
|
|
780
|
+
--------
|
|
781
|
+
|
|
782
|
+
diff --git ./example.ts ./example.ts
|
|
783
|
+
index e05035d..a70eb4b 100644
|
|
784
|
+
--- ./example.ts
|
|
785
|
+
+++ ./example.ts
|
|
786
|
+
@@ -236,14 +236,27 @@ export async function sayHello(name?: string) {
|
|
787
|
+
+ if (name) {
|
|
788
|
+
+ return \`Hello, \${name}!\`
|
|
789
|
+
+ }
|
|
790
|
+
- await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
791
|
+
return 'Hello, World!'
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
--------
|
|
795
|
+
|
|
796
|
+
In this example, you should tell the user they still need to:
|
|
797
|
+
- add the if statement to return the name if it's provided
|
|
798
|
+
- remove the await promise that resolves after 1 second
|
|
799
|
+
|
|
800
|
+
For additional context, you can use the \`get_exercise_instructions\` tool
|
|
801
|
+
to get the instructions for the current exercise step to help explain the
|
|
802
|
+
significance of changes.
|
|
803
|
+
`.trim();
|
|
804
|
+
}
|
|
492
805
|
//# sourceMappingURL=tools.js.map
|