learnhouse-mcp-server 1.0.0 → 1.0.1

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 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { LearnHouseClient } from "./client.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ const config = {
5
+ baseUrl: process.env.LEARNHOUSE_URL || "http://localhost:3000",
6
+ email: process.env.LEARNHOUSE_EMAIL || "",
7
+ password: process.env.LEARNHOUSE_PASSWORD || "",
8
+ orgId: parseInt(process.env.LEARNHOUSE_ORG_ID || "1", 10),
9
+ };
10
+ async function main() {
11
+ const client = new LearnHouseClient({
12
+ baseUrl: config.baseUrl,
13
+ orgId: config.orgId,
14
+ });
15
+ console.log("Logging in...");
16
+ await client.login(config.email, config.password);
17
+ // 1. Upload Thumbnail to Mastering MCP
18
+ const courseUuid = "course_d6d76f09-c553-4f82-ab81-82a1c69fdd5f";
19
+ const imagePath = "\\\\192.168.2.108\\Users\\Laptop\\Desktop\\Projects\\Studies\\Winter_2026\\01_SIA 3000 - Projet integrateur\\AgentOne_Project\\_Handover\\Hero\\1_Hero_Backgrounds\\hero_grid_08_circuit_grid.png";
20
+ if (fs.existsSync(imagePath)) {
21
+ console.log(`Uploading thumbnail: ${imagePath}`);
22
+ const buffer = fs.readFileSync(imagePath);
23
+ const fileName = path.basename(imagePath);
24
+ const updatedCourse = await client.uploadCourseThumbnail(courseUuid, buffer, fileName);
25
+ console.log(`✅ Thumbnail uploaded! URL: ${updatedCourse.thumbnail_image}`);
26
+ }
27
+ else {
28
+ console.warn(`⚠️ Image not found: ${imagePath}`);
29
+ }
30
+ // 2. Create Collection
31
+ console.log("Creating collection 'Artemis AI Core Protocols'...");
32
+ const collection = await client.createCollection({
33
+ name: "Artemis AI Core Protocols",
34
+ description: "Foundational protocols and frameworks for the Agentic Internet.",
35
+ courses: [16], // Mastering MCP
36
+ public: true,
37
+ });
38
+ console.log(`✅ Collection created! UUID: ${collection.collection_uuid}`);
39
+ }
40
+ main().catch(console.error);
@@ -0,0 +1,171 @@
1
+ /**
2
+ * LearnHouse API Client
3
+ *
4
+ * A TypeScript client for interacting with the LearnHouse LMS API.
5
+ */
6
+ import { LearnHouseConfig, LoginResponse, User, Organization, Course, CourseCreate, CourseUpdate, FullCourse, Chapter, ChapterCreate, ChapterUpdate, Activity, ActivityCreate, ActivityUpdate, Collection, CollectionCreate, TrailProgress, ActivityStatus, SearchResult } from "./types.js";
7
+ export declare class LearnHouseClient {
8
+ private config;
9
+ private accessToken?;
10
+ constructor(config: LearnHouseConfig);
11
+ private request;
12
+ private get;
13
+ private post;
14
+ private put;
15
+ private delete;
16
+ private postForm;
17
+ private putForm;
18
+ /**
19
+ * Login with email and password
20
+ */
21
+ login(email: string, password: string): Promise<LoginResponse>;
22
+ /**
23
+ * Get current access token
24
+ */
25
+ getAccessToken(): string | undefined;
26
+ /**
27
+ * Set access token manually
28
+ */
29
+ setAccessToken(token: string): void;
30
+ /**
31
+ * Get current authenticated user
32
+ */
33
+ getCurrentUser(): Promise<User>;
34
+ /**
35
+ * List all organizations
36
+ */
37
+ listOrganizations(): Promise<Organization[]>;
38
+ /**
39
+ * Get organization by ID
40
+ */
41
+ getOrganization(orgId: number): Promise<Organization>;
42
+ /**
43
+ * Get organization by slug
44
+ */
45
+ getOrganizationBySlug(slug: string): Promise<Organization>;
46
+ /**
47
+ * List courses for the configured organization
48
+ */
49
+ listCourses(page?: number, limit?: number): Promise<Course[]>;
50
+ /**
51
+ * Get course by UUID
52
+ */
53
+ getCourse(courseUuid: string): Promise<Course>;
54
+ /**
55
+ * Get course by numeric ID
56
+ */
57
+ getCourseById(courseId: number): Promise<Course>;
58
+ /**
59
+ * Get course with full metadata (chapters & activities)
60
+ */
61
+ getCourseMeta(courseUuid: string, withUnpublished?: boolean): Promise<FullCourse>;
62
+ /**
63
+ * Create a new course
64
+ */
65
+ createCourse(course: CourseCreate): Promise<Course>;
66
+ /**
67
+ * Update a course
68
+ */
69
+ updateCourse(courseUuid: string, updates: CourseUpdate): Promise<Course>;
70
+ /**
71
+ * Upload a course thumbnail
72
+ */
73
+ uploadCourseThumbnail(courseUuid: string, imageBuffer: Buffer, fileName: string, type?: "IMAGE" | "VIDEO"): Promise<Course>;
74
+ /**
75
+ * Delete a course
76
+ */
77
+ deleteCourse(courseUuid: string): Promise<void>;
78
+ /**
79
+ * Get chapter by ID
80
+ */
81
+ getChapter(chapterId: number): Promise<Chapter>;
82
+ /**
83
+ * List chapters for a course
84
+ */
85
+ listChapters(courseId: number, page?: number, limit?: number): Promise<Chapter[]>;
86
+ /**
87
+ * Create a new chapter
88
+ */
89
+ createChapter(chapter: ChapterCreate): Promise<Chapter>;
90
+ /**
91
+ * Update a chapter
92
+ */
93
+ updateChapter(chapterId: number, updates: ChapterUpdate): Promise<Chapter>;
94
+ /**
95
+ * Delete a chapter
96
+ */
97
+ deleteChapter(chapterId: number): Promise<void>;
98
+ /**
99
+ * Get activity by UUID
100
+ */
101
+ getActivity(activityUuid: string): Promise<Activity>;
102
+ /**
103
+ * Get activity by numeric ID
104
+ */
105
+ getActivityById(activityId: number): Promise<Activity>;
106
+ /**
107
+ * List activities for a chapter
108
+ */
109
+ listActivities(chapterId: number): Promise<Activity[]>;
110
+ /**
111
+ * Create a new activity
112
+ */
113
+ createActivity(activity: ActivityCreate): Promise<Activity>;
114
+ /**
115
+ * Update an activity
116
+ */
117
+ updateActivity(activityUuid: string, updates: ActivityUpdate): Promise<Activity>;
118
+ /**
119
+ * Delete an activity
120
+ */
121
+ deleteActivity(activityUuid: string): Promise<void>;
122
+ /**
123
+ * List collections for the configured organization
124
+ */
125
+ listCollections(page?: number, limit?: number): Promise<Collection[]>;
126
+ /**
127
+ * Get collection by UUID
128
+ */
129
+ getCollection(collectionUuid: string): Promise<Collection>;
130
+ /**
131
+ * Create a new collection
132
+ */
133
+ createCollection(collection: Omit<CollectionCreate, "org_id">): Promise<Collection>;
134
+ /**
135
+ * Update a collection
136
+ */
137
+ updateCollection(collectionUuid: string, updates: Partial<CollectionCreate>): Promise<Collection>;
138
+ /**
139
+ * Delete a collection
140
+ */
141
+ deleteCollection(collectionUuid: string): Promise<void>;
142
+ /**
143
+ * Get progress for a course
144
+ */
145
+ getCourseProgress(courseUuid: string): Promise<TrailProgress>;
146
+ /**
147
+ * Get activity completion status
148
+ */
149
+ getActivityStatus(activityUuid: string): Promise<ActivityStatus>;
150
+ /**
151
+ * Mark activity as complete
152
+ */
153
+ markActivityComplete(activityUuid: string): Promise<{
154
+ completed: boolean;
155
+ }>;
156
+ /**
157
+ * Mark activity as incomplete
158
+ */
159
+ markActivityIncomplete(activityUuid: string): Promise<{
160
+ completed: boolean;
161
+ }>;
162
+ /**
163
+ * Search across courses, users, and collections
164
+ */
165
+ search(orgSlug: string, query: string): Promise<SearchResult>;
166
+ /**
167
+ * Check API health
168
+ */
169
+ healthCheck(): Promise<Record<string, unknown>>;
170
+ }
171
+ export declare function createClient(config: LearnHouseConfig): LearnHouseClient;
package/dist/client.js ADDED
@@ -0,0 +1,378 @@
1
+ /**
2
+ * LearnHouse API Client
3
+ *
4
+ * A TypeScript client for interacting with the LearnHouse LMS API.
5
+ */
6
+ export class LearnHouseClient {
7
+ config;
8
+ accessToken;
9
+ constructor(config) {
10
+ this.config = {
11
+ ...config,
12
+ baseUrl: config.baseUrl.replace(/\/$/, ""),
13
+ };
14
+ this.accessToken = config.accessToken;
15
+ }
16
+ // ============================================================
17
+ // HTTP Helpers
18
+ // ============================================================
19
+ async request(method, path, options = {}) {
20
+ const url = new URL(`${this.config.baseUrl}/api/v1${path}`);
21
+ // Add query parameters
22
+ if (options.query) {
23
+ Object.entries(options.query).forEach(([key, value]) => {
24
+ if (value !== undefined) {
25
+ url.searchParams.set(key, String(value));
26
+ }
27
+ });
28
+ }
29
+ const headers = {};
30
+ if (this.accessToken) {
31
+ headers["Authorization"] = `Bearer ${this.accessToken}`;
32
+ }
33
+ let body;
34
+ if (options.formData) {
35
+ body = options.formData;
36
+ // Don't set Content-Type for FormData - browser will set it with boundary
37
+ }
38
+ else if (options.body) {
39
+ headers["Content-Type"] = "application/json";
40
+ body = JSON.stringify(options.body);
41
+ }
42
+ const response = await fetch(url.toString(), {
43
+ method,
44
+ headers,
45
+ body,
46
+ });
47
+ if (!response.ok) {
48
+ const error = await response.json().catch(() => ({
49
+ detail: `HTTP ${response.status}: ${response.statusText}`,
50
+ }));
51
+ throw new Error(error.detail || `Request failed: ${response.status}`);
52
+ }
53
+ // Handle empty responses
54
+ const text = await response.text();
55
+ if (!text) {
56
+ return {};
57
+ }
58
+ return JSON.parse(text);
59
+ }
60
+ get(path, query) {
61
+ return this.request("GET", path, { query });
62
+ }
63
+ post(path, body, query) {
64
+ return this.request("POST", path, { body, query });
65
+ }
66
+ put(path, body) {
67
+ return this.request("PUT", path, { body });
68
+ }
69
+ delete(path) {
70
+ return this.request("DELETE", path);
71
+ }
72
+ postForm(path, formData, query) {
73
+ return this.request("POST", path, { formData, query });
74
+ }
75
+ putForm(path, formData, query) {
76
+ return this.request("PUT", path, { formData, query });
77
+ }
78
+ // ============================================================
79
+ // Authentication
80
+ // ============================================================
81
+ /**
82
+ * Login with email and password
83
+ */
84
+ async login(email, password) {
85
+ const formData = new URLSearchParams();
86
+ formData.set("username", email);
87
+ formData.set("password", password);
88
+ const response = await fetch(`${this.config.baseUrl}/api/v1/auth/login`, {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/x-www-form-urlencoded",
92
+ },
93
+ body: formData.toString(),
94
+ });
95
+ if (!response.ok) {
96
+ const error = await response.json().catch(() => ({ detail: "Login failed" }));
97
+ throw new Error(error.detail || "Login failed");
98
+ }
99
+ const result = await response.json();
100
+ this.accessToken = result.tokens.access_token;
101
+ return result;
102
+ }
103
+ /**
104
+ * Get current access token
105
+ */
106
+ getAccessToken() {
107
+ return this.accessToken;
108
+ }
109
+ /**
110
+ * Set access token manually
111
+ */
112
+ setAccessToken(token) {
113
+ this.accessToken = token;
114
+ }
115
+ /**
116
+ * Get current authenticated user
117
+ */
118
+ async getCurrentUser() {
119
+ return this.get("/users/profile");
120
+ }
121
+ // ============================================================
122
+ // Organizations
123
+ // ============================================================
124
+ /**
125
+ * List all organizations
126
+ */
127
+ async listOrganizations() {
128
+ return this.get("/orgs/");
129
+ }
130
+ /**
131
+ * Get organization by ID
132
+ */
133
+ async getOrganization(orgId) {
134
+ return this.get(`/orgs/${orgId}`);
135
+ }
136
+ /**
137
+ * Get organization by slug
138
+ */
139
+ async getOrganizationBySlug(slug) {
140
+ return this.get(`/orgs/slug/${slug}`);
141
+ }
142
+ // ============================================================
143
+ // Courses
144
+ // ============================================================
145
+ /**
146
+ * List courses for the configured organization
147
+ */
148
+ async listCourses(page = 1, limit = 50) {
149
+ const org = await this.getOrganization(this.config.orgId);
150
+ return this.get(`/courses/org_slug/${org.slug}/page/${page}/limit/${limit}`);
151
+ }
152
+ /**
153
+ * Get course by UUID
154
+ */
155
+ async getCourse(courseUuid) {
156
+ return this.get(`/courses/${courseUuid}`);
157
+ }
158
+ /**
159
+ * Get course by numeric ID
160
+ */
161
+ async getCourseById(courseId) {
162
+ return this.get(`/courses/id/${courseId}`);
163
+ }
164
+ /**
165
+ * Get course with full metadata (chapters & activities)
166
+ */
167
+ async getCourseMeta(courseUuid, withUnpublished = false) {
168
+ return this.get(`/courses/${courseUuid}/meta`, {
169
+ with_unpublished_activities: withUnpublished,
170
+ });
171
+ }
172
+ /**
173
+ * Create a new course
174
+ */
175
+ async createCourse(course) {
176
+ const formData = new FormData();
177
+ formData.set("name", course.name);
178
+ formData.set("description", course.description);
179
+ formData.set("public", String(course.public ?? true));
180
+ formData.set("about", course.about ?? course.description);
181
+ if (course.learnings) {
182
+ formData.set("learnings", JSON.stringify(course.learnings));
183
+ }
184
+ if (course.tags) {
185
+ formData.set("tags", JSON.stringify(course.tags));
186
+ }
187
+ return this.postForm(`/courses/`, formData, { org_id: this.config.orgId });
188
+ }
189
+ /**
190
+ * Update a course
191
+ */
192
+ async updateCourse(courseUuid, updates) {
193
+ return this.put(`/courses/${courseUuid}`, updates);
194
+ }
195
+ /**
196
+ * Upload a course thumbnail
197
+ */
198
+ async uploadCourseThumbnail(courseUuid, imageBuffer, fileName, type = "IMAGE") {
199
+ const formData = new FormData();
200
+ const blob = new Blob([imageBuffer], { type: "image/png" });
201
+ formData.append("thumbnail", blob, fileName);
202
+ formData.append("thumbnail_type", type);
203
+ return this.putForm(`/courses/${courseUuid}/thumbnail`, formData);
204
+ }
205
+ /**
206
+ * Delete a course
207
+ */
208
+ async deleteCourse(courseUuid) {
209
+ await this.delete(`/courses/${courseUuid}`);
210
+ }
211
+ // ============================================================
212
+ // Chapters
213
+ // ============================================================
214
+ /**
215
+ * Get chapter by ID
216
+ */
217
+ async getChapter(chapterId) {
218
+ return this.get(`/chapters/${chapterId}`);
219
+ }
220
+ /**
221
+ * List chapters for a course
222
+ */
223
+ async listChapters(courseId, page = 1, limit = 50) {
224
+ return this.get(`/chapters/course/${courseId}/page/${page}/limit/${limit}`);
225
+ }
226
+ /**
227
+ * Create a new chapter
228
+ */
229
+ async createChapter(chapter) {
230
+ return this.post("/chapters/", chapter);
231
+ }
232
+ /**
233
+ * Update a chapter
234
+ */
235
+ async updateChapter(chapterId, updates) {
236
+ return this.put(`/chapters/${chapterId}`, updates);
237
+ }
238
+ /**
239
+ * Delete a chapter
240
+ */
241
+ async deleteChapter(chapterId) {
242
+ await this.delete(`/chapters/${chapterId}`);
243
+ }
244
+ // ============================================================
245
+ // Activities
246
+ // ============================================================
247
+ /**
248
+ * Get activity by UUID
249
+ */
250
+ async getActivity(activityUuid) {
251
+ return this.get(`/activities/${activityUuid}`);
252
+ }
253
+ /**
254
+ * Get activity by numeric ID
255
+ */
256
+ async getActivityById(activityId) {
257
+ return this.get(`/activities/id/${activityId}`);
258
+ }
259
+ /**
260
+ * List activities for a chapter
261
+ */
262
+ async listActivities(chapterId) {
263
+ return this.get(`/activities/chapter/${chapterId}`);
264
+ }
265
+ /**
266
+ * Create a new activity
267
+ */
268
+ async createActivity(activity) {
269
+ return this.post("/activities/", {
270
+ ...activity,
271
+ activity_type: activity.activity_type ?? "TYPE_DYNAMIC",
272
+ activity_sub_type: activity.activity_sub_type ?? "SUBTYPE_DYNAMIC_PAGE",
273
+ content: activity.content ?? { type: "doc", content: [] },
274
+ published: activity.published ?? false,
275
+ details: activity.details ?? {},
276
+ });
277
+ }
278
+ /**
279
+ * Update an activity
280
+ */
281
+ async updateActivity(activityUuid, updates) {
282
+ return this.put(`/activities/${activityUuid}`, updates);
283
+ }
284
+ /**
285
+ * Delete an activity
286
+ */
287
+ async deleteActivity(activityUuid) {
288
+ await this.delete(`/activities/${activityUuid}`);
289
+ }
290
+ // ============================================================
291
+ // Collections
292
+ // ============================================================
293
+ /**
294
+ * List collections for the configured organization
295
+ */
296
+ async listCollections(page = 1, limit = 50) {
297
+ return this.get(`/collections/org/${this.config.orgId}/page/${page}/limit/${limit}`);
298
+ }
299
+ /**
300
+ * Get collection by UUID
301
+ */
302
+ async getCollection(collectionUuid) {
303
+ return this.get(`/collections/${collectionUuid}`);
304
+ }
305
+ /**
306
+ * Create a new collection
307
+ */
308
+ async createCollection(collection) {
309
+ return this.post("/collections/", {
310
+ ...collection,
311
+ org_id: this.config.orgId,
312
+ public: collection.public ?? true,
313
+ });
314
+ }
315
+ /**
316
+ * Update a collection
317
+ */
318
+ async updateCollection(collectionUuid, updates) {
319
+ return this.put(`/collections/${collectionUuid}`, updates);
320
+ }
321
+ /**
322
+ * Delete a collection
323
+ */
324
+ async deleteCollection(collectionUuid) {
325
+ await this.delete(`/collections/${collectionUuid}`);
326
+ }
327
+ // ============================================================
328
+ // Progress / Trail
329
+ // ============================================================
330
+ /**
331
+ * Get progress for a course
332
+ */
333
+ async getCourseProgress(courseUuid) {
334
+ return this.get(`/trail/course/${courseUuid}/completion`);
335
+ }
336
+ /**
337
+ * Get activity completion status
338
+ */
339
+ async getActivityStatus(activityUuid) {
340
+ return this.get(`/trail/activity/${activityUuid}/status`);
341
+ }
342
+ /**
343
+ * Mark activity as complete
344
+ */
345
+ async markActivityComplete(activityUuid) {
346
+ return this.post(`/trail/activity/${activityUuid}/mark_complete`);
347
+ }
348
+ /**
349
+ * Mark activity as incomplete
350
+ */
351
+ async markActivityIncomplete(activityUuid) {
352
+ return this.post(`/trail/activity/${activityUuid}/mark_incomplete`);
353
+ }
354
+ // ============================================================
355
+ // Search
356
+ // ============================================================
357
+ /**
358
+ * Search across courses, users, and collections
359
+ */
360
+ async search(orgSlug, query) {
361
+ return this.get(`/search/org_slug/${orgSlug}`, {
362
+ query: query,
363
+ });
364
+ }
365
+ // ============================================================
366
+ // Health
367
+ // ============================================================
368
+ /**
369
+ * Check API health
370
+ */
371
+ async healthCheck() {
372
+ return this.get("/health");
373
+ }
374
+ }
375
+ // Export a factory function
376
+ export function createClient(config) {
377
+ return new LearnHouseClient(config);
378
+ }
@@ -0,0 +1,7 @@
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
+ export {};