learnhouse-mcp-server 1.0.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/src/index.ts ADDED
@@ -0,0 +1,532 @@
1
+ /**
2
+ * LearnHouse MCP Server
3
+ *
4
+ * Model Context Protocol server for LearnHouse LMS.
5
+ * Provides tools for managing courses, chapters, activities, and users.
6
+ */
7
+
8
+ import { FastMCP } from "fastmcp";
9
+ import { z } from "zod";
10
+ import { LearnHouseClient } from "./client.js";
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+
14
+ // ============================================================
15
+ // Configuration
16
+ // ============================================================
17
+
18
+ const config = {
19
+ baseUrl: process.env.LEARNHOUSE_URL || "http://localhost:3000",
20
+ email: process.env.LEARNHOUSE_EMAIL || "",
21
+ password: process.env.LEARNHOUSE_PASSWORD || "",
22
+ orgId: parseInt(process.env.LEARNHOUSE_ORG_ID || "1", 10),
23
+ };
24
+
25
+ // Singleton client instance
26
+ let client: LearnHouseClient | null = null;
27
+
28
+ async function getClient(): Promise<LearnHouseClient> {
29
+ if (!client) {
30
+ client = new LearnHouseClient({
31
+ baseUrl: config.baseUrl,
32
+ orgId: config.orgId,
33
+ });
34
+
35
+ if (config.email && config.password) {
36
+ await client.login(config.email, config.password);
37
+ }
38
+ }
39
+ return client;
40
+ }
41
+
42
+ // ============================================================
43
+ // MCP Server
44
+ // ============================================================
45
+
46
+ const server = new FastMCP({
47
+ name: "LearnHouse MCP Server",
48
+ version: "1.0.0",
49
+ instructions: `
50
+ LearnHouse MCP Server provides tools to manage an LMS (Learning Management System).
51
+
52
+ Available capabilities:
53
+ - **Course Management**: List, create, update, delete courses
54
+ - **Chapter Management**: Organize chapters within courses
55
+ - **Activity Management**: Create and manage learning activities (documents, videos, PDFs)
56
+ - **Content Creation**: Set TipTap document content for activities
57
+ - **Progress Tracking**: Track user progress through courses
58
+ - **Search**: Search across courses and content
59
+
60
+ Organization ID: ${config.orgId}
61
+ API URL: ${config.baseUrl}
62
+ `.trim(),
63
+ });
64
+
65
+ server.addTool({
66
+ name: "ping",
67
+ description: "Ping the MCP server to verify it is running and identify version.",
68
+ parameters: z.object({}),
69
+ execute: async () => {
70
+ return "LearnHouse MCP Server is UP (Version 1.0.2 - Fixed PUT)";
71
+ },
72
+ });
73
+
74
+ // ============================================================
75
+ // Course Tools
76
+ // ============================================================
77
+
78
+ server.addTool({
79
+ name: "list_courses",
80
+ description: "List all courses in the organization",
81
+ parameters: z.object({
82
+ page: z.number().optional().default(1).describe("Page number"),
83
+ limit: z.number().optional().default(50).describe("Number of courses per page"),
84
+ }),
85
+ execute: async (args) => {
86
+ const api = await getClient();
87
+ const courses = await api.listCourses(args.page, args.limit);
88
+ return JSON.stringify(courses, null, 2);
89
+ },
90
+ });
91
+
92
+ server.addTool({
93
+ name: "get_course",
94
+ description: "Get detailed information about a specific course",
95
+ parameters: z.object({
96
+ course_uuid: z.string().describe("The UUID of the course"),
97
+ }),
98
+ execute: async (args) => {
99
+ const api = await getClient();
100
+ const course = await api.getCourse(args.course_uuid);
101
+ return JSON.stringify(course, null, 2);
102
+ },
103
+ });
104
+
105
+ server.addTool({
106
+ name: "create_course",
107
+ description: "Create a new course",
108
+ parameters: z.object({
109
+ name: z.string().describe("Course name"),
110
+ description: z.string().describe("Course description"),
111
+ public: z.boolean().optional().default(true).describe("Whether the course is public"),
112
+ }),
113
+ execute: async (args) => {
114
+ const api = await getClient();
115
+ const course = await api.createCourse({
116
+ name: args.name,
117
+ description: args.description,
118
+ public: args.public,
119
+ learnings: [],
120
+ });
121
+ return JSON.stringify(course, null, 2);
122
+ },
123
+ });
124
+
125
+ server.addTool({
126
+ name: "update_course",
127
+ description: "Update an existing course",
128
+ parameters: z.object({
129
+ course_uuid: z.string().describe("The UUID of the course to update"),
130
+ name: z.string().optional().describe("New course name"),
131
+ description: z.string().optional().describe("New course description"),
132
+ published: z.boolean().optional().describe("Publish/unpublish the course"),
133
+ thumbnail_image: z.string().optional().describe("URL of the thumbnail image"),
134
+ thumbnail_type: z.enum(["IMAGE", "VIDEO"]).optional().describe("Type of thumbnail"),
135
+ }),
136
+ execute: async (args) => {
137
+ const api = await getClient();
138
+ const update: Record<string, unknown> = {};
139
+ if (args.name) update.name = args.name;
140
+ if (args.description) update.description = args.description;
141
+ if (args.published !== undefined) update.published = args.published;
142
+ if (args.thumbnail_image) update.thumbnail_image = args.thumbnail_image;
143
+ if (args.thumbnail_type) update.thumbnail_type = args.thumbnail_type;
144
+
145
+ const course = await api.updateCourse(args.course_uuid, update);
146
+ return JSON.stringify(course, null, 2);
147
+ },
148
+ });
149
+
150
+ server.addTool({
151
+ name: "delete_course",
152
+ description: "Delete a course (use with caution!)",
153
+ parameters: z.object({
154
+ course_uuid: z.string().describe("The UUID of the course to delete"),
155
+ }),
156
+ execute: async (args) => {
157
+ const api = await getClient();
158
+ await api.deleteCourse(args.course_uuid);
159
+ return `Course ${args.course_uuid} deleted successfully`;
160
+ },
161
+ });
162
+
163
+ server.addTool({
164
+ name: "upload_course_thumbnail",
165
+ description: "Upload a thumbnail image for a course from a local file path",
166
+ parameters: z.object({
167
+ course_uuid: z.string().describe("The UUID of the course"),
168
+ file_path: z.string().describe("Local absolute path to the image file"),
169
+ type: z.enum(["IMAGE", "VIDEO"]).optional().default("IMAGE").describe("Thumbnail type"),
170
+ }),
171
+ execute: async (args) => {
172
+ const api = await getClient();
173
+
174
+ if (!fs.existsSync(args.file_path)) {
175
+ throw new Error(`File not found: ${args.file_path}`);
176
+ }
177
+
178
+ const buffer = fs.readFileSync(args.file_path);
179
+ const fileName = path.basename(args.file_path);
180
+
181
+ const course = await api.uploadCourseThumbnail(args.course_uuid, buffer, fileName, args.type);
182
+ return JSON.stringify(course, null, 2);
183
+ },
184
+ });
185
+
186
+ // ============================================================
187
+ // Chapter Tools
188
+ // ============================================================
189
+
190
+ server.addTool({
191
+ name: "list_chapters",
192
+ description: "List all chapters in a course",
193
+ parameters: z.object({
194
+ course_id: z.number().describe("The numeric ID of the course"),
195
+ }),
196
+ execute: async (args) => {
197
+ const api = await getClient();
198
+ const chapters = await api.listChapters(args.course_id);
199
+ return JSON.stringify(chapters, null, 2);
200
+ },
201
+ });
202
+
203
+ server.addTool({
204
+ name: "get_chapter",
205
+ description: "Get details of a specific chapter",
206
+ parameters: z.object({
207
+ chapter_id: z.number().describe("The ID of the chapter"),
208
+ }),
209
+ execute: async (args) => {
210
+ const api = await getClient();
211
+ const chapter = await api.getChapter(args.chapter_id);
212
+ return JSON.stringify(chapter, null, 2);
213
+ },
214
+ });
215
+
216
+ server.addTool({
217
+ name: "create_chapter",
218
+ description: "Create a new chapter in a course",
219
+ parameters: z.object({
220
+ course_id: z.number().describe("The numeric ID of the course"),
221
+ org_id: z.number().optional().default(config.orgId).describe("Organization ID"),
222
+ name: z.string().describe("Chapter name"),
223
+ description: z.string().optional().default("").describe("Chapter description"),
224
+ }),
225
+ execute: async (args) => {
226
+ const api = await getClient();
227
+ const chapter = await api.createChapter({
228
+ name: args.name,
229
+ description: args.description || "",
230
+ course_id: args.course_id,
231
+ org_id: args.org_id,
232
+ });
233
+ return JSON.stringify(chapter, null, 2);
234
+ },
235
+ });
236
+
237
+ server.addTool({
238
+ name: "update_chapter",
239
+ description: "Update an existing chapter",
240
+ parameters: z.object({
241
+ chapter_id: z.number().describe("The ID of the chapter to update"),
242
+ name: z.string().optional().describe("New chapter name"),
243
+ description: z.string().optional().describe("New chapter description"),
244
+ }),
245
+ execute: async (args) => {
246
+ const api = await getClient();
247
+ const update: Record<string, unknown> = {};
248
+ if (args.name) update.name = args.name;
249
+ if (args.description !== undefined) update.description = args.description;
250
+
251
+ const chapter = await api.updateChapter(args.chapter_id, update);
252
+ return JSON.stringify(chapter, null, 2);
253
+ },
254
+ });
255
+
256
+ // ============================================================
257
+ // Activity Tools
258
+ // ============================================================
259
+
260
+ server.addTool({
261
+ name: "list_activities",
262
+ description: "List all activities in a chapter",
263
+ parameters: z.object({
264
+ chapter_id: z.number().describe("The ID of the chapter"),
265
+ }),
266
+ execute: async (args) => {
267
+ const api = await getClient();
268
+ const activities = await api.listActivities(args.chapter_id);
269
+ return JSON.stringify(activities, null, 2);
270
+ },
271
+ });
272
+
273
+ server.addTool({
274
+ name: "get_activity",
275
+ description: "Get details of a specific activity",
276
+ parameters: z.object({
277
+ activity_uuid: z.string().describe("The UUID of the activity"),
278
+ }),
279
+ execute: async (args) => {
280
+ const api = await getClient();
281
+ const activity = await api.getActivity(args.activity_uuid);
282
+ return JSON.stringify(activity, null, 2);
283
+ },
284
+ });
285
+
286
+ server.addTool({
287
+ name: "create_activity",
288
+ description: "Create a new activity in a chapter",
289
+ parameters: z.object({
290
+ chapter_id: z.number().describe("The ID of the chapter"),
291
+ name: z.string().describe("Activity name"),
292
+ activity_type: z.enum(["TYPE_DYNAMIC", "TYPE_VIDEO", "TYPE_PDF"]).optional().default("TYPE_DYNAMIC").describe("Type of activity"),
293
+ activity_sub_type: z.enum(["SUBTYPE_DYNAMIC_PAGE", "SUBTYPE_VIDEO_YOUTUBE", "SUBTYPE_VIDEO_HOSTED", "SUBTYPE_PDF"]).optional().default("SUBTYPE_DYNAMIC_PAGE").describe("Subtype of activity"),
294
+ published: z.boolean().optional().default(false).describe("Whether the activity is published"),
295
+ }),
296
+ execute: async (args) => {
297
+ const api = await getClient();
298
+ const activity = await api.createActivity({
299
+ name: args.name,
300
+ chapter_id: args.chapter_id,
301
+ activity_type: args.activity_type as any,
302
+ activity_sub_type: args.activity_sub_type as any,
303
+ published: args.published,
304
+ });
305
+ return JSON.stringify(activity, null, 2);
306
+ },
307
+ });
308
+
309
+ server.addTool({
310
+ name: "update_activity",
311
+ description: "Update an existing activity",
312
+ parameters: z.object({
313
+ activity_uuid: z.string().describe("The UUID of the activity to update"),
314
+ name: z.string().optional().describe("New activity name"),
315
+ published: z.boolean().optional().describe("Publish/unpublish the activity"),
316
+ }),
317
+ execute: async (args) => {
318
+ const api = await getClient();
319
+ const update: Record<string, unknown> = {};
320
+ if (args.name) update.name = args.name;
321
+ if (args.published !== undefined) update.published = args.published;
322
+
323
+ const activity = await api.updateActivity(args.activity_uuid, update);
324
+ return JSON.stringify(activity, null, 2);
325
+ },
326
+ });
327
+
328
+ server.addTool({
329
+ name: "publish_activity",
330
+ description: "Publish an activity to make it visible",
331
+ parameters: z.object({
332
+ activity_uuid: z.string().describe("The UUID of the activity to publish"),
333
+ }),
334
+ execute: async (args) => {
335
+ const api = await getClient();
336
+ const activity = await api.updateActivity(args.activity_uuid, { published: true });
337
+ return JSON.stringify(activity, null, 2);
338
+ },
339
+ });
340
+
341
+ // ============================================================
342
+ // Content Tools
343
+ // ============================================================
344
+
345
+ server.addTool({
346
+ name: "set_document_content",
347
+ description: "Set the TipTap document content for a document activity. Content must be valid TipTap JSON.",
348
+ parameters: z.object({
349
+ activity_uuid: z.string().describe("The UUID of the activity"),
350
+ content: z.string().describe("TipTap JSON content as a string"),
351
+ }),
352
+ execute: async (args) => {
353
+ const api = await getClient();
354
+ let content: Record<string, unknown>;
355
+ try {
356
+ content = JSON.parse(args.content) as Record<string, unknown>;
357
+ } catch {
358
+ throw new Error("Invalid JSON content. Please provide valid TipTap JSON.");
359
+ }
360
+
361
+ await api.updateActivity(args.activity_uuid, { content });
362
+ return `Document content updated for activity ${args.activity_uuid}`;
363
+ },
364
+ });
365
+
366
+ server.addTool({
367
+ name: "set_video_content",
368
+ description: "Set video URL for a video activity",
369
+ parameters: z.object({
370
+ activity_uuid: z.string().describe("The UUID of the activity"),
371
+ video_url: z.string().describe("YouTube or hosted video URL"),
372
+ }),
373
+ execute: async (args) => {
374
+ const api = await getClient();
375
+ const activity = await api.updateActivity(args.activity_uuid, {
376
+ content: { video_url: args.video_url },
377
+ });
378
+ return `Video URL set for activity ${args.activity_uuid}: ${args.video_url}`;
379
+ },
380
+ });
381
+
382
+ // ============================================================
383
+ // User & Organization Tools
384
+ // ============================================================
385
+
386
+ server.addTool({
387
+ name: "get_current_user",
388
+ description: "Get information about the currently authenticated user",
389
+ parameters: z.object({}),
390
+ execute: async () => {
391
+ const api = await getClient();
392
+ const user = await api.getCurrentUser();
393
+ return JSON.stringify(user, null, 2);
394
+ },
395
+ });
396
+
397
+ server.addTool({
398
+ name: "get_organization",
399
+ description: "Get organization details",
400
+ parameters: z.object({
401
+ org_id: z.number().optional().default(config.orgId).describe("Organization ID"),
402
+ }),
403
+ execute: async (args) => {
404
+ const api = await getClient();
405
+ const org = await api.getOrganization(args.org_id);
406
+ return JSON.stringify(org, null, 2);
407
+ },
408
+ });
409
+
410
+ // ============================================================
411
+ // Collection Tools
412
+ // ============================================================
413
+
414
+ server.addTool({
415
+ name: "list_collections",
416
+ description: "List all collections in the organization",
417
+ parameters: z.object({
418
+ page: z.number().optional().default(1).describe("Page number"),
419
+ limit: z.number().optional().default(50).describe("Number of collections per page"),
420
+ }),
421
+ execute: async (args) => {
422
+ const api = await getClient();
423
+ const collections = await api.listCollections(args.page, args.limit);
424
+ return JSON.stringify(collections, null, 2);
425
+ },
426
+ });
427
+
428
+ server.addTool({
429
+ name: "get_collection",
430
+ description: "Get detailed information about a specific collection",
431
+ parameters: z.object({
432
+ collection_uuid: z.string().describe("The UUID of the collection"),
433
+ }),
434
+ execute: async (args) => {
435
+ const api = await getClient();
436
+ const collection = await api.getCollection(args.collection_uuid);
437
+ return JSON.stringify(collection, null, 2);
438
+ },
439
+ });
440
+
441
+ server.addTool({
442
+ name: "create_collection",
443
+ description: "Create a new collection",
444
+ parameters: z.object({
445
+ name: z.string().describe("Collection name"),
446
+ description: z.string().optional().describe("Collection description"),
447
+ public: z.boolean().optional().default(true).describe("Whether the collection is public"),
448
+ courses: z.array(z.number()).optional().describe("List of course IDs to include"),
449
+ }),
450
+ execute: async (args) => {
451
+ const api = await getClient();
452
+ const collection = await api.createCollection({
453
+ name: args.name,
454
+ description: args.description,
455
+ public: args.public,
456
+ courses: args.courses,
457
+ org_id: config.orgId,
458
+ });
459
+ return JSON.stringify(collection, null, 2);
460
+ },
461
+ });
462
+
463
+ server.addTool({
464
+ name: "delete_collection",
465
+ description: "Delete a collection",
466
+ parameters: z.object({
467
+ collection_uuid: z.string().describe("The UUID of the collection to delete"),
468
+ }),
469
+ execute: async (args) => {
470
+ const api = await getClient();
471
+ await api.deleteCollection(args.collection_uuid);
472
+ return `Collection ${args.collection_uuid} deleted successfully`;
473
+ },
474
+ });
475
+
476
+ // ============================================================
477
+ // Progress Tools
478
+ // ============================================================
479
+
480
+ server.addTool({
481
+ name: "get_course_progress",
482
+ description: "Get user's progress for a specific course",
483
+ parameters: z.object({
484
+ course_uuid: z.string().describe("The UUID of the course"),
485
+ }),
486
+ execute: async (args) => {
487
+ const api = await getClient();
488
+ const progress = await api.getCourseProgress(args.course_uuid);
489
+ return JSON.stringify(progress, null, 2);
490
+ },
491
+ });
492
+
493
+ server.addTool({
494
+ name: "mark_activity_complete",
495
+ description: "Mark an activity as completed for the current user",
496
+ parameters: z.object({
497
+ activity_uuid: z.string().describe("The UUID of the activity to mark complete"),
498
+ }),
499
+ execute: async (args) => {
500
+ const api = await getClient();
501
+ await api.markActivityComplete(args.activity_uuid);
502
+ return `Activity ${args.activity_uuid} marked as complete`;
503
+ },
504
+ });
505
+
506
+ // ============================================================
507
+ // Search Tools
508
+ // ============================================================
509
+
510
+ server.addTool({
511
+ name: "search",
512
+ description: "Search for courses and content in the organization",
513
+ parameters: z.object({
514
+ org_slug: z.string().optional().default("default").describe("Organization slug"),
515
+ query: z.string().describe("Search query"),
516
+ }),
517
+ execute: async (args) => {
518
+ const api = await getClient();
519
+ const results = await api.search(args.org_slug, args.query);
520
+ return JSON.stringify(results, null, 2);
521
+ },
522
+ });
523
+
524
+ // ============================================================
525
+ // Start Server
526
+ // ============================================================
527
+
528
+ server.start({
529
+ transportType: "stdio",
530
+ });
531
+
532
+ console.error("LearnHouse MCP Server started");