@epic-web/workshop-mcp 5.18.2 → 5.20.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/cli.js DELETED
@@ -1,400 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { invariant } from '@epic-web/invariant';
5
- import { getApps, isPlaygroundApp, findSolutionDir, getFullPathFromAppName, init as initApps, extractNumbersAndTypeFromAppNameOrPath, getExercise, getPlaygroundApp, getPlaygroundAppName, isProblemApp, isExerciseStepApp, setPlayground, } from '@epic-web/workshop-utils/apps.server';
6
- import { getAuthInfo } from '@epic-web/workshop-utils/db.server';
7
- import { getEpicVideoInfos, userHasAccessToWorkshop, } from '@epic-web/workshop-utils/epic-api.server';
8
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
- import { z } from 'zod';
11
- // Create server instance
12
- const server = new McpServer({
13
- name: 'epicshop',
14
- version: '1.0.0',
15
- capabilities: {
16
- tools: {},
17
- },
18
- }, {
19
- instructions: `
20
- This is intended to be used within a workshop using the Epic Workshop App
21
- (@epic-web/workshop-app) to help learners in the process of completing the
22
- workshop exercises and understanding the learning outcomes.
23
-
24
- The user's work in progress is in the \`playground\` directory. Any changes they
25
- ask you to make should be in this directory.
26
- `.trim(),
27
- });
28
- server.tool('get_diff_between_apps', `
29
- Intended to give context about the changes between two apps.
30
-
31
- The output is a git diff of the playground directory as BASE (their work in
32
- progress) against the solution directory as HEAD (the final state they're trying
33
- to achieve).
34
-
35
- The output is formatted as a git diff.
36
-
37
- App IDs are formatted as \`{exerciseNumber}.{stepNumber}.{type}\`.
38
-
39
- If the user asks for the diff for 2.3, then use 02.03.problem for app1 and 02.03.solution for app2.
40
- `, {
41
- workshopDirectory: z
42
- .string()
43
- .describe('The workshop directory (the root directory of the workshop repo.).'),
44
- app1: z.string().describe('The ID of the first app'),
45
- app2: z.string().describe('The ID of the second app'),
46
- }, async ({ workshopDirectory, app1, app2 }) => {
47
- try {
48
- await handleWorkshopDirectory(workshopDirectory);
49
- const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
50
- const app1Name = extractNumbersAndTypeFromAppNameOrPath(app1);
51
- const app2Name = extractNumbersAndTypeFromAppNameOrPath(app2);
52
- const apps = await getApps();
53
- const app1App = apps
54
- .filter(isExerciseStepApp)
55
- .find((a) => a.exerciseNumber === Number(app1Name?.exerciseNumber) &&
56
- a.stepNumber === Number(app1Name?.stepNumber) &&
57
- a.type === app1Name?.type);
58
- const app2App = apps
59
- .filter(isExerciseStepApp)
60
- .find((a) => a.exerciseNumber === Number(app2Name?.exerciseNumber) &&
61
- a.stepNumber === Number(app2Name?.stepNumber) &&
62
- a.type === app2Name?.type);
63
- invariant(app1App, `No app found for ${app1}`);
64
- invariant(app2App, `No app found for ${app2}`);
65
- const diffCode = await getDiffOutputWithRelativePaths(app1App, app2App);
66
- if (!diffCode)
67
- return replyWithText('No changes');
68
- return replyWithText(diffCode);
69
- }
70
- catch (error) {
71
- return replyWithError(error);
72
- }
73
- });
74
- server.tool('get_exercise_step_progress_diff', `
75
- Intended to help a student understand what work they still have to complete.
76
-
77
- This returns a git diff of the playground directory as BASE (their work in
78
- progress) against the solution directory as HEAD (the final state they're trying
79
- to achieve). Meaning, if there are lines removed, it means they still need to
80
- add those lines and if they are added, it means they still need to remove them.
81
-
82
- If there's a diff with significant changes, you should explain what the changes
83
- are and their significance. Be brief. Let them tell you whether they need you to
84
- elaborate.
85
-
86
- The output for this changes over time so it's useful to call multiple times.
87
-
88
- For additional context, you can use the \`get_exercise_instructions\` tool
89
- to get the instructions for the current exercise step to help explain the
90
- significance of changes.
91
- `.trim(), {
92
- workshopDirectory: z
93
- .string()
94
- .describe('The workshop directory (the root directory of the workshop repo.).'),
95
- }, async ({ workshopDirectory }) => {
96
- try {
97
- await handleWorkshopDirectory(workshopDirectory);
98
- const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
99
- const apps = await getApps();
100
- const playgroundApp = apps.find(isPlaygroundApp);
101
- if (!playgroundApp) {
102
- return {
103
- content: [{ type: 'text', text: 'No playground app found' }],
104
- isError: true,
105
- };
106
- }
107
- const baseApp = playgroundApp;
108
- const solutionDir = await findSolutionDir({
109
- fullPath: await getFullPathFromAppName(playgroundApp.appName),
110
- });
111
- const headApp = apps.find((a) => a.fullPath === solutionDir);
112
- if (!headApp) {
113
- return {
114
- content: [{ type: 'text', text: 'No playground solution app found' }],
115
- isError: true,
116
- };
117
- }
118
- const diffCode = await getDiffOutputWithRelativePaths(baseApp, headApp);
119
- if (!diffCode)
120
- return replyWithText('No changes');
121
- return replyWithText(diffCode);
122
- }
123
- catch (error) {
124
- return replyWithError(error);
125
- }
126
- });
127
- server.tool('get_exercise_context', `
128
- Intended to help a student understand what they need to do for the current
129
- exercise step.
130
-
131
- This returns the instructions MDX content for the current exercise and each
132
- exercise step. If the user is has the paid version of the workshop, it will also
133
- include the transcript from each of the videos as well.
134
-
135
- The output for this will rarely change, so it's unnecessary to call this tool
136
- more than once.
137
-
138
- \`get_exercise_context\` is often best when used with the
139
- \`get_exercise_step_progress_diff\` tool to help a student understand what
140
- work they still need to do and answer any questions about the exercise.
141
- `.trim(), {
142
- workshopDirectory: z
143
- .string()
144
- .describe('The workshop directory (the root directory of the workshop repo.).'),
145
- exerciseNumber: z.coerce
146
- .number()
147
- .optional()
148
- .describe(`The exercise number to get the context for (defaults to the exercise number the playground is currently set to)`),
149
- }, async ({ workshopDirectory, exerciseNumber }) => {
150
- try {
151
- await handleWorkshopDirectory(workshopDirectory);
152
- const userHasAccess = await userHasAccessToWorkshop();
153
- const authInfo = await getAuthInfo();
154
- let stepNumber = 1;
155
- const playgroundApp = await getPlaygroundApp();
156
- invariant(playgroundApp, 'No playground app found');
157
- const numbers = extractNumbersAndTypeFromAppNameOrPath(playgroundApp.appName);
158
- const isCurrentExercise = exerciseNumber === undefined ||
159
- exerciseNumber === Number(numbers?.exerciseNumber);
160
- if (exerciseNumber === undefined) {
161
- invariant(numbers, 'No numbers found in playground app name');
162
- exerciseNumber = Number(numbers.exerciseNumber);
163
- stepNumber = Number(numbers.stepNumber);
164
- }
165
- const exercise = await getExercise(exerciseNumber);
166
- invariant(exercise, `No exercise found for exercise number ${exerciseNumber}`);
167
- const videoInfos = await getEpicVideoInfos([
168
- ...(exercise.instructionsEpicVideoEmbeds ?? []),
169
- ...exercise.steps.flatMap((s) => s.problem?.epicVideoEmbeds ?? []),
170
- ...exercise.steps.flatMap((s) => s.solution?.epicVideoEmbeds ?? []),
171
- ...(exercise.finishedEpicVideoEmbeds ?? []),
172
- ]);
173
- function getTranscriptsElement(embeds) {
174
- if (!embeds)
175
- return '<transcripts />';
176
- if (!userHasAccess && embeds.length) {
177
- return `
178
- <transcripts>
179
- User must upgrade before they can get access to ${embeds.length} transcript${embeds.length === 1 ? '' : 's'}.
180
- </transcripts>
181
- `.trim();
182
- }
183
- const transcripts = ['<transcripts>'];
184
- for (const embed of embeds) {
185
- const info = videoInfos[embed];
186
- if (info) {
187
- if (info.status === 'error') {
188
- if (info.type === 'region-restricted') {
189
- transcripts.push(`
190
- <transcript
191
- embed="${embed}"
192
- status="error"
193
- type="${info.type}"
194
- requested-country="${info.requestCountry}"
195
- restricted-country="${info.restrictedCountry}"
196
- />
197
- `.trim());
198
- }
199
- else {
200
- transcripts.push(`
201
- <transcript
202
- embed="${embed}"
203
- status="error"
204
- type="${info.type}"
205
- status-code="${info.statusCode}"
206
- status-text="${info.statusText}"
207
- />
208
- `.trim());
209
- }
210
- }
211
- else {
212
- transcripts.push(`<transcript embed="${embed}" status="success">${info.transcript}</transcript>`);
213
- }
214
- }
215
- else {
216
- transcripts.push(`<transcript embed="${embed}" status="error" type="not-found">No transcript found</transcript>`);
217
- }
218
- }
219
- transcripts.push('</transcripts>');
220
- return transcripts.join('\n');
221
- }
222
- async function getFileContentElement(filePath) {
223
- return `<file path="${filePath}">${(await safeReadFile(filePath)) ?? 'None found'}</file>`;
224
- }
225
- let text = `
226
- Below is all the context for this exercise and each step.
227
-
228
- <currentContext>
229
- <user hasAccess="${userHasAccess}" isAuthenticated="${Boolean(authInfo)}" email="${authInfo?.email}" />
230
- ${isCurrentExercise
231
- ? `<playground>
232
- <exerciseNumber>${exerciseNumber}</exerciseNumber>
233
- <stepNumber>${stepNumber}</stepNumber>
234
- </playground>`
235
- : '<playground>currently set to a different exercise</playground>'}
236
- </currentContext>
237
-
238
- <exerciseBackground number="${exerciseNumber}">
239
- <intro>
240
- ${await getFileContentElement(path.join(exercise.fullPath, 'README.mdx'))}
241
- ${getTranscriptsElement(exercise.instructionsEpicVideoEmbeds)}
242
- </intro>
243
- <outro>
244
- ${await getFileContentElement(path.join(exercise.fullPath, 'FINISHED.mdx'))}
245
- ${getTranscriptsElement(exercise.finishedEpicVideoEmbeds)}
246
- </outro>
247
- </exerciseBackground>
248
- `.trim();
249
- if (exercise.steps) {
250
- text += '\n\n<steps>';
251
- for (const app of exercise.steps) {
252
- text += `
253
- <step number="${app.stepNumber}" isCurrent="${isCurrentExercise && app.stepNumber === stepNumber}">
254
- <problem>
255
- ${app.problem ? await getFileContentElement(path.join(app.problem?.fullPath, `README.mdx`)) : 'No problem found'}
256
- ${getTranscriptsElement(app.problem?.epicVideoEmbeds ?? [])}
257
- </problem>
258
- <solution>
259
- ${app.solution ? await getFileContentElement(path.join(app.solution?.fullPath, `README.mdx`)) : 'No solution found'}
260
- ${getTranscriptsElement(app.solution?.epicVideoEmbeds ?? [])}
261
- </solution>
262
- </step>`;
263
- }
264
- text += '</steps>\n\n';
265
- text += `Reminder, the current step is ${stepNumber} of ${exercise.steps.length + 1}. The most relevant information will be in the context abouve within the current step.`;
266
- }
267
- else {
268
- text += `Unusually, this exercise has no steps.`;
269
- }
270
- return {
271
- content: [{ type: 'text', text: text }],
272
- };
273
- }
274
- catch (error) {
275
- return replyWithError(error);
276
- }
277
- });
278
- server.tool('set_playground', `
279
- Sets the playground environment so the user can continue to that exercise or see
280
- what that step looks like in their playground environment.
281
-
282
- NOTE: this will override their current exercise step work in the playground!
283
-
284
- Generally, it is better to not provide an exerciseNumber, stepNumber, and type
285
- and let the user continue to the next exercise. Only provide these arguments if
286
- the user explicitely asks to go to a specific exercise or step. If the user asks
287
- to start an exercise, specify stepNumber 1 and type 'problem' unless otherwise
288
- directed.
289
-
290
- Argument examples:
291
- A. Set to next exercise step from current (or first if there is none) - Most common
292
- - [No arguments]
293
- B. Set to a specific exercise step
294
- - exerciseNumber: 1
295
- - stepNumber: 1
296
- - type: 'solution'
297
- C. Set to the solution of the current exercise step
298
- - type: 'solution'
299
- D. Set to the second step problem of the current exercise
300
- - stepNumber: 2
301
- E. Set to the first step problem of the fifth exercise
302
- - exerciseNumber: 5
303
-
304
- An error will be returned if no app is found for the given arguments.
305
- `.trim(), {
306
- workshopDirectory: z.string().describe('The workshop directory'),
307
- exerciseNumber: z.coerce
308
- .number()
309
- .optional()
310
- .describe('The exercise number to set the playground to'),
311
- stepNumber: z.coerce
312
- .number()
313
- .optional()
314
- .describe('The step number to set the playground to'),
315
- type: z
316
- .enum(['problem', 'solution'])
317
- .optional()
318
- .describe('The type of app to set the playground to'),
319
- }, async ({ workshopDirectory, exerciseNumber, stepNumber, type }) => {
320
- try {
321
- await handleWorkshopDirectory(workshopDirectory);
322
- const apps = await getApps();
323
- const exerciseStepApps = apps.filter(isExerciseStepApp);
324
- const playgroundAppName = await getPlaygroundAppName();
325
- const currentExerciseStepAppIndex = exerciseStepApps.findIndex((a) => a.name === playgroundAppName);
326
- let desiredApp;
327
- // if nothing was provided, set to the next step problem app
328
- const noArgumentsProvided = !exerciseNumber && !stepNumber && !type;
329
- if (noArgumentsProvided) {
330
- desiredApp = exerciseStepApps
331
- .slice(currentExerciseStepAppIndex + 1)
332
- .find(isProblemApp);
333
- invariant(desiredApp, 'No next problem app found to set playground to');
334
- }
335
- else {
336
- const currentExerciseStepApp = exerciseStepApps[currentExerciseStepAppIndex];
337
- // otherwise, default to the current exercise step app for arguments
338
- exerciseNumber ??= currentExerciseStepApp?.exerciseNumber;
339
- stepNumber ??= currentExerciseStepApp?.stepNumber;
340
- type ??= currentExerciseStepApp?.type;
341
- desiredApp = exerciseStepApps.find((a) => a.exerciseNumber === exerciseNumber &&
342
- a.stepNumber === stepNumber &&
343
- a.type === type);
344
- }
345
- invariant(desiredApp, `No app found for values derived by the arguments: ${exerciseNumber}.${stepNumber}.${type}`);
346
- await setPlayground(desiredApp.fullPath);
347
- return replyWithText(`Playground set to ${desiredApp.name}`);
348
- }
349
- catch (error) {
350
- return replyWithError(error);
351
- }
352
- });
353
- // TODO: add preferences tools
354
- async function handleWorkshopDirectory(workshopDirectory) {
355
- if (workshopDirectory.endsWith('playground')) {
356
- workshopDirectory = path.join(workshopDirectory, '..');
357
- }
358
- await initApps(workshopDirectory);
359
- return workshopDirectory;
360
- }
361
- async function safeReadFile(filePath) {
362
- try {
363
- return await fs.readFile(filePath, 'utf-8');
364
- }
365
- catch {
366
- return null;
367
- }
368
- }
369
- function replyWithText(text) {
370
- return {
371
- content: [{ type: 'text', text }],
372
- };
373
- }
374
- function replyWithError(error) {
375
- return {
376
- content: [{ type: 'text', text: getErrorMessage(error) }],
377
- isError: true,
378
- };
379
- }
380
- function getErrorMessage(error, defaultMessage = 'Unknown Error') {
381
- if (typeof error === 'string')
382
- return error;
383
- if (error &&
384
- typeof error === 'object' &&
385
- 'message' in error &&
386
- typeof error.message === 'string') {
387
- return error.message;
388
- }
389
- return defaultMessage;
390
- }
391
- async function main() {
392
- const transport = new StdioServerTransport();
393
- await server.connect(transport);
394
- console.error('epicshop MCP Server running on stdio');
395
- }
396
- main().catch((error) => {
397
- console.error('Fatal error in main():', error);
398
- process.exit(1);
399
- });
400
- //# sourceMappingURL=cli.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { invariant } from '@epic-web/invariant'\nimport {\n\tgetApps,\n\tisPlaygroundApp,\n\tfindSolutionDir,\n\tgetFullPathFromAppName,\n\tinit as initApps,\n\textractNumbersAndTypeFromAppNameOrPath,\n\tgetExercise,\n\tgetPlaygroundApp,\n\tgetPlaygroundAppName,\n\tisProblemApp,\n\tisExerciseStepApp,\n\tsetPlayground,\n\ttype ExerciseStepApp,\n} from '@epic-web/workshop-utils/apps.server'\nimport { getAuthInfo } from '@epic-web/workshop-utils/db.server'\nimport {\n\tgetEpicVideoInfos,\n\tuserHasAccessToWorkshop,\n} from '@epic-web/workshop-utils/epic-api.server'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { type CallToolResult } from '@modelcontextprotocol/sdk/types.js'\nimport { z } from 'zod'\n\n// Create server instance\nconst server = new McpServer(\n\t{\n\t\tname: 'epicshop',\n\t\tversion: '1.0.0',\n\t\tcapabilities: {\n\t\t\ttools: {},\n\t\t},\n\t},\n\t{\n\t\tinstructions: `\nThis is intended to be used within a workshop using the Epic Workshop App\n(@epic-web/workshop-app) to help learners in the process of completing the\nworkshop exercises and understanding the learning outcomes.\n\nThe user's work in progress is in the \\`playground\\` directory. Any changes they\nask you to make should be in this directory.\n\t\t`.trim(),\n\t},\n)\n\nserver.tool(\n\t'get_diff_between_apps',\n\t`\nIntended to give context about the changes between two apps.\n\nThe output is a git diff of the playground directory as BASE (their work in\nprogress) against the solution directory as HEAD (the final state they're trying\nto achieve).\n\nThe output is formatted as a git diff.\n\nApp IDs are formatted as \\`{exerciseNumber}.{stepNumber}.{type}\\`.\n\nIf the user asks for the diff for 2.3, then use 02.03.problem for app1 and 02.03.solution for app2.\n\t`,\n\t{\n\t\tworkshopDirectory: z\n\t\t\t.string()\n\t\t\t.describe(\n\t\t\t\t'The workshop directory (the root directory of the workshop repo.).',\n\t\t\t),\n\t\tapp1: z.string().describe('The ID of the first app'),\n\t\tapp2: z.string().describe('The ID of the second app'),\n\t},\n\tasync ({ workshopDirectory, app1, app2 }) => {\n\t\ttry {\n\t\t\tawait handleWorkshopDirectory(workshopDirectory)\n\n\t\t\tconst { getDiffOutputWithRelativePaths } = await import(\n\t\t\t\t'@epic-web/workshop-utils/diff.server'\n\t\t\t)\n\n\t\t\tconst app1Name = extractNumbersAndTypeFromAppNameOrPath(app1)\n\t\t\tconst app2Name = extractNumbersAndTypeFromAppNameOrPath(app2)\n\n\t\t\tconst apps = await getApps()\n\t\t\tconst app1App = apps\n\t\t\t\t.filter(isExerciseStepApp)\n\t\t\t\t.find(\n\t\t\t\t\t(a) =>\n\t\t\t\t\t\ta.exerciseNumber === Number(app1Name?.exerciseNumber) &&\n\t\t\t\t\t\ta.stepNumber === Number(app1Name?.stepNumber) &&\n\t\t\t\t\t\ta.type === app1Name?.type,\n\t\t\t\t)\n\t\t\tconst app2App = apps\n\t\t\t\t.filter(isExerciseStepApp)\n\t\t\t\t.find(\n\t\t\t\t\t(a) =>\n\t\t\t\t\t\ta.exerciseNumber === Number(app2Name?.exerciseNumber) &&\n\t\t\t\t\t\ta.stepNumber === Number(app2Name?.stepNumber) &&\n\t\t\t\t\t\ta.type === app2Name?.type,\n\t\t\t\t)\n\n\t\t\tinvariant(app1App, `No app found for ${app1}`)\n\t\t\tinvariant(app2App, `No app found for ${app2}`)\n\n\t\t\tconst diffCode = await getDiffOutputWithRelativePaths(app1App, app2App)\n\n\t\t\tif (!diffCode) return replyWithText('No changes')\n\n\t\t\treturn replyWithText(diffCode)\n\t\t} catch (error) {\n\t\t\treturn replyWithError(error)\n\t\t}\n\t},\n)\n\nserver.tool(\n\t'get_exercise_step_progress_diff',\n\t`\nIntended to help a student understand what work they still have to complete.\n\nThis returns a git diff of the playground directory as BASE (their work in\nprogress) against the solution directory as HEAD (the final state they're trying\nto achieve). Meaning, if there are lines removed, it means they still need to\nadd those lines and if they are added, it means they still need to remove them.\n\nIf there's a diff with significant changes, you should explain what the changes\nare and their significance. Be brief. Let them tell you whether they need you to\nelaborate.\n\nThe output for this changes over time so it's useful to call multiple times.\n\nFor additional context, you can use the \\`get_exercise_instructions\\` tool\nto get the instructions for the current exercise step to help explain the\nsignificance of changes.\n\t`.trim(),\n\t{\n\t\tworkshopDirectory: z\n\t\t\t.string()\n\t\t\t.describe(\n\t\t\t\t'The workshop directory (the root directory of the workshop repo.).',\n\t\t\t),\n\t},\n\tasync ({ workshopDirectory }) => {\n\t\ttry {\n\t\t\tawait handleWorkshopDirectory(workshopDirectory)\n\n\t\t\tconst { getDiffOutputWithRelativePaths } = await import(\n\t\t\t\t'@epic-web/workshop-utils/diff.server'\n\t\t\t)\n\n\t\t\tconst apps = await getApps()\n\t\t\tconst playgroundApp = apps.find(isPlaygroundApp)\n\n\t\t\tif (!playgroundApp) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: 'text', text: 'No playground app found' }],\n\t\t\t\t\tisError: true,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst baseApp = playgroundApp\n\t\t\tconst solutionDir = await findSolutionDir({\n\t\t\t\tfullPath: await getFullPathFromAppName(playgroundApp.appName),\n\t\t\t})\n\t\t\tconst headApp = apps.find((a) => a.fullPath === solutionDir)\n\n\t\t\tif (!headApp) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: 'text', text: 'No playground solution app found' }],\n\t\t\t\t\tisError: true,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst diffCode = await getDiffOutputWithRelativePaths(baseApp, headApp)\n\n\t\t\tif (!diffCode) return replyWithText('No changes')\n\n\t\t\treturn replyWithText(diffCode)\n\t\t} catch (error) {\n\t\t\treturn replyWithError(error)\n\t\t}\n\t},\n)\n\nserver.tool(\n\t'get_exercise_context',\n\t`\nIntended to help a student understand what they need to do for the current\nexercise step.\n\nThis returns the instructions MDX content for the current exercise and each\nexercise step. If the user is has the paid version of the workshop, it will also\ninclude the transcript from each of the videos as well.\n\nThe output for this will rarely change, so it's unnecessary to call this tool\nmore than once.\n\n\\`get_exercise_context\\` is often best when used with the\n\\`get_exercise_step_progress_diff\\` tool to help a student understand what\nwork they still need to do and answer any questions about the exercise.\n\t`.trim(),\n\t{\n\t\tworkshopDirectory: z\n\t\t\t.string()\n\t\t\t.describe(\n\t\t\t\t'The workshop directory (the root directory of the workshop repo.).',\n\t\t\t),\n\t\texerciseNumber: z.coerce\n\t\t\t.number()\n\t\t\t.optional()\n\t\t\t.describe(\n\t\t\t\t`The exercise number to get the context for (defaults to the exercise number the playground is currently set to)`,\n\t\t\t),\n\t},\n\tasync ({ workshopDirectory, exerciseNumber }) => {\n\t\ttry {\n\t\t\tawait handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst userHasAccess = await userHasAccessToWorkshop()\n\t\t\tconst authInfo = await getAuthInfo()\n\t\t\tlet stepNumber = 1\n\t\t\tconst playgroundApp = await getPlaygroundApp()\n\t\t\tinvariant(playgroundApp, 'No playground app found')\n\t\t\tconst numbers = extractNumbersAndTypeFromAppNameOrPath(\n\t\t\t\tplaygroundApp.appName,\n\t\t\t)\n\t\t\tconst isCurrentExercise =\n\t\t\t\texerciseNumber === undefined ||\n\t\t\t\texerciseNumber === Number(numbers?.exerciseNumber)\n\t\t\tif (exerciseNumber === undefined) {\n\t\t\t\tinvariant(numbers, 'No numbers found in playground app name')\n\t\t\t\texerciseNumber = Number(numbers.exerciseNumber)\n\t\t\t\tstepNumber = Number(numbers.stepNumber)\n\t\t\t}\n\t\t\tconst exercise = await getExercise(exerciseNumber)\n\t\t\tinvariant(\n\t\t\t\texercise,\n\t\t\t\t`No exercise found for exercise number ${exerciseNumber}`,\n\t\t\t)\n\n\t\t\tconst videoInfos = await getEpicVideoInfos([\n\t\t\t\t...(exercise.instructionsEpicVideoEmbeds ?? []),\n\t\t\t\t...exercise.steps.flatMap((s) => s.problem?.epicVideoEmbeds ?? []),\n\t\t\t\t...exercise.steps.flatMap((s) => s.solution?.epicVideoEmbeds ?? []),\n\t\t\t\t...(exercise.finishedEpicVideoEmbeds ?? []),\n\t\t\t])\n\n\t\t\tfunction getTranscriptsElement(embeds?: Array<string>) {\n\t\t\t\tif (!embeds) return '<transcripts />'\n\t\t\t\tif (!userHasAccess && embeds.length) {\n\t\t\t\t\treturn `\n\t\t\t\t\t\t<transcripts>\n\t\t\t\t\t\t\tUser must upgrade before they can get access to ${embeds.length} transcript${embeds.length === 1 ? '' : 's'}.\n\t\t\t\t\t\t</transcripts>\n\t\t\t\t\t`.trim()\n\t\t\t\t}\n\t\t\t\tconst transcripts = ['<transcripts>']\n\t\t\t\tfor (const embed of embeds) {\n\t\t\t\t\tconst info = videoInfos[embed]\n\t\t\t\t\tif (info) {\n\t\t\t\t\t\tif (info.status === 'error') {\n\t\t\t\t\t\t\tif (info.type === 'region-restricted') {\n\t\t\t\t\t\t\t\ttranscripts.push(\n\t\t\t\t\t\t\t\t\t`\n\t\t\t\t\t\t\t\t\t<transcript\n\t\t\t\t\t\t\t\t\t\tembed=\"${embed}\"\n\t\t\t\t\t\t\t\t\t\tstatus=\"error\"\n\t\t\t\t\t\t\t\t\t\ttype=\"${info.type}\"\n\t\t\t\t\t\t\t\t\t\trequested-country=\"${info.requestCountry}\"\n\t\t\t\t\t\t\t\t\t\trestricted-country=\"${info.restrictedCountry}\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t`.trim(),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\ttranscripts.push(\n\t\t\t\t\t\t\t\t\t`\n\t\t\t\t\t\t\t\t\t<transcript\n\t\t\t\t\t\t\t\t\t\tembed=\"${embed}\"\n\t\t\t\t\t\t\t\t\t\tstatus=\"error\"\n\t\t\t\t\t\t\t\t\t\ttype=\"${info.type}\"\n\t\t\t\t\t\t\t\t\t\tstatus-code=\"${info.statusCode}\"\n\t\t\t\t\t\t\t\t\t\tstatus-text=\"${info.statusText}\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t`.trim(),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttranscripts.push(\n\t\t\t\t\t\t\t\t`<transcript embed=\"${embed}\" status=\"success\">${info.transcript}</transcript>`,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttranscripts.push(\n\t\t\t\t\t\t\t`<transcript embed=\"${embed}\" status=\"error\" type=\"not-found\">No transcript found</transcript>`,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttranscripts.push('</transcripts>')\n\t\t\t\treturn transcripts.join('\\n')\n\t\t\t}\n\n\t\t\tasync function getFileContentElement(filePath: string) {\n\t\t\t\treturn `<file path=\"${filePath}\">${(await safeReadFile(filePath)) ?? 'None found'}</file>`\n\t\t\t}\n\t\t\tlet text = `\nBelow is all the context for this exercise and each step.\n\n<currentContext>\n\t<user hasAccess=\"${userHasAccess}\" isAuthenticated=\"${Boolean(authInfo)}\" email=\"${authInfo?.email}\" />\n\t${\n\t\tisCurrentExercise\n\t\t\t? `<playground>\n\t\t<exerciseNumber>${exerciseNumber}</exerciseNumber>\n\t\t<stepNumber>${stepNumber}</stepNumber>\n\t</playground>`\n\t\t\t: '<playground>currently set to a different exercise</playground>'\n\t}\n</currentContext>\n\n<exerciseBackground number=\"${exerciseNumber}\">\n\t<intro>\n\t\t${await getFileContentElement(path.join(exercise.fullPath, 'README.mdx'))}\n\t\t${getTranscriptsElement(exercise.instructionsEpicVideoEmbeds)}\n\t</intro>\n\t<outro>\n\t\t${await getFileContentElement(path.join(exercise.fullPath, 'FINISHED.mdx'))}\n\t\t${getTranscriptsElement(exercise.finishedEpicVideoEmbeds)}\n\t</outro>\n</exerciseBackground>\n\t\t\t`.trim()\n\n\t\t\tif (exercise.steps) {\n\t\t\t\ttext += '\\n\\n<steps>'\n\t\t\t\tfor (const app of exercise.steps) {\n\t\t\t\t\ttext += `\n<step number=\"${app.stepNumber}\" isCurrent=\"${isCurrentExercise && app.stepNumber === stepNumber}\">\n\t<problem>\n\t\t${app.problem ? await getFileContentElement(path.join(app.problem?.fullPath, `README.mdx`)) : 'No problem found'}\n\t\t${getTranscriptsElement(app.problem?.epicVideoEmbeds ?? [])}\n\t</problem>\n\t<solution>\n\t\t${app.solution ? await getFileContentElement(path.join(app.solution?.fullPath, `README.mdx`)) : 'No solution found'}\n\t\t${getTranscriptsElement(app.solution?.epicVideoEmbeds ?? [])}\n\t</solution>\n</step>`\n\t\t\t\t}\n\t\t\t\ttext += '</steps>\\n\\n'\n\n\t\t\t\ttext += `Reminder, the current step is ${stepNumber} of ${exercise.steps.length + 1}. The most relevant information will be in the context abouve within the current step.`\n\t\t\t} else {\n\t\t\t\ttext += `Unusually, this exercise has no steps.`\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text', text: text }],\n\t\t\t}\n\t\t} catch (error) {\n\t\t\treturn replyWithError(error)\n\t\t}\n\t},\n)\n\nserver.tool(\n\t'set_playground',\n\t`\nSets the playground environment so the user can continue to that exercise or see\nwhat that step looks like in their playground environment.\n\nNOTE: this will override their current exercise step work in the playground!\n\nGenerally, it is better to not provide an exerciseNumber, stepNumber, and type\nand let the user continue to the next exercise. Only provide these arguments if\nthe user explicitely asks to go to a specific exercise or step. If the user asks\nto start an exercise, specify stepNumber 1 and type 'problem' unless otherwise\ndirected.\n\nArgument examples:\nA. Set to next exercise step from current (or first if there is none) - Most common\n\t- [No arguments]\nB. Set to a specific exercise step\n\t- exerciseNumber: 1\n\t- stepNumber: 1\n\t- type: 'solution'\nC. Set to the solution of the current exercise step\n\t- type: 'solution'\nD. Set to the second step problem of the current exercise\n\t- stepNumber: 2\nE. Set to the first step problem of the fifth exercise\n\t- exerciseNumber: 5\n\nAn error will be returned if no app is found for the given arguments.\n\t`.trim(),\n\t{\n\t\tworkshopDirectory: z.string().describe('The workshop directory'),\n\t\texerciseNumber: z.coerce\n\t\t\t.number()\n\t\t\t.optional()\n\t\t\t.describe('The exercise number to set the playground to'),\n\t\tstepNumber: z.coerce\n\t\t\t.number()\n\t\t\t.optional()\n\t\t\t.describe('The step number to set the playground to'),\n\t\ttype: z\n\t\t\t.enum(['problem', 'solution'])\n\t\t\t.optional()\n\t\t\t.describe('The type of app to set the playground to'),\n\t},\n\tasync ({ workshopDirectory, exerciseNumber, stepNumber, type }) => {\n\t\ttry {\n\t\t\tawait handleWorkshopDirectory(workshopDirectory)\n\n\t\t\tconst apps = await getApps()\n\t\t\tconst exerciseStepApps = apps.filter(isExerciseStepApp)\n\n\t\t\tconst playgroundAppName = await getPlaygroundAppName()\n\t\t\tconst currentExerciseStepAppIndex = exerciseStepApps.findIndex(\n\t\t\t\t(a) => a.name === playgroundAppName,\n\t\t\t)\n\n\t\t\tlet desiredApp: ExerciseStepApp | undefined\n\t\t\t// if nothing was provided, set to the next step problem app\n\t\t\tconst noArgumentsProvided = !exerciseNumber && !stepNumber && !type\n\t\t\tif (noArgumentsProvided) {\n\t\t\t\tdesiredApp = exerciseStepApps\n\t\t\t\t\t.slice(currentExerciseStepAppIndex + 1)\n\t\t\t\t\t.find(isProblemApp)\n\t\t\t\tinvariant(desiredApp, 'No next problem app found to set playground to')\n\t\t\t} else {\n\t\t\t\tconst currentExerciseStepApp =\n\t\t\t\t\texerciseStepApps[currentExerciseStepAppIndex]\n\n\t\t\t\t// otherwise, default to the current exercise step app for arguments\n\t\t\t\texerciseNumber ??= currentExerciseStepApp?.exerciseNumber\n\t\t\t\tstepNumber ??= currentExerciseStepApp?.stepNumber\n\t\t\t\ttype ??= currentExerciseStepApp?.type\n\n\t\t\t\tdesiredApp = exerciseStepApps.find(\n\t\t\t\t\t(a) =>\n\t\t\t\t\t\ta.exerciseNumber === exerciseNumber &&\n\t\t\t\t\t\ta.stepNumber === stepNumber &&\n\t\t\t\t\t\ta.type === type,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tinvariant(\n\t\t\t\tdesiredApp,\n\t\t\t\t`No app found for values derived by the arguments: ${exerciseNumber}.${stepNumber}.${type}`,\n\t\t\t)\n\t\t\tawait setPlayground(desiredApp.fullPath)\n\t\t\treturn replyWithText(`Playground set to ${desiredApp.name}`)\n\t\t} catch (error) {\n\t\t\treturn replyWithError(error)\n\t\t}\n\t},\n)\n\n// TODO: add preferences tools\n\nasync function handleWorkshopDirectory(workshopDirectory: string) {\n\tif (workshopDirectory.endsWith('playground')) {\n\t\tworkshopDirectory = path.join(workshopDirectory, '..')\n\t}\n\n\tawait initApps(workshopDirectory)\n\treturn workshopDirectory\n}\n\nasync function safeReadFile(filePath: string) {\n\ttry {\n\t\treturn await fs.readFile(filePath, 'utf-8')\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction replyWithText(text: string): CallToolResult {\n\treturn {\n\t\tcontent: [{ type: 'text', text }],\n\t}\n}\n\nfunction replyWithError(error: unknown): CallToolResult {\n\treturn {\n\t\tcontent: [{ type: 'text', text: getErrorMessage(error) }],\n\t\tisError: true,\n\t}\n}\n\nfunction getErrorMessage(\n\terror: unknown,\n\tdefaultMessage: string = 'Unknown Error',\n) {\n\tif (typeof error === 'string') return error\n\tif (\n\t\terror &&\n\t\ttypeof error === 'object' &&\n\t\t'message' in error &&\n\t\ttypeof error.message === 'string'\n\t) {\n\t\treturn error.message\n\t}\n\treturn defaultMessage\n}\n\nasync function main() {\n\tconst transport = new StdioServerTransport()\n\tawait server.connect(transport)\n\tconsole.error('epicshop MCP Server running on stdio')\n}\n\nmain().catch((error) => {\n\tconsole.error('Fatal error in main():', error)\n\tprocess.exit(1)\n})\n"]}