@@ -15,6 +15,8 @@ import { getAuthInfo } from '@epic-web/workshop-utils/db.server'
1515import {
1616 getEpicVideoInfos ,
1717 userHasAccessToWorkshop ,
18+ getUserInfo ,
19+ getProgress ,
1820} from '@epic-web/workshop-utils/epic-api.server'
1921import {
2022 type McpServer ,
@@ -42,15 +44,22 @@ export async function getWorkshopContext({
4244 const workshopRoot = await handleWorkshopDirectory ( workshopDirectory )
4345 const inWorkshop = ( ...d : Array < string > ) => path . join ( workshopRoot , ...d )
4446 const readInWorkshop = ( ...d : Array < string > ) => safeReadFile ( inWorkshop ( ...d ) )
47+ const progress = await getProgress ( )
4548
4649 const output = {
4750 meta : {
4851 'README.md' : await readInWorkshop ( 'README.md' ) ,
4952 config : (
5053 JSON . parse ( ( await readInWorkshop ( 'package.json' ) ) || '{}' ) as any
5154 ) . epicshop ,
52- instructions : await readInWorkshop ( 'exercise' , 'README.mdx' ) ,
53- finishedInstructions : await readInWorkshop ( 'exercise' , 'FINISHED.mdx' ) ,
55+ instructions : {
56+ content : await readInWorkshop ( 'exercise' , 'README.mdx' ) ,
57+ progress : progress . find ( ( p ) => p . type === 'instructions' ) ,
58+ } ,
59+ finishedInstructions : {
60+ content : await readInWorkshop ( 'exercise' , 'FINISHED.mdx' ) ,
61+ progress : progress . find ( ( p ) => p . type === 'finished' ) ,
62+ } ,
5463 } ,
5564 exercises : [ ] as Array < any > ,
5665 }
@@ -61,15 +70,33 @@ export async function getWorkshopContext({
6170 fullPath : exercise . fullPath ,
6271 exerciseNumber : exercise . exerciseNumber ,
6372 title : exercise . title ,
64- instructions : await safeReadFile (
65- path . join ( exercise . fullPath , 'README.mdx' ) ,
66- ) ,
67- finishedInstructions : await safeReadFile (
68- path . join ( exercise . fullPath , 'FINISHED.mdx' ) ,
69- ) ,
73+ instructions : {
74+ content : await safeReadFile ( path . join ( exercise . fullPath , 'README.mdx' ) ) ,
75+ progress : progress . find (
76+ ( p ) =>
77+ p . type === 'instructions' &&
78+ p . exerciseNumber === exercise . exerciseNumber ,
79+ ) ,
80+ } ,
81+ finishedInstructions : {
82+ content : await safeReadFile (
83+ path . join ( exercise . fullPath , 'FINISHED.mdx' ) ,
84+ ) ,
85+ progress : progress . find (
86+ ( p ) =>
87+ p . type === 'finished' &&
88+ p . exerciseNumber === exercise . exerciseNumber ,
89+ ) ,
90+ } ,
7091 steps : exercise . steps . map ( ( step ) => {
7192 return {
7293 stepNumber : step . stepNumber ,
94+ progress : progress . find (
95+ ( p ) =>
96+ p . type === 'step' &&
97+ p . exerciseNumber === exercise . exerciseNumber &&
98+ p . stepNumber === step . stepNumber ,
99+ ) ,
73100 title : step . problem ?. title ?? step . solution ?. title ?? null ,
74101 problem : step . problem
75102 ? {
@@ -145,6 +172,7 @@ async function getExerciseContext({
145172 await handleWorkshopDirectory ( workshopDirectory )
146173 const userHasAccess = await userHasAccessToWorkshop ( )
147174 const authInfo = await getAuthInfo ( )
175+ const progress = await getProgress ( )
148176 let stepNumber = 1
149177 const playgroundApp = await getPlaygroundApp ( )
150178 invariant ( playgroundApp , 'No playground app found' )
@@ -236,26 +264,39 @@ async function getExerciseContext({
236264 }
237265 : 'currently set to a different exercise' ,
238266 } ,
239- exerciseBackground : {
267+ exerciseInfo : {
240268 number : exerciseNumber ,
241269 intro : {
242270 content : await getFileContent (
243271 path . join ( exercise . fullPath , 'README.mdx' ) ,
244272 ) ,
245273 transcripts : getTranscripts ( exercise . instructionsEpicVideoEmbeds ) ,
274+ progress : progress . find (
275+ ( p ) =>
276+ p . type === 'instructions' && p . exerciseNumber === exerciseNumber ,
277+ ) ,
246278 } ,
247279 outro : {
248280 content : await getFileContent (
249281 path . join ( exercise . fullPath , 'FINISHED.mdx' ) ,
250282 ) ,
251283 transcripts : getTranscripts ( exercise . finishedEpicVideoEmbeds ) ,
284+ progress : progress . find (
285+ ( p ) => p . type === 'finished' && p . exerciseNumber === exerciseNumber ,
286+ ) ,
252287 } ,
253288 } ,
254289 steps : exercise . steps
255290 ? await Promise . all (
256291 exercise . steps . map ( async ( app ) => ( {
257292 number : app . stepNumber ,
258293 isCurrent : isCurrentExercise && app . stepNumber === stepNumber ,
294+ progress : progress . find (
295+ ( p ) =>
296+ p . type === 'step' &&
297+ p . exerciseNumber === exerciseNumber &&
298+ p . stepNumber === app . stepNumber ,
299+ ) ,
259300 problem : {
260301 content : app . problem
261302 ? await getFileContent (
@@ -310,7 +351,7 @@ async function getExerciseContextResource({
310351 return {
311352 uri : exerciseContextUriTemplate . uriTemplate . expand ( {
312353 workshopDirectory,
313- exerciseNumber : String ( context . exerciseBackground . number ) ,
354+ exerciseNumber : String ( context . exerciseInfo . number ) ,
314355 } ) ,
315356 mimeType : 'application/json' ,
316357 text : JSON . stringify ( context ) ,
@@ -439,6 +480,11 @@ async function getExerciseStepProgressDiff({
439480 return diffCode
440481}
441482
483+ const exerciseStepProgressDiffUriTemplate = new ResourceTemplate (
484+ 'epicshop://{workshopDirectory}/exercise-step-progress-diff' ,
485+ { list : undefined } ,
486+ )
487+
442488async function getExerciseStepProgressDiffResource ( {
443489 workshopDirectory,
444490} : InputSchemaType < typeof getExerciseStepProgressDiffInputSchema > ) : Promise <
@@ -455,11 +501,6 @@ async function getExerciseStepProgressDiffResource({
455501 }
456502}
457503
458- const exerciseStepProgressDiffUriTemplate = new ResourceTemplate (
459- 'epicshop://{workshopDirectory}/exercise-step-progress-diff' ,
460- { list : undefined } ,
461- )
462-
463504export const exerciseStepProgressDiffResource = {
464505 name : 'exercise_step_progress_diff' ,
465506 description : 'The diff between the current exercise step and the solution' ,
@@ -468,6 +509,96 @@ export const exerciseStepProgressDiffResource = {
468509 inputSchema : getExerciseStepProgressDiffInputSchema ,
469510}
470511
512+ const getUserInfoInputSchema = {
513+ workshopDirectory : z . string ( ) . describe ( 'The workshop directory' ) ,
514+ }
515+
516+ const userInfoUri = new ResourceTemplate (
517+ 'epicshop://{workshopDirectory}/user/info' ,
518+ { list : undefined } ,
519+ )
520+
521+ async function getUserInfoResource ( {
522+ workshopDirectory,
523+ } : InputSchemaType < typeof getUserInfoInputSchema > ) {
524+ const userInfo = await getUserInfo ( )
525+ return {
526+ uri : userInfoUri . uriTemplate . expand ( {
527+ workshopDirectory,
528+ } ) ,
529+ mimeType : 'application/json' ,
530+ text : JSON . stringify ( userInfo ) ,
531+ }
532+ }
533+
534+ export const userInfoResource = {
535+ name : 'user_info' ,
536+ description : 'Information about the current user' ,
537+ uriTemplate : userInfoUri ,
538+ getResource : getUserInfoResource ,
539+ inputSchema : getUserInfoInputSchema ,
540+ }
541+
542+ const getUserAccessInputSchema = {
543+ workshopDirectory : z . string ( ) . describe ( 'The workshop directory' ) ,
544+ }
545+
546+ const userAccessUriTemplate = new ResourceTemplate (
547+ 'epicshop://{workshopDirectory}/user/access' ,
548+ { list : undefined } ,
549+ )
550+
551+ async function getUserAccessResource ( {
552+ workshopDirectory,
553+ } : InputSchemaType < typeof getUserAccessInputSchema > ) {
554+ const userHasAccess = await userHasAccessToWorkshop ( )
555+ return {
556+ uri : userAccessUriTemplate . uriTemplate . expand ( {
557+ workshopDirectory,
558+ } ) ,
559+ mimeType : 'application/json' ,
560+ text : JSON . stringify ( { userHasAccess } ) ,
561+ }
562+ }
563+
564+ export const userAccessResource = {
565+ name : 'user_access' ,
566+ description : 'Whether the current user has access to the workshop' ,
567+ uriTemplate : userAccessUriTemplate ,
568+ getResource : getUserAccessResource ,
569+ inputSchema : getUserAccessInputSchema ,
570+ }
571+
572+ const userProgressInputSchema = {
573+ workshopDirectory : z . string ( ) . describe ( 'The workshop directory' ) ,
574+ }
575+
576+ const userProgressUriTemplate = new ResourceTemplate (
577+ 'epicshop://{workshopDirectory}/user/progress' ,
578+ { list : undefined } ,
579+ )
580+
581+ async function getUserProgressResource ( {
582+ workshopDirectory,
583+ } : InputSchemaType < typeof userProgressInputSchema > ) {
584+ const userProgress = await getProgress ( )
585+ return {
586+ uri : userProgressUriTemplate . uriTemplate . expand ( {
587+ workshopDirectory,
588+ } ) ,
589+ mimeType : 'application/json' ,
590+ text : JSON . stringify ( userProgress ) ,
591+ }
592+ }
593+
594+ export const userProgressResource = {
595+ name : 'user_progress' ,
596+ description : 'The progress of the current user' ,
597+ uriTemplate : userProgressUriTemplate ,
598+ getResource : getUserProgressResource ,
599+ inputSchema : userProgressInputSchema ,
600+ }
601+
471602export function initResources ( server : McpServer ) {
472603 server . resource (
473604 workshopContextResource . name ,
@@ -559,4 +690,51 @@ export function initResources(server: McpServer) {
559690 }
560691 } ,
561692 )
693+
694+ server . resource (
695+ userInfoResource . name ,
696+ userInfoResource . uriTemplate ,
697+ { description : userInfoResource . description } ,
698+ async ( _uri , { workshopDirectory } ) => {
699+ invariant (
700+ typeof workshopDirectory === 'string' ,
701+ 'A single workshopDirectory is required' ,
702+ )
703+ return {
704+ contents : [ await userInfoResource . getResource ( { workshopDirectory } ) ] ,
705+ }
706+ } ,
707+ )
708+
709+ server . resource (
710+ userAccessResource . name ,
711+ userAccessResource . uriTemplate ,
712+ { description : userAccessResource . description } ,
713+ async ( _uri , { workshopDirectory } ) => {
714+ invariant (
715+ typeof workshopDirectory === 'string' ,
716+ 'A single workshopDirectory is required' ,
717+ )
718+ return {
719+ contents : [ await userAccessResource . getResource ( { workshopDirectory } ) ] ,
720+ }
721+ } ,
722+ )
723+
724+ server . resource (
725+ userProgressResource . name ,
726+ userProgressResource . uriTemplate ,
727+ { description : userProgressResource . description } ,
728+ async ( _uri , { workshopDirectory } ) => {
729+ invariant (
730+ typeof workshopDirectory === 'string' ,
731+ 'A single workshopDirectory is required' ,
732+ )
733+ return {
734+ contents : [
735+ await userProgressResource . getResource ( { workshopDirectory } ) ,
736+ ] ,
737+ }
738+ } ,
739+ )
562740}
0 commit comments