wicker-study-mcp 2.14.2 → 2.15.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 CHANGED
@@ -190,6 +190,7 @@ authoritative list of endpoints and scopes.
190
190
 
191
191
  - Source reading: `read_course_source`, `canvas_course_materials`, `canvas_search_announcements`.
192
192
  - Assignment details: `canvas_assignment_detail` returns the full brief, deadlines, own submission, rubric and comments; link to the first-party Updates assignment view.
193
+ - Refresh course materials: `refresh_course_materials({canvasCourseId, canvasUrl, confirmed:true})` queues an immediate refresh of one exact Canvas edition, matching the course page. Discover the ID, host and academic year with `canvas_corpus_status`; existing collection consent is required. Follow status/logs to completion, then read `canvas_course_materials`. Available through hosted MCP and the optional local package.
193
194
  - Sync: `canvas_corpus_status`, `canvas_corpus_sync`, `canvas_sync_course`, `canvas_sync_logs`, `canvas_sync_control`. Latest current-period editions refresh updates every 30 minutes and materials every six hours. Historic retakes stay available on demand; unchanged resources reuse their durable originals/indexes.
194
195
  - Personal study context: `get_study_work`, `get_attendance`, `get_course_obligations`, `get_study_readiness`, `get_weekly_review`.
195
196
  - Persistent Tutor: `tutor_history`, `tutor_conversation`, `tutor_ask`, `tutor_approve_action`, `tutor_delete_conversation`.
@@ -0,0 +1,21 @@
1
+ // The web calendar includes reconciliation proposals and whole-term analytics.
2
+ // They are not evidence for a bounded calendar lookup and can bury its events.
3
+ export function calendarToolResult(data, { from, to } = {}) {
4
+ if (from && to && from > to) throw new Error('from must be on or before to')
5
+ const inRange = (day) => (!from || day >= from) && (!to || day <= to)
6
+ const events = (data.events || []).filter((event) => inRange(String(event.start).slice(0, 10)))
7
+ // Existing notices keep the original date in their server-generated detail.
8
+ // Include moves both away from and into the requested day.
9
+ const changes = (data.changes || []).filter((change) =>
10
+ [change.date, ...(String(change.detail || '').match(/\b\d{4}-\d{2}-\d{2}\b/g) || [])]
11
+ .filter(Boolean).some(inRange))
12
+ return {
13
+ events,
14
+ changes,
15
+ range: { from: from || null, to: to || null, inclusive: true },
16
+ eventCount: events.length,
17
+ feeds: data.feeds || [],
18
+ canvas: data.canvas || { connected: false },
19
+ problems: data.problems || [],
20
+ }
21
+ }
package/core-tools.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { calendarToolResult } from './calendar-result.mjs'
1
2
  import { searchCourseSchema } from './search-course-schema.mjs'
2
3
  // Shared by stdio and Streamable HTTP. Keep schemas and behavior in one place.
3
4
  export function registerCoreTools(server, { z, run, api, defaultCanvasUrl: DEFAULT_CANVAS_URL }) {
@@ -43,8 +44,8 @@ server.tool('get_mock_session', 'One mock session with every answer and correcti
43
44
  server.tool('get_academic_plan', 'Active academic programme: courses, attempts, exam dates, events, gates, summary.', {}, run(() => api('/api/academics')))
44
45
  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')))
45
46
  server.tool('list_known_programmes', 'The catalogue of known bachelor programmes.', {}, run(() => api('/api/editorial-programmes')))
46
- 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() },
47
- 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 } }))
47
+ 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().date().optional().describe('Inclusive first calendar date (YYYY-MM-DD); omit for no lower bound.'), to: z.string().date().optional().describe('Inclusive last calendar date (YYYY-MM-DD). Use the same date as from for one full day.') },
48
+ run(async ({ from, to }) => { const data = await api('/api/calendar/events'); return calendarToolResult(data, { from, to }) }))
48
49
  server.tool('get_activity', 'Study activity series, streak, weekly totals, recent events.', { days: z.number().int().min(7).max(120).optional() }, run(({ days }) => api('/api/activity', { query: { days } })))
49
50
  server.tool('get_account_summary', 'What is stored for the account, per record family.', {}, run(() => api('/api/account/summary')))
50
51
 
package/guidance.md CHANGED
@@ -186,6 +186,14 @@ refreshable. Unchanged versioned files reuse originals and indexes. Changed or u
186
186
  files are fetched again. Dataset text may be a labelled structural sample; the full original
187
187
  is retained. A stored original does not imply complete text extraction.
188
188
 
189
+ When the student asks to refresh course materials, use `canvas_corpus_status` to identify
190
+ the exact `canvas_course_id`, `origin` and academic year, then call
191
+ `refresh_course_materials` with that ID and `canvasUrl` after the required confirmation.
192
+ It queues the same forced edition refresh as the course page. A queue receipt does not
193
+ mean the files are ready: inspect the job status and logs, then call
194
+ `canvas_course_materials` after completion. Report failures or local/unavailable mode
195
+ honestly. This refresh does not generate study guides or change upstream Canvas content.
196
+
189
197
  Use `canvas_corpus_status` for editions/jobs, `canvas_sync_logs` for real progress and
190
198
  `canvas_sync_control` to stop or retry one requested job. Follow `nextCursor` through logs.
191
199
  A recent worker checkpoint with old resource progress is not proof of healthy advancement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicker-study-mcp",
3
- "version": "2.14.2",
3
+ "version": "2.15.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",
@@ -42,7 +42,8 @@
42
42
  "README.md",
43
43
  "core-tools.mjs",
44
44
  "tool-annotations.mjs",
45
- "search-course-schema.mjs"
45
+ "search-course-schema.mjs",
46
+ "calendar-result.mjs"
46
47
  ],
47
48
  "dependencies": {
48
49
  "@modelcontextprotocol/sdk": "^1.30.0",
package/study-tools.mjs CHANGED
@@ -25,6 +25,7 @@ export function registerStudyTools(server, { z, run, api, defaultCanvasUrl }) {
25
25
  tool('canvas_assignment_detail', 'Read the full assignment brief, due/unlock/lock dates, own submission status, grade, rubric and submission comments. Use Canvas numeric course and assignment IDs from canvas_updates. Direct the student to /app/updates?tab=assignments&assignment=COURSE_ID%3AASSIGNMENT_ID; Canvas remains a secondary link.', { canvasUrl, courseId: z.string().regex(/^\d+$/), assignmentId: z.string().regex(/^\d+$/), refresh: z.boolean().optional() }, ({ refresh, ...args }) => api('/api/integrations/canvas/assignment', { query: { ...args, canvasUrl: args.canvasUrl || defaultCanvasUrl, refresh: refresh ? '1' : undefined } }))
26
26
  tool('canvas_sync_logs', 'Inspect durable progress for one sync job, optionally filtered by stage or severity. before is the next cursor returned by an earlier page. Last progress and worker checkpoints are distinct; do not claim a task is advancing from its heartbeat alone.', { job: id, before: z.string().max(160).optional(), stage: z.string().max(40).optional(), level: z.enum(['info','warning','error']).optional() }, args => api('/api/account/integrations/canvas/corpus/logs', { query: args }))
27
27
  tool('canvas_sync_control', 'Stop or retry one owned course-edition sync, after the student requests that action. Retry resumes saved originals and unfinished stages; stop fences the running worker and pauses future material refreshes for that edition.', { jobId: id, action: z.enum(['stop','retry']) }, ({ jobId, action }) => api(`/api/integrations/canvas/corpus/jobs/${encodeURIComponent(jobId)}`, { method: 'POST', body: { action } }))
28
+ tool('refresh_course_materials', 'Refresh stored materials for one Canvas course edition now, including new or changed files. First use canvas_corpus_status to select the exact canvas_course_id and origin for the requested course and academic year; never guess a numeric ID or refresh another year. Uses the same forced edition refresh as the course page. Existing browser-granted collection consent is required and cannot be expanded here. Returns the queue receipt, not completed materials: inspect canvas_corpus_status and canvas_sync_logs until the job finishes, then call canvas_course_materials. Does not generate study guides or modify Canvas.', { canvasUrl, canvasCourseId: z.string().regex(/^\d+$/).describe('Exact Canvas edition ID from canvas_corpus_status, not a course code.') }, ({ canvasUrl, canvasCourseId }) => api('/api/integrations/canvas/corpus/course', { method: 'POST', body: { canvasUrl: canvasUrl || defaultCanvasUrl, canvasCourseId, force: true } }))
28
29
  tool('canvas_sync_course', 'Queue a specific accessible Canvas course edition. Current-period latest editions refresh automatically; use this for a missing or historical edition or an explicit refresh. Existing material consent is required; this cannot grant consent. force:false reuses verified unchanged resources.', { canvasUrl, canvasCourseId: z.string().regex(/^\d+$/), force: z.boolean().default(false) }, args => api('/api/integrations/canvas/corpus/course', { method: 'POST', body: { ...args, canvasUrl: args.canvasUrl || defaultCanvasUrl } }))
29
30
  tool('tutor_prepare_attendance_update', 'Prepare an exact attendance change without applying it or calling a model. First read get_attendance, then use its exact session IDs and the student’s explicit report. Show the returned proposal detail and ask for confirmation before tutor_confirm_update.', { courseCode: code, from: date, to: date, eventIds: z.array(id).min(1).max(20), status: z.enum(['attended','missed','unknown']), note: z.string().max(500).optional() }, body => api('/api/tutor/updates/prepare', { method: 'POST', body: { ...body, kind: 'attendance' } }))
30
31
  tool('tutor_prepare_context', 'Proactively prepare lasting student preferences, project decisions/roles, constraints or availability learned during a conversation; do not wait for a reminder to save context. Read tutor_sources first to avoid duplicates or detect corrections. This prepares a draft, not saved memory, and calls no model. Preserve student-provided facts; dates bound temporary context. Availability is not proof of absence. Show exact wording/dates/weekdays and obtain confirmation before tutor_confirm_update.', { kind: z.enum(['preference','availability','context']), text: z.string().min(1).max(400), weekdays: z.array(z.enum(['monday','tuesday','wednesday','thursday','friday','saturday','sunday'])).max(7).optional(), startDate: date, endDate: date }, body => api('/api/tutor/updates/prepare', { method: 'POST', body }))
@@ -1,7 +1,7 @@
1
1
  import { toolAnnotations } from './tool-annotations.mjs'
2
2
  import { AsyncLocalStorage } from 'node:async_hooks'
3
3
  export const toolRequestContext = new AsyncLocalStorage()
4
- const writes = new Set('feedback_withdraw_contact feedback_submit feedback_reply feedback_withdraw_evidence feedback_react wicker_sign_out join_programme canvas_corpus_sync submit_answer set_mastery review_card add_to_deck create_flashcard review_flashcard resolve_mistake record_chapter_read save_academic_plan update_planning_objective set_course_visibility apply_changes save_calendar_link sync_calendar_link remove_calendar_link canvas_import_remote_course canvas_import_remote_course_set canvas_sync_control canvas_sync_course tutor_ask tutor_approve_action tutor_delete_conversation tutor_add_source tutor_remove_source tutor_forget_context tutor_confirm_update answer_study_diagnostic'.split(' '))
4
+ const writes = new Set('feedback_withdraw_contact feedback_submit feedback_reply feedback_withdraw_evidence feedback_react wicker_sign_out join_programme canvas_corpus_sync submit_answer set_mastery review_card add_to_deck create_flashcard review_flashcard resolve_mistake record_chapter_read save_academic_plan update_planning_objective set_course_visibility apply_changes save_calendar_link sync_calendar_link remove_calendar_link canvas_import_remote_course canvas_import_remote_course_set canvas_sync_control canvas_sync_course refresh_course_materials tutor_ask tutor_approve_action tutor_delete_conversation tutor_add_source tutor_remove_source tutor_forget_context tutor_confirm_update answer_study_diagnostic'.split(' '))
5
5
  export function requiresWriteConfirmation(name) {
6
6
  return writes.has(name) || name.startsWith('admin_') && !/^(admin_status|admin_inventory_|admin_list_|admin_estimate_)/.test(name)
7
7
  }