@epic-web/workshop-mcp 5.13.6 → 5.14.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 CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs/promises';
3
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';
4
8
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
10
  import { z } from 'zod';
@@ -33,7 +37,9 @@ If there's a diff with significant changes, you should explain what the changes
33
37
  are and their significance. Be brief. Let them tell you whether they need you to
34
38
  elaborate.
35
39
 
36
- For additional context, you can use the \`get_exercise_step_instructions\` tool
40
+ The output for this changes over time so it's useful to call multiple times.
41
+
42
+ For additional context, you can use the \`get_exercise_instructions\` tool
37
43
  to get the instructions for the current exercise step to help explain the
38
44
  significance of changes.
39
45
  `.trim(), {
@@ -41,81 +47,287 @@ significance of changes.
41
47
  .string()
42
48
  .describe('The workshop directory (the root directory of the workshop repo. Best to not bother asking the user and just use the project root path).'),
43
49
  }, async ({ workshopDirectory }) => {
44
- if (workshopDirectory.endsWith('playground')) {
45
- workshopDirectory = path.join(workshopDirectory, '..');
46
- }
47
- process.env.EPICSHOP_CONTEXT_CWD = workshopDirectory;
48
- const { getApps, isPlaygroundApp, findSolutionDir, getFullPathFromAppName, init, } = await import('@epic-web/workshop-utils/apps.server');
49
- await init();
50
- const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
51
- const apps = await getApps();
52
- const playgroundApp = apps.find(isPlaygroundApp);
53
- if (!playgroundApp) {
54
- return {
55
- content: [{ type: 'text', text: 'No playground app found' }],
56
- isError: true,
57
- };
50
+ try {
51
+ await handleWorkshopDirectory(workshopDirectory);
52
+ const { getDiffOutputWithRelativePaths } = await import('@epic-web/workshop-utils/diff.server');
53
+ const apps = await getApps();
54
+ const playgroundApp = apps.find(isPlaygroundApp);
55
+ if (!playgroundApp) {
56
+ return {
57
+ content: [{ type: 'text', text: 'No playground app found' }],
58
+ isError: true,
59
+ };
60
+ }
61
+ const baseApp = playgroundApp;
62
+ const solutionDir = await findSolutionDir({
63
+ fullPath: await getFullPathFromAppName(playgroundApp.appName),
64
+ });
65
+ const headApp = apps.find((a) => a.fullPath === solutionDir);
66
+ if (!headApp) {
67
+ return {
68
+ content: [{ type: 'text', text: 'No playground solution app found' }],
69
+ isError: true,
70
+ };
71
+ }
72
+ const diffCode = await getDiffOutputWithRelativePaths(baseApp, headApp);
73
+ if (!diffCode)
74
+ return replyWithText('No changes');
75
+ return replyWithText(diffCode);
58
76
  }
59
- const baseApp = playgroundApp;
60
- const solutionDir = await findSolutionDir({
61
- fullPath: await getFullPathFromAppName(playgroundApp.appName),
62
- });
63
- const headApp = apps.find((a) => a.fullPath === solutionDir);
64
- if (!headApp) {
65
- return {
66
- content: [{ type: 'text', text: 'No playground solution app found' }],
67
- isError: true,
68
- };
69
- }
70
- const diffCode = await getDiffOutputWithRelativePaths(baseApp, headApp);
71
- if (!diffCode) {
72
- return {
73
- content: [{ type: 'text', text: 'No changes' }],
74
- };
77
+ catch (error) {
78
+ return replyWithError(error);
75
79
  }
76
- return {
77
- content: [
78
- {
79
- type: 'text',
80
- text: diffCode,
81
- },
82
- ],
83
- };
84
80
  });
85
- server.tool('get_exercise_step_instructions', `
81
+ server.tool('get_exercise_context', `
86
82
  Intended to help a student understand what they need to do for the current
87
83
  exercise step.
88
84
 
89
- This returns the instructions MDX content for the current exercise step. It's
90
- often best when used with the \`get_exercise_step_progress_diff\` tool to help
91
- a student understand what work they still need to do.
85
+ This returns the instructions MDX content for the current exercise and each
86
+ exercise step. If the user is has the paid version of the workshop, it will also
87
+ include the transcript from each of the videos as well.
88
+
89
+ The output for this will rarely change, so it's unnecessary to call this tool
90
+ more than once.
91
+
92
+ \`get_exercise_context\` is often best when used with the
93
+ \`get_exercise_step_progress_diff\` tool to help a student understand what
94
+ work they still need to do and answer any questions about the exercise.
92
95
  `.trim(), {
93
96
  workshopDirectory: z
94
97
  .string()
95
98
  .describe('The workshop directory (the root directory of the workshop repo. Best to not bother asking the user and just use the project root path).'),
96
99
  }, async ({ workshopDirectory }) => {
100
+ try {
101
+ await handleWorkshopDirectory(workshopDirectory);
102
+ const userHasAccess = await userHasAccessToWorkshop();
103
+ const authInfo = await getAuthInfo();
104
+ const playgroundApp = await getPlaygroundApp();
105
+ invariant(playgroundApp, 'No playground app found');
106
+ const numbers = extractNumbersAndTypeFromAppNameOrPath(playgroundApp.appName);
107
+ invariant(numbers, 'No numbers found in playground app name');
108
+ const { exerciseNumber, stepNumber } = numbers;
109
+ const exercise = await getExercise(exerciseNumber);
110
+ invariant(exercise, `No exercise found for exercise number ${exerciseNumber}`);
111
+ const videoInfos = await getEpicVideoInfos([
112
+ ...(exercise.instructionsEpicVideoEmbeds ?? []),
113
+ ...exercise.steps.flatMap((s) => s.problem?.epicVideoEmbeds ?? []),
114
+ ...exercise.steps.flatMap((s) => s.solution?.epicVideoEmbeds ?? []),
115
+ ...(exercise.finishedEpicVideoEmbeds ?? []),
116
+ ]);
117
+ function getTranscriptsElement(embeds) {
118
+ if (!embeds)
119
+ return '<transcripts />';
120
+ if (!userHasAccess && embeds.length) {
121
+ return `
122
+ <transcripts>
123
+ User must upgrade before they can get access to ${embeds.length} transcript${embeds.length === 1 ? '' : 's'}.
124
+ </transcripts>
125
+ `.trim();
126
+ }
127
+ const transcripts = ['<transcripts>'];
128
+ for (const embed of embeds) {
129
+ const info = videoInfos[embed];
130
+ if (info) {
131
+ if (info.status === 'error') {
132
+ if (info.type === 'region-restricted') {
133
+ transcripts.push(`
134
+ <transcript
135
+ embed="${embed}"
136
+ status="error"
137
+ type="${info.type}"
138
+ requested-country="${info.requestCountry}"
139
+ restricted-country="${info.restrictedCountry}"
140
+ />
141
+ `.trim());
142
+ }
143
+ else {
144
+ transcripts.push(`
145
+ <transcript
146
+ embed="${embed}"
147
+ status="error"
148
+ type="${info.type}"
149
+ status-code="${info.statusCode}"
150
+ status-text="${info.statusText}"
151
+ />
152
+ `.trim());
153
+ }
154
+ }
155
+ else {
156
+ transcripts.push(`<transcript embed="${embed}" status="success">${info.transcript}</transcript>`);
157
+ }
158
+ }
159
+ else {
160
+ transcripts.push(`<transcript embed="${embed}" status="error" type="not-found">No transcript found</transcript>`);
161
+ }
162
+ }
163
+ transcripts.push('</transcripts>');
164
+ return transcripts.join('\n');
165
+ }
166
+ function getFileContentElement(filePath) {
167
+ return `<file path="${filePath}">${safeReadFile(filePath) ?? 'None found'}</file>`;
168
+ }
169
+ let text = `
170
+ Below is all the context for this exercise and each step.
171
+
172
+ <currentContext>
173
+ <user hasAccess="${userHasAccess}" isAuthenticated="${Boolean(authInfo)}" email="${authInfo?.email}" />
174
+ <playground>
175
+ <exerciseNumber>${exerciseNumber}</exerciseNumber>
176
+ <stepNumber>${stepNumber}</stepNumber>
177
+ </playground>
178
+ </currentContext>
179
+
180
+ <exerciseBackground number="${exerciseNumber}">
181
+ <intro>
182
+ ${getFileContentElement(path.join(exercise.fullPath, 'README.mdx'))}
183
+ ${getTranscriptsElement(exercise.instructionsEpicVideoEmbeds)}
184
+ </intro>
185
+ <outro>
186
+ ${getFileContentElement(path.join(exercise.fullPath, 'FINISHED.mdx'))}
187
+ ${getTranscriptsElement(exercise.finishedEpicVideoEmbeds)}
188
+ </outro>
189
+ </exerciseBackground>
190
+ `.trim();
191
+ if (exercise.steps) {
192
+ text += '\n\n<steps>';
193
+ for (const app of exercise.steps) {
194
+ text += `
195
+ <step number="${app.stepNumber}" isCurrent="${app.stepNumber === Number(stepNumber)}">
196
+ <problem>
197
+ ${app.problem ? getFileContentElement(path.join(app.problem?.fullPath, `README.mdx`)) : 'No problem found'}
198
+ ${getTranscriptsElement(app.problem?.epicVideoEmbeds ?? [])}
199
+ </problem>
200
+ <solution>
201
+ ${app.solution ? getFileContentElement(path.join(app.solution?.fullPath, `README.mdx`)) : 'No solution found'}
202
+ ${getTranscriptsElement(app.solution?.epicVideoEmbeds ?? [])}
203
+ </solution>
204
+ </step>`;
205
+ }
206
+ text += '</steps>\n\n';
207
+ 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.`;
208
+ }
209
+ else {
210
+ text += `Unusually, this exercise has no steps.`;
211
+ }
212
+ return {
213
+ content: [{ type: 'text', text: text }],
214
+ };
215
+ }
216
+ catch (error) {
217
+ return replyWithError(error);
218
+ }
219
+ });
220
+ server.tool('set_playground', `
221
+ Sets the playground environment so the user can continue to that exercise or see
222
+ what that step looks like in their playground environment.
223
+
224
+ NOTE: this will override their current exercise step work in the playground!
225
+
226
+ Generally, it is better to not provide an exerciseNumber, stepNumber, and type
227
+ and let the user continue to the next exercise. Only provide these arguments if
228
+ the user explicitely asks to go to a specific exercise or step.
229
+
230
+ Argument examples:
231
+ A. Set to next exercise step from current (or first if there is none) - Most common
232
+ - [No arguments]
233
+ B. Set to a specific exercise step
234
+ - exerciseNumber: 1
235
+ - stepNumber: 1
236
+ - type: 'solution'
237
+ C. Set to the solution of the current exercise step
238
+ - type: 'solution'
239
+ D. Set to the second step problem of the current exercise
240
+ - stepNumber: 2
241
+ E. Set to the first step problem of the fifth exercise
242
+ - exerciseNumber: 5
243
+
244
+ An error will be returned if no app is found for the given arguments.
245
+ `.trim(), {
246
+ workshopDirectory: z.string().describe('The workshop directory'),
247
+ exerciseNumber: z.coerce
248
+ .number()
249
+ .optional()
250
+ .describe('The exercise number to set the playground to'),
251
+ stepNumber: z.coerce
252
+ .number()
253
+ .optional()
254
+ .describe('The step number to set the playground to'),
255
+ type: z
256
+ .enum(['problem', 'solution'])
257
+ .optional()
258
+ .describe('The type of app to set the playground to'),
259
+ }, async ({ workshopDirectory, exerciseNumber, stepNumber, type }) => {
260
+ try {
261
+ await handleWorkshopDirectory(workshopDirectory);
262
+ const apps = await getApps();
263
+ const exerciseStepApps = apps.filter(isExerciseStepApp);
264
+ const playgroundAppName = await getPlaygroundAppName();
265
+ const currentExerciseStepAppIndex = exerciseStepApps.findIndex((a) => a.name === playgroundAppName);
266
+ let desiredApp;
267
+ // if nothing was provided, set to the next step problem app
268
+ const noArgumentsProvided = !exerciseNumber && !stepNumber && !type;
269
+ if (noArgumentsProvided) {
270
+ desiredApp = exerciseStepApps
271
+ .slice(currentExerciseStepAppIndex + 1)
272
+ .find(isProblemApp);
273
+ invariant(desiredApp, 'No next problem app found to set playground to');
274
+ }
275
+ else {
276
+ const currentExerciseStepApp = exerciseStepApps[currentExerciseStepAppIndex];
277
+ // otherwise, default to the current exercise step app for arguments
278
+ exerciseNumber ??= currentExerciseStepApp?.exerciseNumber;
279
+ stepNumber ??= currentExerciseStepApp?.stepNumber;
280
+ type ??= 'problem';
281
+ desiredApp = exerciseStepApps.find((a) => a.exerciseNumber === exerciseNumber &&
282
+ a.stepNumber === stepNumber &&
283
+ a.type === type);
284
+ }
285
+ invariant(desiredApp, `No app found for values derived by the arguments: ${exerciseNumber}.${stepNumber}.${type}`);
286
+ await setPlayground(desiredApp.fullPath);
287
+ return replyWithText(`Playground set to ${desiredApp.name}`);
288
+ }
289
+ catch (error) {
290
+ return replyWithError(error);
291
+ }
292
+ });
293
+ // TODO: add preferences tools
294
+ async function handleWorkshopDirectory(workshopDirectory) {
97
295
  if (workshopDirectory.endsWith('playground')) {
98
296
  workshopDirectory = path.join(workshopDirectory, '..');
99
297
  }
100
- process.env.EPICSHOP_CONTEXT_CWD = workshopDirectory;
101
- const { getApps, isPlaygroundApp } = await import('@epic-web/workshop-utils/apps.server');
102
- const apps = await getApps();
103
- const playgroundApp = apps.find(isPlaygroundApp);
104
- if (!playgroundApp) {
105
- return {
106
- content: [{ type: 'text', text: 'No playground app found' }],
107
- isError: true,
108
- };
298
+ await initApps(workshopDirectory);
299
+ return workshopDirectory;
300
+ }
301
+ async function safeReadFile(filePath) {
302
+ try {
303
+ return await fs.readFile(filePath, 'utf-8');
304
+ }
305
+ catch {
306
+ return null;
109
307
  }
308
+ }
309
+ function replyWithText(text) {
110
310
  return {
111
- content: [
112
- {
113
- type: 'text',
114
- text: await fs.readFile(path.join(playgroundApp.fullPath, 'README.mdx'), 'utf-8'),
115
- },
116
- ],
311
+ content: [{ type: 'text', text }],
117
312
  };
118
- });
313
+ }
314
+ function replyWithError(error) {
315
+ return {
316
+ content: [{ type: 'text', text: getErrorMessage(error) }],
317
+ isError: true,
318
+ };
319
+ }
320
+ function getErrorMessage(error, defaultMessage = 'Unknown Error') {
321
+ if (typeof error === 'string')
322
+ return error;
323
+ if (error &&
324
+ typeof error === 'object' &&
325
+ 'message' in error &&
326
+ typeof error.message === 'string') {
327
+ return error.message;
328
+ }
329
+ return defaultMessage;
330
+ }
119
331
  async function main() {
120
332
  const transport = new StdioServerTransport();
121
333
  await server.connect(transport);
@@ -1 +1 @@
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 { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.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_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\nFor additional context, you can use the \\`get_exercise_step_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. Best to not bother asking the user and just use the project root path).',\n\t\t\t),\n\t},\n\tasync ({ workshopDirectory }) => {\n\t\tif (workshopDirectory.endsWith('playground')) {\n\t\t\tworkshopDirectory = path.join(workshopDirectory, '..')\n\t\t}\n\t\tprocess.env.EPICSHOP_CONTEXT_CWD = workshopDirectory\n\n\t\tconst {\n\t\t\tgetApps,\n\t\t\tisPlaygroundApp,\n\t\t\tfindSolutionDir,\n\t\t\tgetFullPathFromAppName,\n\t\t\tinit,\n\t\t} = await import('@epic-web/workshop-utils/apps.server')\n\t\tawait init()\n\n\t\tconst { getDiffOutputWithRelativePaths } = await import(\n\t\t\t'@epic-web/workshop-utils/diff.server'\n\t\t)\n\n\t\tconst apps = await getApps()\n\t\tconst playgroundApp = apps.find(isPlaygroundApp)\n\n\t\tif (!playgroundApp) {\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text', text: 'No playground app found' }],\n\t\t\t\tisError: true,\n\t\t\t}\n\t\t}\n\n\t\tconst baseApp = playgroundApp\n\t\tconst solutionDir = await findSolutionDir({\n\t\t\tfullPath: await getFullPathFromAppName(playgroundApp.appName),\n\t\t})\n\t\tconst headApp = apps.find((a) => a.fullPath === solutionDir)\n\n\t\tif (!headApp) {\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text', text: 'No playground solution app found' }],\n\t\t\t\tisError: true,\n\t\t\t}\n\t\t}\n\n\t\tconst diffCode = await getDiffOutputWithRelativePaths(baseApp, headApp)\n\n\t\tif (!diffCode) {\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text', text: 'No changes' }],\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'text',\n\t\t\t\t\ttext: diffCode,\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\t},\n)\n\nserver.tool(\n\t'get_exercise_step_instructions',\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 step. It's\noften best when used with the \\`get_exercise_step_progress_diff\\` tool to help\na student understand what work they still need to do.\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. Best to not bother asking the user and just use the project root path).',\n\t\t\t),\n\t},\n\tasync ({ workshopDirectory }) => {\n\t\tif (workshopDirectory.endsWith('playground')) {\n\t\t\tworkshopDirectory = path.join(workshopDirectory, '..')\n\t\t}\n\t\tprocess.env.EPICSHOP_CONTEXT_CWD = workshopDirectory\n\n\t\tconst { getApps, isPlaygroundApp } = await import(\n\t\t\t'@epic-web/workshop-utils/apps.server'\n\t\t)\n\t\tconst apps = await getApps()\n\t\tconst playgroundApp = apps.find(isPlaygroundApp)\n\t\tif (!playgroundApp) {\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text', text: 'No playground app found' }],\n\t\t\t\tisError: true,\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'text',\n\t\t\t\t\ttext: await fs.readFile(\n\t\t\t\t\t\tpath.join(playgroundApp.fullPath, 'README.mdx'),\n\t\t\t\t\t\t'utf-8',\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\t},\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"]}
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_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. Best to not bother asking the user and just use the project root path).',\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. Best to not bother asking the user and just use the project root path).',\n\t\t\t),\n\t},\n\tasync ({ workshopDirectory }) => {\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\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\tinvariant(numbers, 'No numbers found in playground app name')\n\t\t\tconst { exerciseNumber, stepNumber } = numbers\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\tfunction getFileContentElement(filePath: string) {\n\t\t\t\treturn `<file path=\"${filePath}\">${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<playground>\n\t\t<exerciseNumber>${exerciseNumber}</exerciseNumber>\n\t\t<stepNumber>${stepNumber}</stepNumber>\n\t</playground>\n</currentContext>\n\n<exerciseBackground number=\"${exerciseNumber}\">\n\t<intro>\n\t\t${getFileContentElement(path.join(exercise.fullPath, 'README.mdx'))}\n\t\t${getTranscriptsElement(exercise.instructionsEpicVideoEmbeds)}\n\t</intro>\n\t<outro>\n\t\t${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=\"${app.stepNumber === Number(stepNumber)}\">\n\t<problem>\n\t\t${app.problem ? 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 ? 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.\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 ??= 'problem'\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-mcp",
3
- "version": "5.13.6",
3
+ "version": "5.14.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -24,19 +24,22 @@
24
24
  "dist"
25
25
  ],
26
26
  "scripts": {
27
+ "dev": "tsx src/cli.ts",
27
28
  "typecheck": "tsc -b --noEmit",
28
29
  "build": "tshy",
29
30
  "build:watch": "nx watch --projects=@epic-web/workshop-mcp -- nx run \\$NX_PROJECT_NAME:build"
30
31
  },
31
32
  "dependencies": {
32
- "@epic-web/workshop-utils": "5.13.6",
33
+ "@epic-web/workshop-utils": "5.14.0",
34
+ "@epic-web/invariant": "^1.0.0",
33
35
  "@modelcontextprotocol/sdk": "^1.9.0",
34
36
  "zod": "^3.24.2"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@types/node": "^22.14.1",
38
40
  "tshy": "^3.0.2",
39
- "typescript": "^5.8.3"
41
+ "typescript": "^5.8.3",
42
+ "tsx": "^4.19.0"
40
43
  },
41
44
  "repository": {
42
45
  "type": "git",