@vue-skuilder/mcp 0.1.8-3

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,313 @@
1
+ import type { CourseDBInterface } from '@vue-skuilder/db';
2
+ import { z } from 'zod';
3
+ import { isSuccessRow } from '../utils/index.js';
4
+
5
+ // Types for card resources
6
+ export interface CardResourceData {
7
+ cardId: string;
8
+ datashape: string;
9
+ data: any;
10
+ tags: string[];
11
+ elo?: number;
12
+ created?: string;
13
+ modified?: string;
14
+ }
15
+
16
+ export interface CardsCollection {
17
+ cards: CardResourceData[];
18
+ total: number;
19
+ page?: number;
20
+ limit?: number;
21
+ filter?: string;
22
+ }
23
+
24
+ // Schema for ELO range parsing
25
+ const EloRangeSchema = z.object({
26
+ min: z.number().min(0).max(5000),
27
+ max: z.number().min(0).max(5000)
28
+ }).refine(data => data.min <= data.max, {
29
+ message: "Min ELO must be less than or equal to max ELO"
30
+ });
31
+
32
+ /**
33
+ * Handle cards://all resource - List all cards in the course
34
+ */
35
+ export async function handleCardsAllResource(
36
+ courseDB: CourseDBInterface,
37
+ limit: number = 50,
38
+ offset: number = 0
39
+ ): Promise<CardsCollection> {
40
+ try {
41
+ // Get course info for total count
42
+ const courseInfo = await courseDB.getCourseInfo();
43
+
44
+ // Get cards using ELO-based query (this gives us all cards sorted by ELO)
45
+ const cardIds = await courseDB.getCardsByELO(1500, limit + offset);
46
+
47
+ // Skip offset cards and take limit
48
+ const targetCardIds = cardIds.slice(offset, offset + limit);
49
+
50
+ if (targetCardIds.length === 0) {
51
+ return {
52
+ cards: [],
53
+ total: courseInfo.cardCount,
54
+ page: Math.floor(offset / limit) + 1,
55
+ limit,
56
+ filter: 'all'
57
+ };
58
+ }
59
+
60
+ // Get card documents
61
+ const cardDocs = await courseDB.getCourseDocs(targetCardIds);
62
+
63
+ // Get ELO data for these cards
64
+ const eloData = await courseDB.getCardEloData(targetCardIds);
65
+ const eloMap = new Map(eloData.map((elo, index) => [targetCardIds[index], elo.global?.score || 1500]));
66
+
67
+ // Transform to CardResourceData format
68
+ const cards: CardResourceData[] = [];
69
+ for (const row of cardDocs.rows) {
70
+ if (isSuccessRow(row)) {
71
+ const doc = row.doc;
72
+ cards.push({
73
+ cardId: doc._id,
74
+ datashape: (doc as any).shape?.name || 'unknown',
75
+ data: (doc as any).data || {},
76
+ tags: [], // Will be populated separately if needed
77
+ elo: eloMap.get(doc._id),
78
+ created: (doc as any).created,
79
+ modified: (doc as any).modified
80
+ });
81
+ }
82
+ }
83
+
84
+ return {
85
+ cards,
86
+ total: courseInfo.cardCount,
87
+ page: Math.floor(offset / limit) + 1,
88
+ limit,
89
+ filter: 'all'
90
+ };
91
+
92
+ } catch (error) {
93
+ console.error('Error fetching all cards:', error);
94
+ throw new Error(`Failed to fetch cards: ${error instanceof Error ? error.message : 'Unknown error'}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Handle cards://tag/[tagName] resource - Filter cards by tag
100
+ */
101
+ export async function handleCardsTagResource(
102
+ courseDB: CourseDBInterface,
103
+ tagName: string,
104
+ limit: number = 50,
105
+ offset: number = 0
106
+ ): Promise<CardsCollection> {
107
+ try {
108
+ // Get the tag to validate it exists
109
+ const tag = await courseDB.getTag(tagName);
110
+ if (!tag) {
111
+ throw new Error(`Tag not found: ${tagName}`);
112
+ }
113
+
114
+ // Note: The current CourseDBInterface doesn't have a direct method to get cards by tag
115
+ // We would need to implement this by querying the tag associations
116
+ // For now, we'll return a placeholder that explains this limitation
117
+
118
+ return {
119
+ cards: [],
120
+ total: 0,
121
+ page: Math.floor(offset / limit) + 1,
122
+ limit,
123
+ filter: `tag:${tagName}`,
124
+ };
125
+
126
+ } catch (error) {
127
+ console.error(`Error fetching cards for tag ${tagName}:`, error);
128
+ throw new Error(`Failed to fetch cards for tag ${tagName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Handle cards://shape/[shapeName] resource - Filter cards by DataShape
134
+ */
135
+ export async function handleCardsShapeResource(
136
+ courseDB: CourseDBInterface,
137
+ shapeName: string,
138
+ limit: number = 50,
139
+ offset: number = 0
140
+ ): Promise<CardsCollection> {
141
+ try {
142
+ // Validate shape exists in course config
143
+ const courseConfig = await courseDB.getCourseConfig();
144
+ const validShapes = courseConfig.dataShapes.map(ds => ds.name);
145
+
146
+ if (!validShapes.includes(shapeName)) {
147
+ throw new Error(`DataShape not found: ${shapeName}. Available: ${validShapes.join(', ')}`);
148
+ }
149
+
150
+ // Note: Direct filtering by DataShape would require a more sophisticated query
151
+ // For now, we'll get all cards and filter in memory (not optimal for large datasets)
152
+ const allCardIds = await courseDB.getCardsByELO(1500, 1000); // Get more cards to filter from
153
+
154
+ if (allCardIds.length === 0) {
155
+ return {
156
+ cards: [],
157
+ total: 0,
158
+ page: Math.floor(offset / limit) + 1,
159
+ limit,
160
+ filter: `shape:${shapeName}`
161
+ };
162
+ }
163
+
164
+ // Get card documents to check their shapes
165
+ const cardDocs = await courseDB.getCourseDocs(allCardIds);
166
+
167
+ // Filter by shape and collect card IDs
168
+ const filteredCardIds: string[] = [];
169
+ const allFilteredRows: any[] = [];
170
+
171
+ for (const row of cardDocs.rows) {
172
+ if (isSuccessRow(row) && (row.doc as any).shape?.name === shapeName) {
173
+ allFilteredRows.push(row);
174
+ filteredCardIds.push(row.doc._id);
175
+ }
176
+ }
177
+
178
+ // Apply pagination to filtered results
179
+ const paginatedRows = allFilteredRows.slice(offset, offset + limit);
180
+ const paginatedCardIds = paginatedRows.map(row => row.doc._id);
181
+
182
+ // Get ELO data for paginated cards
183
+ const eloData = await courseDB.getCardEloData(paginatedCardIds);
184
+ const eloMap = new Map(eloData.map((elo, index) => [paginatedCardIds[index], elo.global?.score || 1500]));
185
+
186
+ // Transform to CardResourceData format
187
+ const cards: CardResourceData[] = [];
188
+ for (const row of paginatedRows) {
189
+ if (isSuccessRow(row)) {
190
+ const doc = row.doc;
191
+ cards.push({
192
+ cardId: doc._id,
193
+ datashape: (doc as any).shape?.name || 'unknown',
194
+ data: (doc as any).data || {},
195
+ tags: [],
196
+ elo: eloMap.get(doc._id),
197
+ created: (doc as any).created,
198
+ modified: (doc as any).modified
199
+ });
200
+ }
201
+ }
202
+
203
+ // Count total filtered cards
204
+ const totalFiltered = filteredCardIds.length;
205
+
206
+ return {
207
+ cards,
208
+ total: totalFiltered,
209
+ page: Math.floor(offset / limit) + 1,
210
+ limit,
211
+ filter: `shape:${shapeName}`
212
+ };
213
+
214
+ } catch (error) {
215
+ console.error(`Error fetching cards for shape ${shapeName}:`, error);
216
+ throw new Error(`Failed to fetch cards for shape ${shapeName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Handle cards://elo/[min]-[max] resource - Filter cards by ELO range
222
+ */
223
+ export async function handleCardsEloResource(
224
+ courseDB: CourseDBInterface,
225
+ eloRange: string,
226
+ limit: number = 50,
227
+ offset: number = 0
228
+ ): Promise<CardsCollection> {
229
+ try {
230
+ // Parse ELO range (format: "1200-1800")
231
+ const [minStr, maxStr] = eloRange.split('-');
232
+ if (!minStr || !maxStr) {
233
+ throw new Error(`Invalid ELO range format: ${eloRange}. Expected format: min-max (e.g., 1200-1800)`);
234
+ }
235
+
236
+ const parsedRange = EloRangeSchema.parse({
237
+ min: parseInt(minStr, 10),
238
+ max: parseInt(maxStr, 10)
239
+ });
240
+
241
+ // Get cards around the middle of the ELO range
242
+ const targetElo = Math.floor((parsedRange.min + parsedRange.max) / 2);
243
+ const cardIds = await courseDB.getCardsByELO(targetElo, 1000); // Get more to filter from
244
+
245
+ if (cardIds.length === 0) {
246
+ return {
247
+ cards: [],
248
+ total: 0,
249
+ page: Math.floor(offset / limit) + 1,
250
+ limit,
251
+ filter: `elo:${eloRange}`
252
+ };
253
+ }
254
+
255
+ // Get ELO data for all cards
256
+ const eloData = await courseDB.getCardEloData(cardIds);
257
+
258
+ // Filter by ELO range
259
+ const filteredEloData = eloData
260
+ .map((elo, index) => ({ elo, cardId: cardIds[index] }))
261
+ .filter(({ elo }) => {
262
+ const score = elo.global?.score || 1500;
263
+ return score >= parsedRange.min && score <= parsedRange.max;
264
+ });
265
+
266
+ // Apply pagination
267
+ const paginatedEloData = filteredEloData.slice(offset, offset + limit);
268
+ const paginatedCardIds = paginatedEloData.map(({ cardId }) => cardId);
269
+
270
+ if (paginatedCardIds.length === 0) {
271
+ return {
272
+ cards: [],
273
+ total: filteredEloData.length,
274
+ page: Math.floor(offset / limit) + 1,
275
+ limit,
276
+ filter: `elo:${eloRange}`
277
+ };
278
+ }
279
+
280
+ // Get card documents
281
+ const cardDocs = await courseDB.getCourseDocs(paginatedCardIds);
282
+ const eloMap = new Map(paginatedEloData.map(({ elo, cardId }) => [cardId, elo.global?.score || 1500]));
283
+
284
+ // Transform to CardResourceData format
285
+ const cards: CardResourceData[] = [];
286
+ for (const row of cardDocs.rows) {
287
+ if (isSuccessRow(row)) {
288
+ const doc = row.doc;
289
+ cards.push({
290
+ cardId: doc._id,
291
+ datashape: (doc as any).shape?.name || 'unknown',
292
+ data: (doc as any).data || {},
293
+ tags: [],
294
+ elo: eloMap.get(doc._id),
295
+ created: (doc as any).created,
296
+ modified: (doc as any).modified
297
+ });
298
+ }
299
+ }
300
+
301
+ return {
302
+ cards,
303
+ total: filteredEloData.length,
304
+ page: Math.floor(offset / limit) + 1,
305
+ limit,
306
+ filter: `elo:${eloRange}`
307
+ };
308
+
309
+ } catch (error) {
310
+ console.error(`Error fetching cards for ELO range ${eloRange}:`, error);
311
+ throw new Error(`Failed to fetch cards for ELO range ${eloRange}: ${error instanceof Error ? error.message : 'Unknown error'}`);
312
+ }
313
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ import type { CourseDBInterface } from '@vue-skuilder/db';
3
+ import type { CourseResource } from '../types/resources.js';
4
+
5
+ export async function handleCourseConfigResource(
6
+ courseDB: CourseDBInterface
7
+ ): Promise<CourseResource> {
8
+ try {
9
+ // Get course configuration
10
+ const config = await courseDB.getCourseConfig();
11
+
12
+ // TODO: Implement proper ELO distribution calculation
13
+ // For now, provide basic structure
14
+ const eloStats = {
15
+ min: 1000,
16
+ max: 2000,
17
+ mean: 1500,
18
+ distribution: [100, 200, 300, 250, 150] // Placeholder histogram
19
+ };
20
+
21
+ return {
22
+ config,
23
+ eloStats
24
+ };
25
+ } catch (error) {
26
+ throw new Error(`Failed to load course config: ${error instanceof Error ? error.message : 'Unknown error'}`);
27
+ }
28
+ }
29
+
30
+ // URI pattern validation
31
+ export const CourseConfigUriSchema = z.string().regex(/^course:\/\/config$/);
32
+ export type CourseConfigUri = z.infer<typeof CourseConfigUriSchema>;
@@ -0,0 +1,23 @@
1
+ // Resource registry and exports
2
+ export * from './course.js';
3
+ export * from './cards.js';
4
+ export * from './shapes.js';
5
+ export * from './tags.js';
6
+
7
+ // Resource URI patterns
8
+ export const RESOURCE_PATTERNS = {
9
+ COURSE_CONFIG: 'course://config',
10
+ CARDS_ALL: 'cards://all',
11
+ CARDS_TAG: 'cards://tag/{tagName}',
12
+ CARDS_SHAPE: 'cards://shape/{shapeName}',
13
+ CARDS_ELO: 'cards://elo/{eloRange}',
14
+ SHAPES_ALL: 'shapes://all',
15
+ SHAPES_SPECIFIC: 'shapes://{shapeName}',
16
+ TAGS_ALL: 'tags://all',
17
+ TAGS_STATS: 'tags://stats',
18
+ TAGS_SPECIFIC: 'tags://{tagName}',
19
+ TAGS_UNION: 'tags://union/{tags}',
20
+ TAGS_INTERSECT: 'tags://intersect/{tags}',
21
+ TAGS_EXCLUSIVE: 'tags://exclusive/{tags}',
22
+ TAGS_DISTRIBUTION: 'tags://distribution',
23
+ } as const;
@@ -0,0 +1,121 @@
1
+ import type { CourseDBInterface } from '@vue-skuilder/db';
2
+ import { isSuccessRow } from '../utils/index.js';
3
+
4
+ export interface ShapeResource {
5
+ name: string;
6
+ description?: string;
7
+ fields: Array<{
8
+ name: string;
9
+ type: string;
10
+ required?: boolean;
11
+ description?: string;
12
+ }>;
13
+ category?: string;
14
+ examples?: any[];
15
+ }
16
+
17
+ export interface ShapesCollection {
18
+ shapes: ShapeResource[];
19
+ total: number;
20
+ availableShapes: string[];
21
+ }
22
+
23
+ /**
24
+ * Handle shapes://all resource - List all available DataShapes
25
+ */
26
+ export async function handleShapesAllResource(
27
+ courseDB: CourseDBInterface
28
+ ): Promise<ShapesCollection> {
29
+ try {
30
+ // Get course config to access DataShapes
31
+ const courseConfig = await courseDB.getCourseConfig();
32
+ const dataShapes = courseConfig.dataShapes || [];
33
+
34
+ // Transform DataShapes to ShapeResource format
35
+ const shapes: ShapeResource[] = dataShapes.map(shape => ({
36
+ name: shape.name,
37
+ description: `DataShape for ${shape.name} content type`,
38
+ fields: (shape as any).fields?.map((field: any) => ({
39
+ name: field.name,
40
+ type: field.type || 'string',
41
+ required: field.required || false,
42
+ description: field.description || `Field for ${field.name}`
43
+ })) || [],
44
+ category: 'course-content',
45
+ examples: [] // Could be populated with example cards
46
+ }));
47
+
48
+ const availableShapes = dataShapes.map(shape => shape.name);
49
+
50
+ return {
51
+ shapes,
52
+ total: shapes.length,
53
+ availableShapes
54
+ };
55
+
56
+ } catch (error) {
57
+ console.error('Error fetching all shapes:', error);
58
+ throw new Error(`Failed to fetch shapes: ${error instanceof Error ? error.message : 'Unknown error'}`);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Handle shapes://[shapeName] resource - Get specific DataShape definition
64
+ */
65
+ export async function handleShapeSpecificResource(
66
+ courseDB: CourseDBInterface,
67
+ shapeName: string
68
+ ): Promise<ShapeResource> {
69
+ try {
70
+ // Get course config to access DataShapes
71
+ const courseConfig = await courseDB.getCourseConfig();
72
+ const dataShapes = courseConfig.dataShapes || [];
73
+
74
+ // Find the specific shape
75
+ const targetShape = dataShapes.find(shape => shape.name === shapeName);
76
+ if (!targetShape) {
77
+ const availableShapes = dataShapes.map(s => s.name);
78
+ throw new Error(`DataShape not found: ${shapeName}. Available shapes: ${availableShapes.join(', ')}`);
79
+ }
80
+
81
+ // Get examples by finding cards that use this shape
82
+ let examples: any[] = [];
83
+ try {
84
+ // Get a few cards that use this shape to provide examples
85
+ const cardIds = await courseDB.getCardsByELO(1500, 10); // Get some sample cards
86
+ if (cardIds.length > 0) {
87
+ const cardDocs = await courseDB.getCourseDocs(cardIds.slice(0, 5)); // Limit to 5 examples
88
+ examples = [];
89
+ for (const row of cardDocs.rows) {
90
+ if (isSuccessRow(row) && (row.doc as any).shape?.name === shapeName) {
91
+ const data = (row.doc as any)?.data;
92
+ if (data) {
93
+ examples.push(data);
94
+ if (examples.length >= 3) break; // Max 3 examples
95
+ }
96
+ }
97
+ }
98
+ }
99
+ } catch (error) {
100
+ console.warn('Could not fetch examples for shape:', error);
101
+ // Continue without examples
102
+ }
103
+
104
+ return {
105
+ name: targetShape.name,
106
+ description: `DataShape definition for ${targetShape.name} content type`,
107
+ fields: (targetShape as any).fields?.map((field: any) => ({
108
+ name: field.name,
109
+ type: field.type || 'string',
110
+ required: field.required || false,
111
+ description: field.description || `Field for ${field.name}`
112
+ })) || [],
113
+ category: 'course-content',
114
+ examples
115
+ };
116
+
117
+ } catch (error) {
118
+ console.error(`Error fetching shape ${shapeName}:`, error);
119
+ throw new Error(`Failed to fetch shape ${shapeName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
120
+ }
121
+ }