wicker-study-mcp 2.6.0 → 2.8.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/README.md +15 -3
- package/package.json +1 -1
- package/server.mjs +39 -3
package/README.md
CHANGED
|
@@ -9,6 +9,17 @@ Runs from anywhere. Nothing here needs a checkout of the application.
|
|
|
9
9
|
|
|
10
10
|
## Use it
|
|
11
11
|
|
|
12
|
+
The authenticated **Docs** page in Wicker Study can mint a scoped key and place it directly inside
|
|
13
|
+
one copy-ready Codex or Claude Code installation block. There is no separate credential field to
|
|
14
|
+
copy. The first line stores the embedded key with owner-only permissions and the second registers
|
|
15
|
+
the MCP server.
|
|
16
|
+
|
|
17
|
+
For a manually supplied key, the same secure bootstrap is available as:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
WICKER_STUDY_URL='https://study.wicker.life' WICKER_STUDY_API_KEY='wsk_…' npx -y wicker-study-mcp@2.8.0 configure
|
|
21
|
+
```
|
|
22
|
+
|
|
12
23
|
```jsonc
|
|
13
24
|
// Claude Desktop / Claude Code / Cursor MCP config
|
|
14
25
|
{
|
|
@@ -56,13 +67,14 @@ agent receives proxied course data and nothing else.
|
|
|
56
67
|
`wicker_status`, `wicker_authorize`, `wicker_sign_out`, and `canvas_connect` work without a key —
|
|
57
68
|
they are how a key is obtained. Everything else needs one.
|
|
58
69
|
|
|
59
|
-
- **Reading** — `list_courses`, `get_course`, `get_chapter`, `get_course_outline`, `search_course`,
|
|
70
|
+
- **Reading** — `list_courses`, `get_course`, `get_chapter`, `get_course_outline`, `search_course`, `list_regulation_sources`, `search_regulations`,
|
|
60
71
|
`list_materials`, `list_questions`, `get_practice_queue`, `get_progress`, `list_flashcards`,
|
|
61
72
|
`list_due_cards`, `list_mistakes`, `list_mock_sessions`, `get_mock_session`, `get_academic_plan`,
|
|
62
|
-
`list_known_programmes`, `get_calendar`, `get_activity`,
|
|
73
|
+
`get_planning_context`, `list_known_programmes`, `get_calendar`, `get_activity`,
|
|
74
|
+
`get_account_summary`, `whoami`
|
|
63
75
|
- **Studying** — `submit_answer`, `set_mastery`, `review_card`, `add_to_deck`, `create_flashcard`,
|
|
64
76
|
`review_flashcard`, `resolve_mistake`, `record_chapter_read`, `save_academic_plan`,
|
|
65
|
-
`set_course_visibility`, `join_programme`
|
|
77
|
+
`update_planning_objective`, `set_course_visibility`, `join_programme`
|
|
66
78
|
- **Documents and calendars** — `analyze_documents`, `apply_changes`, `preview_calendar`,
|
|
67
79
|
`save_calendar_link`, `sync_calendar_link`, `remove_calendar_link`
|
|
68
80
|
- **Canvas** — `canvas_connect`, `canvas_updates` (announcements, assignments with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicker-study-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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
|
@@ -25,6 +25,24 @@ import { getSavedCanvasAccessToken, promptForLocalCanvasImport, saveCanvasAccess
|
|
|
25
25
|
import { beginAuthorization } from './authorize.mjs'
|
|
26
26
|
import { configPath, forgetApiKey, listSavedServers, normaliseServerUrl, resolveApiKey, saveApiKey } from './config.mjs'
|
|
27
27
|
|
|
28
|
+
// A copy-ready installation block can provision the credential before an MCP
|
|
29
|
+
// client launches the stdio server. Persist it through the same hardened
|
|
30
|
+
// config writer as browser authorization (0700 directory, 0600 file), then
|
|
31
|
+
// discard the environment value with this short-lived process.
|
|
32
|
+
if (process.argv[2] === 'configure') {
|
|
33
|
+
try {
|
|
34
|
+
const serverUrl = normaliseServerUrl(process.env.WICKER_STUDY_URL || 'https://study.wicker.life')
|
|
35
|
+
const apiKey = String(process.env.WICKER_STUDY_API_KEY || '').trim()
|
|
36
|
+
if (!apiKey) throw new Error('WICKER_STUDY_API_KEY is required for configuration.')
|
|
37
|
+
const saved = await saveApiKey(serverUrl, apiKey, { name: 'Terminal installation', scopes: ['read', 'write'] })
|
|
38
|
+
process.stdout.write(`Wicker Study access saved securely for ${saved.server} at ${saved.path}.\n`)
|
|
39
|
+
process.exit(0)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
process.stderr.write(`${error instanceof Error ? error.message : 'Wicker Study could not be configured.'}\n`)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
const baseUrl = normaliseServerUrl(process.env.WICKER_STUDY_URL || 'https://study.wicker.life')
|
|
29
47
|
const DEFAULT_CANVAS_URL = process.env.WICKER_CANVAS_URL || 'https://canvas.maastrichtuniversity.nl'
|
|
30
48
|
|
|
@@ -71,7 +89,7 @@ const json = (value) => ({ content: [{ type: 'text', text: typeof value === 'str
|
|
|
71
89
|
const failed = (error) => ({ isError: true, content: [{ type: 'text', text: error.message }] })
|
|
72
90
|
const run = (fn) => async (args) => { try { return json(await fn(args)) } catch (error) { return failed(error) } }
|
|
73
91
|
|
|
74
|
-
const server = new McpServer({ name: 'wicker-study', version: '2.
|
|
92
|
+
const server = new McpServer({ name: 'wicker-study', version: '2.8.0' })
|
|
75
93
|
const courseId = z.string().describe('Course id (e.g. "sec"). Use list_courses to discover ids.')
|
|
76
94
|
const chapterId = z.string().describe('Chapter id (e.g. "02").')
|
|
77
95
|
|
|
@@ -417,7 +435,7 @@ server.tool('wicker_sign_out',
|
|
|
417
435
|
stillConnected: Boolean(credential.apiKey),
|
|
418
436
|
note: credential.apiKey
|
|
419
437
|
? '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/
|
|
438
|
+
: `Revoke the key itself at ${baseUrl}/app/settings?tab=api if it should stop working everywhere.`
|
|
421
439
|
}
|
|
422
440
|
}))
|
|
423
441
|
|
|
@@ -426,7 +444,7 @@ server.tool('canvas_connect',
|
|
|
426
444
|
{ canvasUrl: z.string().optional().describe(`Canvas origin. Default ${DEFAULT_CANVAS_URL}.`) },
|
|
427
445
|
run(async ({ canvasUrl }) => {
|
|
428
446
|
const origin = new URL(canvasUrl || DEFAULT_CANVAS_URL).origin
|
|
429
|
-
const settings = `${baseUrl}/app/
|
|
447
|
+
const settings = `${baseUrl}/app/settings?tab=connections`
|
|
430
448
|
const connections = (await api('/api/account/integrations/canvas')).connections || []
|
|
431
449
|
const match = connections.find((connection) => connection.origin === origin) || null
|
|
432
450
|
if (match) {
|
|
@@ -470,6 +488,15 @@ server.tool('search_course', 'Hybrid full-text and embedding retrieval across pu
|
|
|
470
488
|
query: z.string(),
|
|
471
489
|
limit: z.number().int().min(1).max(20).optional()
|
|
472
490
|
}, run((args) => api('/api/retrieve', { method: 'POST', body: args })))
|
|
491
|
+
server.tool('search_regulations', 'Focused retrieval from official regulations for the active programme. Use for the Education and Examination Regulations, Board of Examiners, exam and resit procedures, registration, inspections, appeals, exemptions, hardship, fraud, projects, internships and curriculum transition rules. Results include the governing document, academic year and exact page; programme-restricted originals are not exposed.', {
|
|
492
|
+
query: z.string(),
|
|
493
|
+
academicYear: z.string().optional().describe('Exact academic year such as 2026-2027. Defaults to the active programme year.'),
|
|
494
|
+
documentKind: z.enum(['education-examination-regulations', 'rules-regulations', 'board-of-examiners', 'exam-procedure', 'programme-policy', 'other']).optional(),
|
|
495
|
+
limit: z.number().int().min(1).max(20).optional()
|
|
496
|
+
}, run((args) => api('/api/programme-policies/retrieve', { method: 'POST', body: args })))
|
|
497
|
+
server.tool('list_regulation_sources', 'List the official regulation sources indexed for the active programme and academic year. Returns reviewed metadata and coverage counts, never a programme-restricted original.', {
|
|
498
|
+
academicYear: z.string().optional().describe('Exact academic year such as 2026-2027. Defaults to the active programme year.')
|
|
499
|
+
}, run(({ academicYear }) => api('/api/programme-policies', { query: { academicYear } })))
|
|
473
500
|
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
501
|
canvasUrl: z.string().url().optional()
|
|
475
502
|
}, run(({ canvasUrl }) => api('/api/account/integrations/canvas/corpus', { query: { canvasUrl: canvasUrl || DEFAULT_CANVAS_URL } })))
|
|
@@ -489,6 +516,7 @@ server.tool('list_mistakes', 'Mistake bank.', { open: z.boolean().optional().des
|
|
|
489
516
|
server.tool('list_mock_sessions', 'Completed mock sessions.', {}, run(() => api('/api/mocks')))
|
|
490
517
|
server.tool('get_mock_session', 'One mock session with every answer and correction.', { sessionId: z.string() }, run(({ sessionId }) => api(`/api/mocks/${encodeURIComponent(sessionId)}`)))
|
|
491
518
|
server.tool('get_academic_plan', 'Active academic programme: courses, attempts, exam dates, events, gates, summary.', {}, run(() => api('/api/academics')))
|
|
519
|
+
server.tool('get_planning_context', 'Read the student’s saved exam scenario as a compact planning model. Recorded attempts and grades are explicitly separated from private choices such as a resit, following-year deferral, expected grade, or what-if outcome. Actual academic-calendar records are grouped into dated examination windows, so one window can contain a period’s primary exams and another period’s resits. Each course includes allowed session ids and course-specific roles derived from its teaching period, calendar, transcript fallback, and verified resit rules. Returns stable session ids and a revision; call this before suggesting or changing the plan.', {}, run(() => api('/api/planning/context')))
|
|
492
520
|
server.tool('list_known_programmes', 'The catalogue of known bachelor programmes.', {}, run(() => api('/api/editorial-programmes')))
|
|
493
521
|
server.tool('get_calendar', 'Unified calendar in one call: exam attempts, personal events, registration windows, the institution calendar, saved timetable feeds (lectures, tutorials, labs), and — when Canvas is connected — Canvas assignment deadlines and Canvas course events. This is the tool for "when is my next lecture", "where do I need to be", and "what is due this week". Events carry `category`, `courseCode`, and for Canvas items a `canvasStatus`; `problems` names any source that could not be read, which is how you tell an empty week from a missing timetable feed.', { from: z.string().optional().describe('ISO date; omit for everything'), to: z.string().optional() },
|
|
494
522
|
run(async ({ from, to }) => { const data = await api('/api/calendar/events'); const events = data.events.filter((e) => (!from || String(e.start) >= from) && (!to || String(e.start) <= to)); return { ...data, events } }))
|
|
@@ -518,6 +546,14 @@ server.tool('record_chapter_read', 'Record that the student read a chapter.', {
|
|
|
518
546
|
run(({ courseId, chapterId, label }) => api('/api/activity', { method: 'POST', body: { type: 'read', courseId, chapterId, label } })))
|
|
519
547
|
server.tool('save_academic_plan', 'Save the active academic programme workspace. Pass the revision you read to avoid overwriting concurrent edits.', { workspace: z.record(z.any()), expectedRevision: z.number().int() },
|
|
520
548
|
run(({ workspace, expectedRevision }) => api('/api/academics', { method: 'PUT', body: { workspace, expectedRevision } })))
|
|
549
|
+
server.tool('update_planning_objective', 'Update one course in the student’s private exam scenario without replacing the rest of the academic record. Call get_planning_context first, use a session id from that course’s planningRules.allowedSessionIds, inspect allowedDestinations for whether the shared window is a primary or resit route for that course, explain the exact change to the student, and pass the revision you read. Invalid sittings and stale revisions are rejected.', {
|
|
550
|
+
courseId: z.string(),
|
|
551
|
+
expectedRevision: z.number().int(),
|
|
552
|
+
mode: z.enum(['current', 'resit', 'none']).optional(),
|
|
553
|
+
targetSession: z.string().max(140).nullable().optional(),
|
|
554
|
+
expectedGrade: z.number().min(0).max(100).nullable().optional(),
|
|
555
|
+
outcome: z.enum(['actual', 'pass', 'fail']).optional()
|
|
556
|
+
}, run(({ courseId: id, expectedRevision, ...objective }) => api(`/api/planning/objectives/${encodeURIComponent(id)}`, { method: 'PATCH', body: { objective, expectedRevision } })))
|
|
521
557
|
server.tool('set_course_visibility', 'Archive/unarchive or reorder a course for the student.', { courseId, archived: z.boolean().optional(), order: z.number().int().optional() },
|
|
522
558
|
run(({ courseId, archived, order }) => api(`/api/courses/${encodeURIComponent(courseId)}`, { method: 'PATCH', body: { archived, order } })))
|
|
523
559
|
|