Skip to content

Commit 1b0cb5c

Browse files
committed
feat(mcp): more tools!
1 parent 444923f commit 1b0cb5c

4 files changed

Lines changed: 529 additions & 25 deletions

File tree

packages/workshop-app/app/routes/_app+/account.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { deleteCache } from '@epic-web/workshop-utils/cache.server'
22
import {
3-
deleteDb,
3+
logout,
44
requireAuthInfo,
55
setPreferences,
66
} from '@epic-web/workshop-utils/db.server'
@@ -30,7 +30,7 @@ export async function action({ request }: { request: Request }) {
3030
const formData = await request.formData()
3131
const intent = formData.get('intent')
3232
if (intent === 'logout') {
33-
await deleteDb()
33+
await logout()
3434
await deleteCache()
3535
return redirectWithToast('/login', {
3636
type: 'success',

packages/workshop-mcp/src/resources.ts

Lines changed: 193 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { getAuthInfo } from '@epic-web/workshop-utils/db.server'
1515
import {
1616
getEpicVideoInfos,
1717
userHasAccessToWorkshop,
18+
getUserInfo,
19+
getProgress,
1820
} from '@epic-web/workshop-utils/epic-api.server'
1921
import {
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+
442488
async 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-
463504
export 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+
471602
export 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

Comments
 (0)