@epic-web/workshop-mcp 0.0.0-semantically-released

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.
@@ -0,0 +1,454 @@
1
+ import { invariant } from '@epic-web/invariant';
2
+ import { getApps, getExerciseApp, getPlaygroundAppName, isExerciseStepApp, isProblemApp, setPlayground, } from '@epic-web/workshop-utils/apps.server';
3
+ import { deleteCache } from '@epic-web/workshop-utils/cache.server';
4
+ import { getWorkshopConfig } from '@epic-web/workshop-utils/config.server';
5
+ import { getAuthInfo, logout, setAuthInfo, } from '@epic-web/workshop-utils/db.server';
6
+ import { getProgress, getUserInfo, updateProgress, } from '@epic-web/workshop-utils/epic-api.server';
7
+ import * as client from 'openid-client';
8
+ import { z } from 'zod';
9
+ 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';
12
+ // not enough support for this yet
13
+ const clientSupportsEmbeddedResources = false;
14
+ export function initTools(server) {
15
+ server.tool('login', `Allow the user to login (or sign up) to the workshop. First`.trim(), {
16
+ workshopDirectory: workshopDirectoryInputSchema,
17
+ }, async ({ workshopDirectory }) => {
18
+ await handleWorkshopDirectory(workshopDirectory);
19
+ const { product: { host }, } = getWorkshopConfig();
20
+ const ISSUER = `https://${host}/oauth`;
21
+ const config = await client.discovery(new URL(ISSUER), 'EPICSHOP_APP');
22
+ const deviceResponse = await client.initiateDeviceAuthorization(config, {});
23
+ void handleAuthFlow();
24
+ return {
25
+ content: [
26
+ {
27
+ type: 'text',
28
+ text: `Please go to ${deviceResponse.verification_uri_complete}. Verify the code on the page is "${deviceResponse.user_code}" to login.`,
29
+ },
30
+ ],
31
+ };
32
+ async function handleAuthFlow() {
33
+ const UserInfoSchema = z.object({
34
+ id: z.string(),
35
+ email: z.string(),
36
+ name: z.string().nullable().optional(),
37
+ });
38
+ const timeout = setTimeout(() => {
39
+ void server.server.notification({
40
+ method: 'notification',
41
+ params: {
42
+ message: 'Device authorization timed out',
43
+ },
44
+ });
45
+ }, deviceResponse.expires_in * 1000);
46
+ try {
47
+ const tokenSet = await client.pollDeviceAuthorizationGrant(config, deviceResponse);
48
+ clearTimeout(timeout);
49
+ if (!tokenSet) {
50
+ await server.server.notification({
51
+ method: 'notification',
52
+ params: {
53
+ message: 'No token set',
54
+ },
55
+ });
56
+ return;
57
+ }
58
+ const protectedResourceResponse = await client.fetchProtectedResource(config, tokenSet.access_token, new URL(`${ISSUER}/userinfo`), 'GET');
59
+ const userinfoRaw = await protectedResourceResponse.json();
60
+ const userinfoResult = UserInfoSchema.safeParse(userinfoRaw);
61
+ if (!userinfoResult.success) {
62
+ await server.server.notification({
63
+ method: 'notification',
64
+ params: {
65
+ message: `Failed to parse user info: ${userinfoResult.error.message}`,
66
+ },
67
+ });
68
+ return;
69
+ }
70
+ const userinfo = userinfoResult.data;
71
+ await setAuthInfo({
72
+ id: userinfo.id,
73
+ tokenSet,
74
+ email: userinfo.email,
75
+ name: userinfo.name,
76
+ });
77
+ await getUserInfo({ forceFresh: true });
78
+ await server.server.notification({
79
+ method: 'notification',
80
+ params: {
81
+ message: 'Authentication successful',
82
+ },
83
+ });
84
+ }
85
+ catch (error) {
86
+ clearTimeout(timeout);
87
+ throw error;
88
+ }
89
+ }
90
+ });
91
+ server.tool('logout', `Allow the user to logout of the workshop (based on the workshop's host) and delete cache data.`, {
92
+ workshopDirectory: workshopDirectoryInputSchema,
93
+ }, async ({ workshopDirectory }) => {
94
+ await handleWorkshopDirectory(workshopDirectory);
95
+ await logout();
96
+ await deleteCache();
97
+ return {
98
+ content: [{ type: 'text', text: 'Logged out' }],
99
+ };
100
+ });
101
+ server.tool('set_playground', `
102
+ Sets the playground environment so the user can continue to that exercise or see
103
+ what that step looks like in their playground environment.
104
+
105
+ NOTE: this will override their current exercise step work in the playground!
106
+
107
+ Generally, it is better to not provide an exerciseNumber, stepNumber, and type
108
+ and let the user continue to the next exercise. Only provide these arguments if
109
+ the user explicitely asks to go to a specific exercise or step. If the user asks
110
+ to start an exercise, specify stepNumber 1 and type 'problem' unless otherwise
111
+ directed.
112
+
113
+ Argument examples:
114
+ A. If logged in and there is an incomplete exercise step, set to next incomplete exercise step based on the user's progress - Most common
115
+ - [No arguments]
116
+ B. If not logged in or all exercises are complete, set to next exercise step from current (or first if there is none)
117
+ - [No arguments]
118
+ C. Set to a specific exercise step
119
+ - exerciseNumber: 1
120
+ - stepNumber: 1
121
+ - type: 'solution'
122
+ D. Set to the solution of the current exercise step
123
+ - type: 'solution'
124
+ E. Set to the second step problem of the current exercise
125
+ - stepNumber: 2
126
+ F. Set to the first step problem of the fifth exercise
127
+ - exerciseNumber: 5
128
+
129
+ An error will be returned if no app is found for the given arguments.
130
+ `.trim(), {
131
+ workshopDirectory: workshopDirectoryInputSchema,
132
+ exerciseNumber: z.coerce
133
+ .number()
134
+ .optional()
135
+ .describe('The exercise number to set the playground to'),
136
+ stepNumber: z.coerce
137
+ .number()
138
+ .optional()
139
+ .describe('The step number to set the playground to'),
140
+ type: z
141
+ .enum(['problem', 'solution'])
142
+ .optional()
143
+ .describe('The type of app to set the playground to'),
144
+ }, async ({ workshopDirectory, exerciseNumber, stepNumber, type }) => {
145
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
146
+ const authInfo = await getAuthInfo();
147
+ if (authInfo) {
148
+ const progress = await getProgress();
149
+ const scoreProgress = (a) => {
150
+ if (a.type === 'workshop-instructions')
151
+ return 0;
152
+ if (a.type === 'workshop-finished')
153
+ return 10000;
154
+ if (a.type === 'instructions')
155
+ return a.exerciseNumber * 100;
156
+ if (a.type === 'step')
157
+ return a.exerciseNumber * 100 + a.stepNumber;
158
+ if (a.type === 'finished')
159
+ return a.exerciseNumber * 100 + 100;
160
+ if (a.type === 'unknown')
161
+ return 100000;
162
+ return -1;
163
+ };
164
+ const sortedProgress = progress.sort((a, b) => {
165
+ return scoreProgress(a) - scoreProgress(b);
166
+ });
167
+ const nextProgress = sortedProgress.find((p) => !p.epicCompletedAt);
168
+ if (nextProgress) {
169
+ if (nextProgress.type === 'step') {
170
+ const exerciseApp = await getExerciseApp({
171
+ exerciseNumber: nextProgress.exerciseNumber.toString(),
172
+ stepNumber: nextProgress.stepNumber.toString(),
173
+ type: 'problem',
174
+ });
175
+ invariant(exerciseApp, 'No exercise app found');
176
+ await setPlayground(exerciseApp.fullPath);
177
+ return {
178
+ content: [
179
+ {
180
+ type: 'text',
181
+ text: `Playground set to ${exerciseApp.exerciseNumber}.${exerciseApp.stepNumber}.${exerciseApp.type}`,
182
+ },
183
+ ],
184
+ };
185
+ }
186
+ if (nextProgress.type === 'instructions' ||
187
+ nextProgress.type === 'finished') {
188
+ 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.`);
189
+ }
190
+ if (nextProgress.type === 'workshop-instructions' ||
191
+ nextProgress.type === 'workshop-finished') {
192
+ 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.`);
193
+ }
194
+ 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.`);
195
+ }
196
+ }
197
+ const apps = await getApps();
198
+ const exerciseStepApps = apps.filter(isExerciseStepApp);
199
+ const playgroundAppName = await getPlaygroundAppName();
200
+ const currentExerciseStepAppIndex = exerciseStepApps.findIndex((a) => a.name === playgroundAppName);
201
+ let desiredApp;
202
+ // if nothing was provided, set to the next step problem app
203
+ const noArgumentsProvided = !exerciseNumber && !stepNumber && !type;
204
+ if (noArgumentsProvided) {
205
+ desiredApp = exerciseStepApps
206
+ .slice(currentExerciseStepAppIndex + 1)
207
+ .find(isProblemApp);
208
+ invariant(desiredApp, 'No next problem app found to set playground to');
209
+ }
210
+ else {
211
+ const currentExerciseStepApp = exerciseStepApps[currentExerciseStepAppIndex];
212
+ // otherwise, default to the current exercise step app for arguments
213
+ exerciseNumber ??= currentExerciseStepApp?.exerciseNumber;
214
+ stepNumber ??= currentExerciseStepApp?.stepNumber;
215
+ type ??= currentExerciseStepApp?.type;
216
+ desiredApp = exerciseStepApps.find((a) => a.exerciseNumber === exerciseNumber &&
217
+ a.stepNumber === stepNumber &&
218
+ a.type === type);
219
+ }
220
+ invariant(desiredApp, `No app found for values derived by the arguments: ${exerciseNumber}.${stepNumber}.${type}`);
221
+ await setPlayground(desiredApp.fullPath);
222
+ const exerciseContext = await exerciseContextResource.getResource({
223
+ workshopDirectory,
224
+ exerciseNumber: desiredApp.exerciseNumber,
225
+ });
226
+ return {
227
+ content: [
228
+ {
229
+ type: 'text',
230
+ text: `Playground set to ${desiredApp.name}.`,
231
+ },
232
+ getEmbeddedResourceContent(exerciseContext),
233
+ ],
234
+ };
235
+ });
236
+ server.tool('update_progress', `
237
+ Intended to help you mark an Epic lesson as complete or incomplete.
238
+
239
+ This will mark the Epic lesson as complete or incomplete and update the user's progress (get updated progress with the \`get_user_progress\` tool, the \`get_exercise_context\` tool, or the \`get_workshop_context\` tool).
240
+ `.trim(), {
241
+ workshopDirectory: workshopDirectoryInputSchema,
242
+ epicLessonSlug: z
243
+ .string()
244
+ .describe('The slug of the Epic lesson to mark as complete (can be retrieved from the `get_exercise_context` tool or the `get_workshop_context` tool)'),
245
+ complete: z
246
+ .boolean()
247
+ .optional()
248
+ .default(true)
249
+ .describe('Whether to mark the lesson as complete or incomplete (defaults to true)'),
250
+ }, async ({ workshopDirectory, epicLessonSlug, complete }) => {
251
+ await handleWorkshopDirectory(workshopDirectory);
252
+ await updateProgress({ lessonSlug: epicLessonSlug, complete });
253
+ return {
254
+ content: [
255
+ {
256
+ type: 'text',
257
+ text: `Lesson with slug ${epicLessonSlug} marked as ${complete ? 'complete' : 'incomplete'}`,
258
+ },
259
+ ],
260
+ };
261
+ });
262
+ // TODO: add a tool to run the dev/test script for the given app
263
+ }
264
+ // These are tools that retrieve resources. Not all resources should be
265
+ // accessible via tools, but allowing the LLM to access them on demand is useful
266
+ // for some situations.
267
+ export function initResourceTools(server) {
268
+ server.tool('get_workshop_context', `
269
+ Indended to help you get wholistic context of the topics covered in this
270
+ workshop. This doesn't go into as much detail per exercise as the
271
+ \`get_exercise_context\` tool, but it is a good starting point to orient
272
+ yourself on the workshop as a whole.
273
+ `.trim(), workshopContextResource.inputSchema, async ({ workshopDirectory }) => {
274
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
275
+ const resource = await workshopContextResource.getResource({
276
+ workshopDirectory,
277
+ });
278
+ return {
279
+ content: [getEmbeddedResourceContent(resource)],
280
+ };
281
+ });
282
+ server.tool('get_exercise_context', `
283
+ Intended to help a student understand what they need to do for the current
284
+ exercise step.
285
+
286
+ This returns the instructions MDX content for the current exercise and each
287
+ exercise step. If the user is has the paid version of the workshop, it will also
288
+ include the transcript from each of the videos as well.
289
+
290
+ The output for this will rarely change, so it's unnecessary to call this tool
291
+ more than once.
292
+
293
+ \`get_exercise_context\` is often best when used with the
294
+ \`get_exercise_step_progress_diff\` tool to help a student understand what
295
+ work they still need to do and answer any questions about the exercise.
296
+ `.trim(), exerciseContextResource.inputSchema, async ({ workshopDirectory, exerciseNumber }) => {
297
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
298
+ const resource = await exerciseContextResource.getResource({
299
+ workshopDirectory,
300
+ exerciseNumber,
301
+ });
302
+ return {
303
+ content: [getEmbeddedResourceContent(resource)],
304
+ };
305
+ });
306
+ server.tool('get_diff_between_apps', `
307
+ Intended to give context about the changes between two apps.
308
+
309
+ The output is a git diff of the playground directory as BASE (their work in
310
+ progress) against the solution directory as HEAD (the final state they're trying
311
+ to achieve).
312
+
313
+ The output is formatted as a git diff.
314
+
315
+ App IDs are formatted as \`{exerciseNumber}.{stepNumber}.{type}\`.
316
+
317
+ If the user asks for the diff for 2.3, then use 02.03.problem for app1 and 02.03.solution for app2.
318
+ `, diffBetweenAppsResource.inputSchema, async ({ workshopDirectory, app1, app2 }) => {
319
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
320
+ const resource = await diffBetweenAppsResource.getResource({
321
+ workshopDirectory,
322
+ app1,
323
+ app2,
324
+ });
325
+ return {
326
+ content: [getEmbeddedResourceContent(resource)],
327
+ };
328
+ });
329
+ server.tool('get_exercise_step_progress_diff', `
330
+ Intended to help a student understand what work they still have to complete.
331
+
332
+ This is not a typical diff. It's a diff of the user's work in progress against
333
+ the solution.
334
+
335
+ - Lines starting with \`-\` show code that needs to be removed from the user's solution
336
+ - Lines starting with \`+\` show code that needs to be added to the user's solution
337
+ - If there are differences, the user's work is incomplete
338
+
339
+ Only tell the user they have more work to do if the diff output affects the
340
+ required behavior, API, or user experience. If the differences are only
341
+ stylistic or organizational, explain that things look different, but they are
342
+ still valid and ready to be tested.
343
+
344
+ If there's a diff with significant changes, you should explain what the changes
345
+ are and their significance. Be brief. Let them tell you whether they need you to
346
+ elaborate.
347
+
348
+ The output for this changes over time so it's useful to call multiple times.
349
+
350
+ For additional context, you can use the \`get_exercise_instructions\` tool
351
+ to get the instructions for the current exercise step to help explain the
352
+ significance of changes.
353
+ `.trim(), exerciseStepProgressDiffResource.inputSchema, async ({ workshopDirectory }) => {
354
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
355
+ const resource = await exerciseStepProgressDiffResource.getResource({
356
+ workshopDirectory,
357
+ });
358
+ return {
359
+ content: [getEmbeddedResourceContent(resource)],
360
+ };
361
+ });
362
+ server.tool('get_user_info', `
363
+ Intended to help you get information about the current user.
364
+
365
+ This includes the user's name, email, etc. It's mostly useful to determine
366
+ whether the user is logged in and know who they are.
367
+
368
+ If the user is not logged in, tell them to log in by running the \`login\` tool.
369
+ `.trim(), userInfoResource.inputSchema, async ({ workshopDirectory }) => {
370
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
371
+ const resource = await userInfoResource.getResource({ workshopDirectory });
372
+ return {
373
+ content: [getEmbeddedResourceContent(resource)],
374
+ };
375
+ });
376
+ server.tool('get_user_access', `
377
+ Will tell you whether the user has access to the paid features of the workshop.
378
+
379
+ Paid features include:
380
+ - Transcripts
381
+ - Progress tracking
382
+ - Access to videos
383
+ - Access to the discord chat
384
+ - Test tab support
385
+ - Diff tab support
386
+
387
+ Encourage the user to upgrade if they need access to the paid features.
388
+ `.trim(), userAccessResource.inputSchema, async ({ workshopDirectory }) => {
389
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
390
+ const resource = await userAccessResource.getResource({
391
+ workshopDirectory,
392
+ });
393
+ return {
394
+ content: [getEmbeddedResourceContent(resource)],
395
+ };
396
+ });
397
+ server.tool('get_user_progress', `
398
+ Intended to help you get the progress of the current user. Can often be helpful
399
+ to know what the next step that needs to be completed is. Make sure to provide
400
+ the user with the URL of relevant incomplete lessons so they can watch them and
401
+ then mark them as complete.
402
+ `.trim(), userProgressResource.inputSchema, async ({ workshopDirectory }) => {
403
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
404
+ const resource = await userProgressResource.getResource({
405
+ workshopDirectory,
406
+ });
407
+ return {
408
+ content: [getEmbeddedResourceContent(resource)],
409
+ };
410
+ });
411
+ }
412
+ // Sometimes the user will ask the LLM to select a prompt to use so they don't have to.
413
+ export function initPromptTools(server) {
414
+ server.tool('get_quiz_instructions', `
415
+ If the user asks you to quiz them on a topic from the workshop, use this tool to
416
+ retrieve the instructions for how to do so.
417
+
418
+ - If the user asks for a specific exercise, supply that exercise number.
419
+ - If they ask for a specific exericse, supply that exercise number.
420
+ - If they ask for a topic and you don't know which exercise that topic is in, use \`get_workshop_context\` to get the list of exercises and their topics and then supply the appropriate exercise number.
421
+ `.trim(), quizMeInputSchema, async ({ workshopDirectory, exerciseNumber }) => {
422
+ workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
423
+ const result = await quizMe({ workshopDirectory, exerciseNumber });
424
+ return {
425
+ // QUESTION: will a prompt ever return messages that have role: 'assistant'?
426
+ // if so, this may be a little confusing for the LLM, but I can't think of a
427
+ // good use case for that so 🤷‍♂️
428
+ content: result.messages.map((m) => {
429
+ if (m.content.type === 'resource') {
430
+ return getEmbeddedResourceContent(m.content.resource);
431
+ }
432
+ return m.content;
433
+ }),
434
+ };
435
+ });
436
+ }
437
+ function getEmbeddedResourceContent(resource) {
438
+ if (clientSupportsEmbeddedResources) {
439
+ return {
440
+ type: 'resource',
441
+ resource,
442
+ };
443
+ }
444
+ else if (typeof resource.text === 'string') {
445
+ return {
446
+ type: 'text',
447
+ text: resource.text,
448
+ };
449
+ }
450
+ else {
451
+ throw new Error(`Unknown resource type: ${resource.type} for ${resource.uri}`);
452
+ }
453
+ }
454
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/tools.ts"],"names":[],"mappings":"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","sourcesContent":["import { invariant } from '@epic-web/invariant'\nimport {\n\tgetApps,\n\tgetExerciseApp,\n\tgetPlaygroundAppName,\n\tisExerciseStepApp,\n\tisProblemApp,\n\tsetPlayground,\n\ttype ExerciseStepApp,\n} from '@epic-web/workshop-utils/apps.server'\nimport { deleteCache } from '@epic-web/workshop-utils/cache.server'\nimport { getWorkshopConfig } from '@epic-web/workshop-utils/config.server'\nimport {\n\tgetAuthInfo,\n\tlogout,\n\tsetAuthInfo,\n} from '@epic-web/workshop-utils/db.server'\nimport {\n\tgetProgress,\n\tgetUserInfo,\n\tupdateProgress,\n} from '@epic-web/workshop-utils/epic-api.server'\nimport { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { type ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'\nimport * as client from 'openid-client'\nimport { z } from 'zod'\nimport { quizMe, quizMeInputSchema } from './prompts.js'\nimport {\n\tdiffBetweenAppsResource,\n\texerciseContextResource,\n\texerciseStepProgressDiffResource,\n\tuserAccessResource,\n\tuserInfoResource,\n\tuserProgressResource,\n\tworkshopContextResource,\n} from './resources.js'\nimport {\n\thandleWorkshopDirectory,\n\tworkshopDirectoryInputSchema,\n} from './utils.js'\n\n// not enough support for this yet\nconst clientSupportsEmbeddedResources = false\n\nexport function initTools(server: McpServer) {\n\tserver.tool(\n\t\t'login',\n\t\t`Allow the user to login (or sign up) to the workshop. First`.trim(),\n\t\t{\n\t\t\tworkshopDirectory: workshopDirectoryInputSchema,\n\t\t},\n\t\tasync ({ workshopDirectory }) => {\n\t\t\tawait handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst {\n\t\t\t\tproduct: { host },\n\t\t\t} = getWorkshopConfig()\n\t\t\tconst ISSUER = `https://${host}/oauth`\n\t\t\tconst config = await client.discovery(new URL(ISSUER), 'EPICSHOP_APP')\n\t\t\tconst deviceResponse = await client.initiateDeviceAuthorization(\n\t\t\t\tconfig,\n\t\t\t\t{},\n\t\t\t)\n\n\t\t\tvoid handleAuthFlow()\n\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: 'text',\n\t\t\t\t\t\ttext: `Please go to ${deviceResponse.verification_uri_complete}. Verify the code on the page is \"${deviceResponse.user_code}\" to login.`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}\n\n\t\t\tasync function handleAuthFlow() {\n\t\t\t\tconst UserInfoSchema = z.object({\n\t\t\t\t\tid: z.string(),\n\t\t\t\t\temail: z.string(),\n\t\t\t\t\tname: z.string().nullable().optional(),\n\t\t\t\t})\n\n\t\t\t\tconst timeout = setTimeout(() => {\n\t\t\t\t\tvoid server.server.notification({\n\t\t\t\t\t\tmethod: 'notification',\n\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\tmessage: 'Device authorization timed out',\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}, deviceResponse.expires_in * 1000)\n\n\t\t\t\ttry {\n\t\t\t\t\tconst tokenSet = await client.pollDeviceAuthorizationGrant(\n\t\t\t\t\t\tconfig,\n\t\t\t\t\t\tdeviceResponse,\n\t\t\t\t\t)\n\t\t\t\t\tclearTimeout(timeout)\n\n\t\t\t\t\tif (!tokenSet) {\n\t\t\t\t\t\tawait server.server.notification({\n\t\t\t\t\t\t\tmethod: 'notification',\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tmessage: 'No token set',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tconst protectedResourceResponse = await client.fetchProtectedResource(\n\t\t\t\t\t\tconfig,\n\t\t\t\t\t\ttokenSet.access_token,\n\t\t\t\t\t\tnew URL(`${ISSUER}/userinfo`),\n\t\t\t\t\t\t'GET',\n\t\t\t\t\t)\n\t\t\t\t\tconst userinfoRaw = await protectedResourceResponse.json()\n\t\t\t\t\tconst userinfoResult = UserInfoSchema.safeParse(userinfoRaw)\n\t\t\t\t\tif (!userinfoResult.success) {\n\t\t\t\t\t\tawait server.server.notification({\n\t\t\t\t\t\t\tmethod: 'notification',\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tmessage: `Failed to parse user info: ${userinfoResult.error.message}`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst userinfo = userinfoResult.data\n\n\t\t\t\t\tawait setAuthInfo({\n\t\t\t\t\t\tid: userinfo.id,\n\t\t\t\t\t\ttokenSet,\n\t\t\t\t\t\temail: userinfo.email,\n\t\t\t\t\t\tname: userinfo.name,\n\t\t\t\t\t})\n\n\t\t\t\t\tawait getUserInfo({ forceFresh: true })\n\n\t\t\t\t\tawait server.server.notification({\n\t\t\t\t\t\tmethod: 'notification',\n\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\tmessage: 'Authentication successful',\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t} catch (error) {\n\t\t\t\t\tclearTimeout(timeout)\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'logout',\n\t\t`Allow the user to logout of the workshop (based on the workshop's host) and delete cache data.`,\n\t\t{\n\t\t\tworkshopDirectory: workshopDirectoryInputSchema,\n\t\t},\n\t\tasync ({ workshopDirectory }) => {\n\t\t\tawait handleWorkshopDirectory(workshopDirectory)\n\t\t\tawait logout()\n\t\t\tawait deleteCache()\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: 'text', text: 'Logged out' }],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'set_playground',\n\t\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. If logged in and there is an incomplete exercise step, set to next incomplete exercise step based on the user's progress - Most common\n\t- [No arguments]\nB. If not logged in or all exercises are complete, set to next exercise step from current (or first if there is none)\n\t- [No arguments]\nC. Set to a specific exercise step\n\t- exerciseNumber: 1\n\t- stepNumber: 1\n\t- type: 'solution'\nD. Set to the solution of the current exercise step\n\t- type: 'solution'\nE. Set to the second step problem of the current exercise\n\t- stepNumber: 2\nF. 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\t{\n\t\t\tworkshopDirectory: workshopDirectoryInputSchema,\n\t\t\texerciseNumber: z.coerce\n\t\t\t\t.number()\n\t\t\t\t.optional()\n\t\t\t\t.describe('The exercise number to set the playground to'),\n\t\t\tstepNumber: z.coerce\n\t\t\t\t.number()\n\t\t\t\t.optional()\n\t\t\t\t.describe('The step number to set the playground to'),\n\t\t\ttype: z\n\t\t\t\t.enum(['problem', 'solution'])\n\t\t\t\t.optional()\n\t\t\t\t.describe('The type of app to set the playground to'),\n\t\t},\n\t\tasync ({ workshopDirectory, exerciseNumber, stepNumber, type }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst authInfo = await getAuthInfo()\n\n\t\t\tif (authInfo) {\n\t\t\t\tconst progress = await getProgress()\n\t\t\t\tconst scoreProgress = (a: (typeof progress)[number]) => {\n\t\t\t\t\tif (a.type === 'workshop-instructions') return 0\n\t\t\t\t\tif (a.type === 'workshop-finished') return 10000\n\t\t\t\t\tif (a.type === 'instructions') return a.exerciseNumber * 100\n\t\t\t\t\tif (a.type === 'step') return a.exerciseNumber * 100 + a.stepNumber\n\t\t\t\t\tif (a.type === 'finished') return a.exerciseNumber * 100 + 100\n\n\t\t\t\t\tif (a.type === 'unknown') return 100000\n\t\t\t\t\treturn -1\n\t\t\t\t}\n\t\t\t\tconst sortedProgress = progress.sort((a, b) => {\n\t\t\t\t\treturn scoreProgress(a) - scoreProgress(b)\n\t\t\t\t})\n\t\t\t\tconst nextProgress = sortedProgress.find((p) => !p.epicCompletedAt)\n\t\t\t\tif (nextProgress) {\n\t\t\t\t\tif (nextProgress.type === 'step') {\n\t\t\t\t\t\tconst exerciseApp = await getExerciseApp({\n\t\t\t\t\t\t\texerciseNumber: nextProgress.exerciseNumber.toString(),\n\t\t\t\t\t\t\tstepNumber: nextProgress.stepNumber.toString(),\n\t\t\t\t\t\t\ttype: 'problem',\n\t\t\t\t\t\t})\n\t\t\t\t\t\tinvariant(exerciseApp, 'No exercise app found')\n\t\t\t\t\t\tawait setPlayground(exerciseApp.fullPath)\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: 'text',\n\t\t\t\t\t\t\t\t\ttext: `Playground set to ${exerciseApp.exerciseNumber}.${exerciseApp.stepNumber}.${exerciseApp.type}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (\n\t\t\t\t\t\tnextProgress.type === 'instructions' ||\n\t\t\t\t\t\tnextProgress.type === 'finished'\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`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.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tif (\n\t\t\t\t\t\tnextProgress.type === 'workshop-instructions' ||\n\t\t\t\t\t\tnextProgress.type === 'workshop-finished'\n\t\t\t\t\t) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`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.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`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.`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\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\tconst exerciseContext = await exerciseContextResource.getResource({\n\t\t\t\tworkshopDirectory,\n\t\t\t\texerciseNumber: desiredApp.exerciseNumber,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: 'text',\n\t\t\t\t\t\ttext: `Playground set to ${desiredApp.name}.`,\n\t\t\t\t\t},\n\t\t\t\t\tgetEmbeddedResourceContent(exerciseContext),\n\t\t\t\t],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'update_progress',\n\t\t`\nIntended to help you mark an Epic lesson as complete or incomplete.\n\nThis will mark the Epic lesson as complete or incomplete and update the user's progress (get updated progress with the \\`get_user_progress\\` tool, the \\`get_exercise_context\\` tool, or the \\`get_workshop_context\\` tool).\n\t\t`.trim(),\n\t\t{\n\t\t\tworkshopDirectory: workshopDirectoryInputSchema,\n\t\t\tepicLessonSlug: z\n\t\t\t\t.string()\n\t\t\t\t.describe(\n\t\t\t\t\t'The slug of the Epic lesson to mark as complete (can be retrieved from the `get_exercise_context` tool or the `get_workshop_context` tool)',\n\t\t\t\t),\n\t\t\tcomplete: z\n\t\t\t\t.boolean()\n\t\t\t\t.optional()\n\t\t\t\t.default(true)\n\t\t\t\t.describe(\n\t\t\t\t\t'Whether to mark the lesson as complete or incomplete (defaults to true)',\n\t\t\t\t),\n\t\t},\n\t\tasync ({ workshopDirectory, epicLessonSlug, complete }) => {\n\t\t\tawait handleWorkshopDirectory(workshopDirectory)\n\t\t\tawait updateProgress({ lessonSlug: epicLessonSlug, complete })\n\t\t\treturn {\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: 'text',\n\t\t\t\t\t\ttext: `Lesson with slug ${epicLessonSlug} marked as ${complete ? 'complete' : 'incomplete'}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t}\n\t\t},\n\t)\n\n\t// TODO: add a tool to run the dev/test script for the given app\n}\n\n// These are tools that retrieve resources. Not all resources should be\n// accessible via tools, but allowing the LLM to access them on demand is useful\n// for some situations.\nexport function initResourceTools(server: McpServer) {\n\tserver.tool(\n\t\t'get_workshop_context',\n\t\t`\nIndended to help you get wholistic context of the topics covered in this\nworkshop. This doesn't go into as much detail per exercise as the\n\\`get_exercise_context\\` tool, but it is a good starting point to orient\nyourself on the workshop as a whole.\n\t\t`.trim(),\n\t\tworkshopContextResource.inputSchema,\n\t\tasync ({ workshopDirectory }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst resource = await workshopContextResource.getResource({\n\t\t\t\tworkshopDirectory,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tcontent: [getEmbeddedResourceContent(resource)],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'get_exercise_context',\n\t\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\t`.trim(),\n\t\texerciseContextResource.inputSchema,\n\t\tasync ({ workshopDirectory, exerciseNumber }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst resource = await exerciseContextResource.getResource({\n\t\t\t\tworkshopDirectory,\n\t\t\t\texerciseNumber,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tcontent: [getEmbeddedResourceContent(resource)],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'get_diff_between_apps',\n\t\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\t`,\n\t\tdiffBetweenAppsResource.inputSchema,\n\t\tasync ({ workshopDirectory, app1, app2 }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst resource = await diffBetweenAppsResource.getResource({\n\t\t\t\tworkshopDirectory,\n\t\t\t\tapp1,\n\t\t\t\tapp2,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tcontent: [getEmbeddedResourceContent(resource)],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'get_exercise_step_progress_diff',\n\t\t`\nIntended to help a student understand what work they still have to complete.\n\nThis is not a typical diff. It's a diff of the user's work in progress against\nthe solution.\n\n- Lines starting with \\`-\\` show code that needs to be removed from the user's solution\n- Lines starting with \\`+\\` show code that needs to be added to the user's solution\n- If there are differences, the user's work is incomplete\n\nOnly tell the user they have more work to do if the diff output affects the\nrequired behavior, API, or user experience. If the differences are only\nstylistic or organizational, explain that things look different, but they are\nstill valid and ready to be tested.\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\t`.trim(),\n\t\texerciseStepProgressDiffResource.inputSchema,\n\t\tasync ({ workshopDirectory }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst resource = await exerciseStepProgressDiffResource.getResource({\n\t\t\t\tworkshopDirectory,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tcontent: [getEmbeddedResourceContent(resource)],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'get_user_info',\n\t\t`\nIntended to help you get information about the current user.\n\nThis includes the user's name, email, etc. It's mostly useful to determine\nwhether the user is logged in and know who they are.\n\nIf the user is not logged in, tell them to log in by running the \\`login\\` tool.\n\t\t`.trim(),\n\t\tuserInfoResource.inputSchema,\n\t\tasync ({ workshopDirectory }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst resource = await userInfoResource.getResource({ workshopDirectory })\n\t\t\treturn {\n\t\t\t\tcontent: [getEmbeddedResourceContent(resource)],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'get_user_access',\n\t\t`\nWill tell you whether the user has access to the paid features of the workshop.\n\nPaid features include:\n- Transcripts\n- Progress tracking\n- Access to videos\n- Access to the discord chat\n- Test tab support\n- Diff tab support\n\nEncourage the user to upgrade if they need access to the paid features.\n\t\t`.trim(),\n\t\tuserAccessResource.inputSchema,\n\t\tasync ({ workshopDirectory }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst resource = await userAccessResource.getResource({\n\t\t\t\tworkshopDirectory,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tcontent: [getEmbeddedResourceContent(resource)],\n\t\t\t}\n\t\t},\n\t)\n\n\tserver.tool(\n\t\t'get_user_progress',\n\t\t`\nIntended to help you get the progress of the current user. Can often be helpful\nto know what the next step that needs to be completed is. Make sure to provide\nthe user with the URL of relevant incomplete lessons so they can watch them and\nthen mark them as complete.\n\t\t`.trim(),\n\t\tuserProgressResource.inputSchema,\n\t\tasync ({ workshopDirectory }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst resource = await userProgressResource.getResource({\n\t\t\t\tworkshopDirectory,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tcontent: [getEmbeddedResourceContent(resource)],\n\t\t\t}\n\t\t},\n\t)\n}\n\n// Sometimes the user will ask the LLM to select a prompt to use so they don't have to.\nexport function initPromptTools(server: McpServer) {\n\tserver.tool(\n\t\t'get_quiz_instructions',\n\t\t`\nIf the user asks you to quiz them on a topic from the workshop, use this tool to\nretrieve the instructions for how to do so.\n\n- If the user asks for a specific exercise, supply that exercise number.\n- If they ask for a specific exericse, supply that exercise number.\n- If they ask for a topic and you don't know which exercise that topic is in, use \\`get_workshop_context\\` to get the list of exercises and their topics and then supply the appropriate exercise number.\n\t\t`.trim(),\n\t\tquizMeInputSchema,\n\t\tasync ({ workshopDirectory, exerciseNumber }) => {\n\t\t\tworkshopDirectory = await handleWorkshopDirectory(workshopDirectory)\n\t\t\tconst result = await quizMe({ workshopDirectory, exerciseNumber })\n\t\t\treturn {\n\t\t\t\t// QUESTION: will a prompt ever return messages that have role: 'assistant'?\n\t\t\t\t// if so, this may be a little confusing for the LLM, but I can't think of a\n\t\t\t\t// good use case for that so 🤷‍♂️\n\t\t\t\tcontent: result.messages.map((m) => {\n\t\t\t\t\tif (m.content.type === 'resource') {\n\t\t\t\t\t\treturn getEmbeddedResourceContent(m.content.resource)\n\t\t\t\t\t}\n\t\t\t\t\treturn m.content\n\t\t\t\t}),\n\t\t\t}\n\t\t},\n\t)\n}\n\nfunction getEmbeddedResourceContent(\n\tresource: ReadResourceResult['contents'][number],\n) {\n\tif (clientSupportsEmbeddedResources) {\n\t\treturn {\n\t\t\ttype: 'resource' as const,\n\t\t\tresource,\n\t\t}\n\t} else if (typeof resource.text === 'string') {\n\t\treturn {\n\t\t\ttype: 'text' as const,\n\t\t\ttext: resource.text,\n\t\t}\n\t} else {\n\t\tthrow new Error(\n\t\t\t`Unknown resource type: ${resource.type} for ${resource.uri}`,\n\t\t)\n\t}\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { z } from 'zod';
2
+ export declare const workshopDirectoryInputSchema: z.ZodString;
3
+ export declare function handleWorkshopDirectory(workshopDirectory: string): Promise<string>;
4
+ export declare function safeReadFile(filePath: string): Promise<string | null>;
5
+ export type InputSchemaType<T extends {
6
+ [K: string]: z.ZodType;
7
+ }> = {
8
+ [K in keyof T]: z.infer<T[K]>;
9
+ };
10
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,4BAA4B,aAIvC,CAAA;AAyBF,wBAAsB,uBAAuB,CAAC,iBAAiB,EAAE,MAAM,mBAuBtE;AAED,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,0BAMlD;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,CAAA;CAAE,IAAI;KAClE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7B,CAAA"}
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { init as initApps } from '@epic-web/workshop-utils/apps.server';
4
+ import { z } from 'zod';
5
+ export const workshopDirectoryInputSchema = z
6
+ .string()
7
+ .describe('The workshop directory (the root directory of the workshop repo). This should be an absolute path.');
8
+ async function isWorkshopDirectory(workshopDirectory) {
9
+ console.error('isWorkshopDirectory', workshopDirectory);
10
+ const packageJson = await safeReadFile(path.join(workshopDirectory, 'package.json'));
11
+ if (!packageJson)
12
+ return false;
13
+ let pkgJson;
14
+ try {
15
+ pkgJson = JSON.parse(packageJson);
16
+ }
17
+ catch (error) {
18
+ if (error instanceof SyntaxError) {
19
+ throw new Error(`Syntax error in package.json in "${workshopDirectory}": ${error.message}`);
20
+ }
21
+ throw error;
22
+ }
23
+ console.error('isWorkshopDirectory', Boolean(pkgJson.epicshop));
24
+ return Boolean(pkgJson.epicshop);
25
+ }
26
+ export async function handleWorkshopDirectory(workshopDirectory) {
27
+ workshopDirectory = workshopDirectory.trim();
28
+ if (!workshopDirectory)
29
+ throw new Error('The workshop directory is required');
30
+ if (!path.isAbsolute(workshopDirectory)) {
31
+ throw new Error('The workshop directory must be an absolute path');
32
+ }
33
+ if (workshopDirectory.endsWith(`${path.sep}playground`)) {
34
+ workshopDirectory = path.join(workshopDirectory, '..');
35
+ }
36
+ while (true) {
37
+ if (await isWorkshopDirectory(workshopDirectory))
38
+ break;
39
+ if (workshopDirectory === path.dirname(workshopDirectory)) {
40
+ throw new Error(`No workshop directory found in "${workshopDirectory}"`);
41
+ }
42
+ workshopDirectory = path.dirname(workshopDirectory);
43
+ }
44
+ await initApps(workshopDirectory);
45
+ return workshopDirectory;
46
+ }
47
+ export async function safeReadFile(filePath) {
48
+ try {
49
+ return await fs.readFile(filePath, 'utf-8');
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,sCAAsC,CAAA;AACvE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC;KAC3C,MAAM,EAAE;KACR,QAAQ,CACR,oGAAoG,CACpG,CAAA;AAEF,KAAK,UAAU,mBAAmB,CAAC,iBAAyB;IAC3D,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,iBAAiB,CAAC,CAAA;IACvD,MAAM,WAAW,GAAG,MAAM,YAAY,CACrC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAC5C,CAAA;IACD,IAAI,CAAC,WAAW;QAAE,OAAO,KAAK,CAAA;IAE9B,IAAI,OAAY,CAAA;IAChB,IAAI,CAAC;QACJ,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,KAAK,YAAY,WAAW,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACd,oCAAoC,iBAAiB,MAAM,KAAK,CAAC,OAAO,EAAE,CAC1E,CAAA;QACF,CAAC;QACD,MAAM,KAAK,CAAA;IACZ,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IAE/D,OAAO,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;AACjC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,iBAAyB;IACtE,iBAAiB,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAA;IAE5C,IAAI,CAAC,iBAAiB;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IAE7E,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAA;IACnE,CAAC;IAED,IAAI,iBAAiB,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,GAAG,YAAY,CAAC,EAAE,CAAC;QACzD,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAA;IACvD,CAAC;IAED,OAAO,IAAI,EAAE,CAAC;QACb,IAAI,MAAM,mBAAmB,CAAC,iBAAiB,CAAC;YAAE,MAAK;QACvD,IAAI,iBAAiB,KAAK,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,CAAC;YAC3D,MAAM,IAAI,KAAK,CAAC,mCAAmC,iBAAiB,GAAG,CAAC,CAAA;QACzE,CAAC;QACD,iBAAiB,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACpD,CAAC;IAED,MAAM,QAAQ,CAAC,iBAAiB,CAAC,CAAA;IACjC,OAAO,iBAAiB,CAAA;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IAClD,IAAI,CAAC;QACJ,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC5C,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAA;IACZ,CAAC;AACF,CAAC","sourcesContent":["import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { init as initApps } from '@epic-web/workshop-utils/apps.server'\nimport { z } from 'zod'\n\nexport const workshopDirectoryInputSchema = z\n\t.string()\n\t.describe(\n\t\t'The workshop directory (the root directory of the workshop repo). This should be an absolute path.',\n\t)\n\nasync function isWorkshopDirectory(workshopDirectory: string) {\n\tconsole.error('isWorkshopDirectory', workshopDirectory)\n\tconst packageJson = await safeReadFile(\n\t\tpath.join(workshopDirectory, 'package.json'),\n\t)\n\tif (!packageJson) return false\n\n\tlet pkgJson: any\n\ttry {\n\t\tpkgJson = JSON.parse(packageJson)\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) {\n\t\t\tthrow new Error(\n\t\t\t\t`Syntax error in package.json in \"${workshopDirectory}\": ${error.message}`,\n\t\t\t)\n\t\t}\n\t\tthrow error\n\t}\n\tconsole.error('isWorkshopDirectory', Boolean(pkgJson.epicshop))\n\n\treturn Boolean(pkgJson.epicshop)\n}\n\nexport async function handleWorkshopDirectory(workshopDirectory: string) {\n\tworkshopDirectory = workshopDirectory.trim()\n\n\tif (!workshopDirectory) throw new Error('The workshop directory is required')\n\n\tif (!path.isAbsolute(workshopDirectory)) {\n\t\tthrow new Error('The workshop directory must be an absolute path')\n\t}\n\n\tif (workshopDirectory.endsWith(`${path.sep}playground`)) {\n\t\tworkshopDirectory = path.join(workshopDirectory, '..')\n\t}\n\n\twhile (true) {\n\t\tif (await isWorkshopDirectory(workshopDirectory)) break\n\t\tif (workshopDirectory === path.dirname(workshopDirectory)) {\n\t\t\tthrow new Error(`No workshop directory found in \"${workshopDirectory}\"`)\n\t\t}\n\t\tworkshopDirectory = path.dirname(workshopDirectory)\n\t}\n\n\tawait initApps(workshopDirectory)\n\treturn workshopDirectory\n}\n\nexport async 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\nexport type InputSchemaType<T extends { [K: string]: z.ZodType }> = {\n\t[K in keyof T]: z.infer<T[K]>\n}\n"]}
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name":"@epic-web/workshop-mcp","version":"0.0.0-semantically-released","publishConfig":{"access":"public"},"bin":"./dist/esm/index.js","type":"module","tshy":{"project":"./tsconfig.build.json","dialects":["esm"],"exports":{"./package.json":"./package.json",".":"./dist/esm/index.js"}},"exports":{"./package.json":"./package.json",".":"./dist/esm/index.js"},"files":["dist"],"scripts":{"dev":"tsx src/index.ts","typecheck":"tsc --noEmit","build":"tshy","inspect":"mcp-inspector","build:watch":"nx watch --projects=@epic-web/workshop-mcp -- nx run \\$NX_PROJECT_NAME:build"},"dependencies":{"@epic-web/invariant":"^1.0.0","@epic-web/workshop-utils":"^5.29.2","@modelcontextprotocol/sdk":"^1.14.0","@modelcontextprotocol/inspector":"^0.15.0","openid-client":"^6.6.2","zod":"^3.25.71"},"devDependencies":{"@types/node":"^24.0.10","tshy":"^3.0.2","tsx":"^4.20.3","typescript":"^5.8.3"},"repository":{"type":"git","url":"https://github.com/epicweb-dev/epicshop.git","directory":"packages/workshop-mcp"},"main":"./dist/esm/index.js","module":"./dist/esm/index.js"}