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.
Files changed (3) hide show
  1. package/README.md +15 -3
  2. package/package.json +1 -1
  3. 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`, `get_account_summary`, `whoami`
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.6.0",
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.6.0' })
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/account?tab=api if it should stop working everywhere.`
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/account?tab=connections`
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