wicker-study-mcp 2.5.0 → 2.6.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/package.json +1 -1
- package/server.mjs +20 -5
- package/vendor/canvas-course-import.mjs +19 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicker-study-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "MCP server for Wicker Study: read course material and a student's academic record, study on their behalf, collect a private Canvas course snapshot, and \u2014 with an admin key \u2014 run the editorial workflow.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
package/server.mjs
CHANGED
|
@@ -71,7 +71,7 @@ const json = (value) => ({ content: [{ type: 'text', text: typeof value === 'str
|
|
|
71
71
|
const failed = (error) => ({ isError: true, content: [{ type: 'text', text: error.message }] })
|
|
72
72
|
const run = (fn) => async (args) => { try { return json(await fn(args)) } catch (error) { return failed(error) } }
|
|
73
73
|
|
|
74
|
-
const server = new McpServer({ name: 'wicker-study', version: '2.
|
|
74
|
+
const server = new McpServer({ name: 'wicker-study', version: '2.6.0' })
|
|
75
75
|
const courseId = z.string().describe('Course id (e.g. "sec"). Use list_courses to discover ids.')
|
|
76
76
|
const chapterId = z.string().describe('Chapter id (e.g. "02").')
|
|
77
77
|
|
|
@@ -417,7 +417,7 @@ server.tool('wicker_sign_out',
|
|
|
417
417
|
stillConnected: Boolean(credential.apiKey),
|
|
418
418
|
note: credential.apiKey
|
|
419
419
|
? 'WICKER_STUDY_API_KEY is set in this process’s environment and still applies; unset it to disconnect fully.'
|
|
420
|
-
: `Revoke the key itself at ${baseUrl}/app
|
|
420
|
+
: `Revoke the key itself at ${baseUrl}/app/account?tab=api if it should stop working everywhere.`
|
|
421
421
|
}
|
|
422
422
|
}))
|
|
423
423
|
|
|
@@ -426,7 +426,7 @@ server.tool('canvas_connect',
|
|
|
426
426
|
{ canvasUrl: z.string().optional().describe(`Canvas origin. Default ${DEFAULT_CANVAS_URL}.`) },
|
|
427
427
|
run(async ({ canvasUrl }) => {
|
|
428
428
|
const origin = new URL(canvasUrl || DEFAULT_CANVAS_URL).origin
|
|
429
|
-
const settings = `${baseUrl}/app
|
|
429
|
+
const settings = `${baseUrl}/app/account?tab=connections`
|
|
430
430
|
const connections = (await api('/api/account/integrations/canvas')).connections || []
|
|
431
431
|
const match = connections.find((connection) => connection.origin === origin) || null
|
|
432
432
|
if (match) {
|
|
@@ -460,8 +460,23 @@ server.tool('get_chapter', 'Chapter markdown content. relPath opens a linked fil
|
|
|
460
460
|
run(({ courseId, chapterId, relPath }) => api(`/api/chapter/${encodeURIComponent(courseId)}/${encodeURIComponent(chapterId)}${relPath ? '/' + relPath.split('/').map(encodeURIComponent).join('/') : ''}`)))
|
|
461
461
|
server.tool('get_course_outline', 'Heading outline of every chapter in a course.', { courseId }, run(({ courseId }) => api(`/api/course-toc/${encodeURIComponent(courseId)}`)))
|
|
462
462
|
server.tool('list_materials', 'Files in a course knowledge base (markdown, PDFs, images, code).', { courseId }, run(({ courseId }) => api('/api/materials', { query: { courseId } })))
|
|
463
|
-
server.tool('search_course', '
|
|
464
|
-
|
|
463
|
+
server.tool('search_course', 'Hybrid full-text and embedding retrieval across published material and authorised Canvas snapshots. Results identify the exact academic-year edition and source path. Specify academicYear for a strict edition query; otherwise current and historical editions may be searched, with newer editions preferred.', {
|
|
464
|
+
courseId: courseId.optional(),
|
|
465
|
+
courseCode: z.string().optional().describe('Stable course code, for example BCS1540. Use this when querying Canvas editions.'),
|
|
466
|
+
canonicalCourseId: z.string().optional().describe('Stable corpus course identity returned by an earlier search.'),
|
|
467
|
+
academicYear: z.string().optional().describe('Exact edition such as 2025-2026.'),
|
|
468
|
+
sourceType: z.enum(['syllabus', 'requirements', 'slides', 'pages', 'assessments', 'activities', 'readings', 'materials']).optional(),
|
|
469
|
+
includeHistorical: z.boolean().optional().describe('Search older editions when no exact year is requested; defaults to true.'),
|
|
470
|
+
query: z.string(),
|
|
471
|
+
limit: z.number().int().min(1).max(20).optional()
|
|
472
|
+
}, run((args) => api('/api/retrieve', { method: 'POST', body: args })))
|
|
473
|
+
server.tool('canvas_corpus_status', 'Material collection consent, background sync jobs, versioned course editions, source counts, and last/next scrape times for the connected account.', {
|
|
474
|
+
canvasUrl: z.string().url().optional()
|
|
475
|
+
}, run(({ canvasUrl }) => api('/api/account/integrations/canvas/corpus', { query: { canvasUrl: canvasUrl || DEFAULT_CANVAS_URL } })))
|
|
476
|
+
server.tool('canvas_corpus_sync', 'Queue a server-side refresh of authorised Canvas material. Collection must first be enabled by the user in the signed-in Wicker Study browser; an MCP key cannot grant or expand consent.', {
|
|
477
|
+
canvasUrl: z.string().url().optional(),
|
|
478
|
+
force: z.boolean().optional()
|
|
479
|
+
}, run(({ canvasUrl, force }) => api('/api/integrations/canvas/corpus/sync', { method: 'POST', body: { canvasUrl: canvasUrl || DEFAULT_CANVAS_URL, force } })))
|
|
465
480
|
server.tool('list_questions', 'Published questions for a chapter plus the student’s personal extra exercises.', { courseId, chapterId },
|
|
466
481
|
run(({ courseId, chapterId }) => api(`/api/questions/${encodeURIComponent(courseId)}/${encodeURIComponent(chapterId)}`)))
|
|
467
482
|
server.tool('get_practice_queue', 'Every published question across active courses (optionally one course).', { courseId: courseId.optional(), limit: z.number().int().min(1).max(500).optional() },
|
|
@@ -715,10 +715,11 @@ export async function importCanvasCourse({ courseUrl, accessToken, outputFolder,
|
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
717
|
|
|
718
|
-
const [courseAssignments, courseQuizzes, courseDiscussions] = await Promise.all([
|
|
718
|
+
const [courseAssignments, courseQuizzes, courseDiscussions, coursePages] = await Promise.all([
|
|
719
719
|
optionalCourseCollection('Course-wide assignments listing', `/api/v1/courses/${encodeURIComponent(canvas.courseId)}/assignments?per_page=100`),
|
|
720
720
|
optionalCourseCollection('Course-wide quizzes listing', `/api/v1/courses/${encodeURIComponent(canvas.courseId)}/quizzes?per_page=100`),
|
|
721
|
-
optionalCourseCollection('Course-wide discussions listing', `/api/v1/courses/${encodeURIComponent(canvas.courseId)}/discussion_topics?per_page=100`)
|
|
721
|
+
optionalCourseCollection('Course-wide discussions listing', `/api/v1/courses/${encodeURIComponent(canvas.courseId)}/discussion_topics?per_page=100`),
|
|
722
|
+
requestedModuleIds ? Promise.resolve([]) : optionalCourseCollection('Course-wide Pages listing', `/api/v1/courses/${encodeURIComponent(canvas.courseId)}/pages?per_page=100`)
|
|
722
723
|
])
|
|
723
724
|
for (const [index, assignment] of courseAssignments.entries()) {
|
|
724
725
|
const id = String(assignment.id || '')
|
|
@@ -736,6 +737,21 @@ export async function importCanvasCourse({ courseUrl, accessToken, outputFolder,
|
|
|
736
737
|
await importDiscussion({ discussionId: id, base: join(root, 'course-communications'), position: index + 1, source: { moduleId: null, moduleName: null, itemId: id, itemType: 'Course discussion', title: text(discussion.title, 300) }, initial: discussion })
|
|
737
738
|
}
|
|
738
739
|
|
|
740
|
+
// Lecturers often publish wiki pages through the Pages navigation without
|
|
741
|
+
// adding them to a module. Enumerate that collection during a full archive;
|
|
742
|
+
// importPage deduplicates anything already reached through modules or links.
|
|
743
|
+
for (const [index, page] of coursePages.entries()) {
|
|
744
|
+
const pageSlug = text(page.url || page.page_url, 300)
|
|
745
|
+
if (!pageSlug || importedPageSlugs.has(pageSlug)) continue
|
|
746
|
+
await importPage({
|
|
747
|
+
slug: pageSlug,
|
|
748
|
+
base: join(root, 'course-pages'),
|
|
749
|
+
position: index + 1,
|
|
750
|
+
title: page.title || pageSlug,
|
|
751
|
+
source: { moduleId: null, moduleName: null, itemId: pageSlug, itemType: 'Course page', title: text(page.title, 300) }
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
|
|
739
755
|
for (const [index, file] of courseFiles.entries()) {
|
|
740
756
|
if (downloadedFileIds.has(String(file.id))) continue
|
|
741
757
|
await importFile(file.id, join(root, 'unassigned-files'), index + 1, { moduleId: null, moduleName: null, itemId: String(file.id), itemType: 'File', title: file.display_name || file.filename })
|
|
@@ -757,7 +773,7 @@ export async function importCanvasCourse({ courseUrl, accessToken, outputFolder,
|
|
|
757
773
|
limits: { maxResources, maxFileBytes }
|
|
758
774
|
}
|
|
759
775
|
await writeFile(manifestPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8')
|
|
760
|
-
await writeFile(join(root, 'README.md'), `# ${courseName}\n\nImported privately from Canvas on ${summary.importedAt.slice(0, 10)}.\n\n- Canvas course: ${canvas.courseUrl}\n- Modules included: ${selectedModules.length}${requestedModuleIds ? ' (chosen subset)' : ''}\n- Resources written: ${records.length}\n- Resources skipped: ${skipped.length}\n- Previous imported paths no longer found: ${staleLocalResources.length}\n\nThe snapshot includes the Canvas rich-text syllabus when the account can read it, plus separately uploaded course files (including syllabus/course-manual files), module material, accessible course-wide assignments, quizzes, discussions, and question banks where Canvas permits question access. Canvas pages are followed recursively when they link to another page in this same course. File links in rich-text records are downloaded when accessible; every HTTP(S) reference is compiled into a nearby \`link-index\` file and the hidden manifest. External sites are recorded, never crawled.\n\nThis folder is a source snapshot. Keep it local until the administrator confirms they are authorised to submit the materials for editorial review. The hidden \`.wicker-canvas-import.json\` file records exactly what was found. Re-run the importer into this same folder to refresh changed or newly published Canvas material. Paths no longer returned by Canvas are listed in that manifest for review; they are never deleted automatically.\n`, 'utf8')
|
|
776
|
+
await writeFile(join(root, 'README.md'), `# ${courseName}\n\nImported privately from Canvas on ${summary.importedAt.slice(0, 10)}.\n\n- Canvas course: ${canvas.courseUrl}\n- Modules included: ${selectedModules.length}${requestedModuleIds ? ' (chosen subset)' : ''}\n- Resources written: ${records.length}\n- Resources skipped: ${skipped.length}\n- Previous imported paths no longer found: ${staleLocalResources.length}\n\nThe snapshot includes the Canvas rich-text syllabus when the account can read it, plus separately uploaded course files (including syllabus/course-manual files), module material, standalone course Pages, accessible course-wide assignments, quizzes, discussions, and question banks where Canvas permits question access. Canvas pages are followed recursively when they link to another page in this same course. File links in rich-text records are downloaded when accessible; every HTTP(S) reference is compiled into a nearby \`link-index\` file and the hidden manifest. External sites are recorded, never crawled.\n\nThis folder is a source snapshot. Keep it local until the administrator confirms they are authorised to submit the materials for editorial review. The hidden \`.wicker-canvas-import.json\` file records exactly what was found. Re-run the importer into this same folder to refresh changed or newly published Canvas material. Paths no longer returned by Canvas are listed in that manifest for review; they are never deleted automatically.\n`, 'utf8')
|
|
761
777
|
|
|
762
778
|
return {
|
|
763
779
|
root,
|