wicker-study-mcp 2.9.0 → 2.10.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 +12 -2
- package/feedback-tools.mjs +126 -0
- package/package.json +2 -1
- package/server.mjs +4 -2
- package/write-confirmation.mjs +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ the MCP server.
|
|
|
17
17
|
For a manually supplied key, the same secure bootstrap is available as:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
WICKER_STUDY_URL='https://study.wicker.life' WICKER_STUDY_API_KEY='wsk_…' npx -y wicker-study-mcp@2.
|
|
20
|
+
WICKER_STUDY_URL='https://study.wicker.life' WICKER_STUDY_API_KEY='wsk_…' npx -y wicker-study-mcp@2.10.0 configure
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
```jsonc
|
|
@@ -100,7 +100,7 @@ authoritative list of endpoints and scopes.
|
|
|
100
100
|
|
|
101
101
|
Direct context reads do not call the model. `tutor_ask` uses the student's AI allowance and can prepare attendance changes, assignment/catch-up trackers, group milestones, focused practice, diagnostics and rubric-based draft reviews. Reuse conversation IDs. Approve only the exact proposal the student reviewed; receipts prevent double application. No tool sends email or submits assignments to Canvas. Personal completion is separate from Canvas submission status.
|
|
102
102
|
|
|
103
|
-
Update an installed client to `wicker-study-mcp@2.
|
|
103
|
+
Update an installed client to `wicker-study-mcp@2.10.0` and restart its MCP connection to discover the new tools. The companion skill is served at [SKILL.md](https://study.wicker.life/skills/wicker-study/SKILL.md); re-download it to update an existing copy.
|
|
104
104
|
|
|
105
105
|
## Licence
|
|
106
106
|
|
|
@@ -135,3 +135,13 @@ Do not infer or store sensitive preferences from course material or third-party
|
|
|
135
135
|
## AI activity log
|
|
136
136
|
|
|
137
137
|
Settings → AI activity (`/app/settings?tab=activity`) shows API-key requests from this release onward, with read/write/prepare filters, outcome, duration, tool/client label and confirmed-review reference. The MCP tags requests automatically. One tool may make several HTTP requests; local actions that never reach the platform are not logged. Client labels and client-reported confirmation are not independent proof of approval. The server records confirmed prepared-review IDs separately. Arguments, query text, responses and credentials are excluded. Activity is private to the account, included in data export, and removed by account-data erasure.
|
|
138
|
+
|
|
139
|
+
Automatic Canvas refresh is configurable in Settings → Connections → Manage: on/off, update frequency (15 minutes to daily), material frequency (hourly to weekly), and studying/completed status. Defaults remain 30 minutes and six hours. Course selection is re-evaluated at least hourly across period boundaries. Summer/break monitoring retains the ending year and discovers upcoming next-year courses, selecting the latest eligible edition per course. Completion or no active programme pauses background collection; manual refresh remains available. These preferences require a signed-in browser, not an MCP write.
|
|
140
|
+
|
|
141
|
+
## Feedback (2.10)
|
|
142
|
+
|
|
143
|
+
Use `feedback_prepare` to create an exact report preview, then show it and obtain explicit user approval before `feedback_submit` with the unchanged draft ID and revision. `feedback_list` and `feedback_read` expose only the user’s reports and public replies. `feedback_reply`, `feedback_withdraw_evidence`, and `feedback_react` each require a fresh `confirmed:true` after individual approval. A prepared draft is not a submitted report. Never attach chat or source excerpts without the user choosing to share them. Feedback is separate from remembered Tutor context.
|
|
144
|
+
|
|
145
|
+
Students can follow reports and withdraw evidence at `/app/feedback`; authorized staff review them at `/app/admin/feedback`. See [the operations guide](../docs/FEEDBACK.md) for data boundaries and retention.
|
|
146
|
+
|
|
147
|
+
Contact sharing is optional per report: use `shareContactEmail:true` only when the student chooses it and show the returned address in the preview. `feedback_withdraw_contact` stops sharing it after fresh confirmation. Reports show receipt, investigation and completion updates with public comments; AI-assisted replies are labeled and reviewed by the team.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export function registerFeedbackTools(server, { z, run, api }) {
|
|
2
|
+
const id = z.string().min(1).max(180),
|
|
3
|
+
subject = z.object({
|
|
4
|
+
kind: z
|
|
5
|
+
.enum([
|
|
6
|
+
"general",
|
|
7
|
+
"answer",
|
|
8
|
+
"material",
|
|
9
|
+
"assignment",
|
|
10
|
+
"announcement",
|
|
11
|
+
"sync",
|
|
12
|
+
"attendance",
|
|
13
|
+
"credits",
|
|
14
|
+
"practice",
|
|
15
|
+
])
|
|
16
|
+
.optional(),
|
|
17
|
+
route: z.string().max(500).optional(),
|
|
18
|
+
conversationId: id.optional(),
|
|
19
|
+
answerId: id.optional(),
|
|
20
|
+
answerRevision: id.optional(),
|
|
21
|
+
courseCode: id.optional(),
|
|
22
|
+
academicYear: id.optional(),
|
|
23
|
+
assetId: id.optional(),
|
|
24
|
+
itemId: id.optional(),
|
|
25
|
+
jobId: id.optional(),
|
|
26
|
+
});
|
|
27
|
+
const tool = (name, description, schema, handler) =>
|
|
28
|
+
server.tool(name, description, schema, run(handler));
|
|
29
|
+
tool(
|
|
30
|
+
"feedback_prepare",
|
|
31
|
+
"Prepare an encrypted feedback preview. This does NOT submit it. Show the complete returned preview and ask explicit confirmation. Include only the user’s intended feedback; excerpts require their consent. This report is separate from Tutor memory.",
|
|
32
|
+
{
|
|
33
|
+
category: z.enum([
|
|
34
|
+
"incorrect",
|
|
35
|
+
"outdated",
|
|
36
|
+
"missing",
|
|
37
|
+
"source",
|
|
38
|
+
"slow",
|
|
39
|
+
"broken",
|
|
40
|
+
"confusing",
|
|
41
|
+
"accessibility",
|
|
42
|
+
"suggestion",
|
|
43
|
+
"other",
|
|
44
|
+
"wrong-edition",
|
|
45
|
+
"incomplete-extraction",
|
|
46
|
+
"broken-download",
|
|
47
|
+
"ignored-context",
|
|
48
|
+
"too-wordy",
|
|
49
|
+
"wrong-action",
|
|
50
|
+
]),
|
|
51
|
+
note: z.string().max(4000),
|
|
52
|
+
shareContactEmail: z
|
|
53
|
+
.boolean()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe(
|
|
56
|
+
"Share the verified account email only if the user explicitly opts in. The preview shows the exact address.",
|
|
57
|
+
),
|
|
58
|
+
subject: subject.optional(),
|
|
59
|
+
evidence: z
|
|
60
|
+
.array(
|
|
61
|
+
z.object({
|
|
62
|
+
label: z.string().max(100),
|
|
63
|
+
mediaType: z.literal("text/plain"),
|
|
64
|
+
content: z.string().max(12000),
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
.max(5)
|
|
68
|
+
.optional(),
|
|
69
|
+
},
|
|
70
|
+
(args) => api("/api/feedback/drafts", { method: "POST", body: args }),
|
|
71
|
+
);
|
|
72
|
+
tool(
|
|
73
|
+
"feedback_submit",
|
|
74
|
+
"Submit exactly the reviewed feedback draft. Obtain fresh explicit user confirmation for this report; pass its unchanged revision. Retrying the same confirmed draft returns the same receipt.",
|
|
75
|
+
{ draftId: id, revision: id },
|
|
76
|
+
(args) => api("/api/feedback/reports", { method: "POST", body: args }),
|
|
77
|
+
);
|
|
78
|
+
tool(
|
|
79
|
+
"feedback_list",
|
|
80
|
+
"Read the user’s submitted feedback and public review status. Private administrator notes are excluded.",
|
|
81
|
+
{ before: id.optional() },
|
|
82
|
+
(args) => api("/api/feedback/reports", { query: args }),
|
|
83
|
+
);
|
|
84
|
+
tool(
|
|
85
|
+
"feedback_read",
|
|
86
|
+
"Read one owned feedback report, public replies and attachment metadata. Does not read any referenced chat or private course file.",
|
|
87
|
+
{ reportId: id },
|
|
88
|
+
(args) => api(`/api/feedback/reports/${encodeURIComponent(args.reportId)}`),
|
|
89
|
+
);
|
|
90
|
+
tool(
|
|
91
|
+
"feedback_reply",
|
|
92
|
+
"Add the exact user-approved follow-up to an owned report.",
|
|
93
|
+
{ reportId: id, body: z.string().min(1).max(4000) },
|
|
94
|
+
(args) =>
|
|
95
|
+
api(
|
|
96
|
+
`/api/feedback/reports/${encodeURIComponent(args.reportId)}/replies`,
|
|
97
|
+
{ method: "POST", body: args },
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
tool(
|
|
101
|
+
"feedback_withdraw_evidence",
|
|
102
|
+
"Permanently withdraw a shared attachment from an owned feedback report after explicit confirmation. Does not delete the original course or Tutor file.",
|
|
103
|
+
{ reportId: id, evidenceId: id },
|
|
104
|
+
(args) =>
|
|
105
|
+
api(
|
|
106
|
+
`/api/feedback/reports/${encodeURIComponent(args.reportId)}/evidence/${encodeURIComponent(args.evidenceId)}`,
|
|
107
|
+
{ method: "DELETE", body: args },
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
tool(
|
|
111
|
+
"feedback_react",
|
|
112
|
+
"Record or remove the student’s explicitly chosen reaction to one exact Tutor answer revision.",
|
|
113
|
+
{ subject, value: z.enum(["helpful", "not-helpful"]).nullable() },
|
|
114
|
+
(args) => api("/api/feedback/reactions", { method: "POST", body: args }),
|
|
115
|
+
);
|
|
116
|
+
tool(
|
|
117
|
+
"feedback_withdraw_contact",
|
|
118
|
+
"Stop sharing the account email on this report, after explicit user confirmation.",
|
|
119
|
+
{ reportId: id },
|
|
120
|
+
(args) =>
|
|
121
|
+
api(
|
|
122
|
+
`/api/feedback/reports/${encodeURIComponent(args.reportId)}/contact`,
|
|
123
|
+
{ method: "DELETE", body: args },
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicker-study-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"server.mjs",
|
|
32
32
|
"study-tools.mjs",
|
|
33
|
+
"feedback-tools.mjs",
|
|
33
34
|
"write-confirmation.mjs",
|
|
34
35
|
"config.mjs",
|
|
35
36
|
"authorize.mjs",
|
package/server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { registerFeedbackTools } from './feedback-tools.mjs'
|
|
2
3
|
import { installWriteConfirmation, toolRequestContext } from './write-confirmation.mjs'
|
|
3
4
|
import { registerStudyTools } from './study-tools.mjs'
|
|
4
5
|
// Wicker Study MCP server — a thin stdio wrapper over the HTTP API so agents
|
|
@@ -65,7 +66,7 @@ async function apiResponse(path, { method = 'GET', body, query, timeoutMs } = {}
|
|
|
65
66
|
const response = await fetch(url, {
|
|
66
67
|
...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
67
68
|
method,
|
|
68
|
-
headers: { authorization: `Bearer ${requireKey()}`, accept: 'application/json', 'x-wicker-client': 'wicker-study-mcp 2.
|
|
69
|
+
headers: { authorization: `Bearer ${requireKey()}`, accept: 'application/json', 'x-wicker-client': 'wicker-study-mcp 2.10.0', ...(toolRequestContext.getStore() ? { 'x-wicker-tool': toolRequestContext.getStore().tool, 'x-wicker-confirmed': String(toolRequestContext.getStore().confirmed) } : {}), ...(body !== undefined ? { 'content-type': 'application/json' } : {}) },
|
|
69
70
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
70
71
|
})
|
|
71
72
|
if (!response.ok) {
|
|
@@ -92,10 +93,11 @@ const json = (value) => ({ content: [{ type: 'text', text: typeof value === 'str
|
|
|
92
93
|
const failed = (error) => ({ isError: true, content: [{ type: 'text', text: error.message }] })
|
|
93
94
|
const run = (fn) => async (args) => { try { return json(await fn(args)) } catch (error) { return failed(error) } }
|
|
94
95
|
|
|
95
|
-
const server = new McpServer({ name: 'wicker-study', version: '2.
|
|
96
|
+
const server = new McpServer({ name: 'wicker-study', version: '2.10.0' })
|
|
96
97
|
installWriteConfirmation(server, z)
|
|
97
98
|
const courseId = z.string().describe('Course id (e.g. "sec"). Use list_courses to discover ids.')
|
|
98
99
|
const chapterId = z.string().describe('Chapter id (e.g. "02").')
|
|
100
|
+
registerFeedbackTools(server, { z, run, api })
|
|
99
101
|
registerStudyTools(server, { z, run, api, defaultCanvasUrl: DEFAULT_CANVAS_URL })
|
|
100
102
|
|
|
101
103
|
const COURSE_SOURCE_EXTENSIONS = new Set(['.pdf', '.ppt', '.pptx', '.doc', '.docx', '.txt', '.md', '.csv', '.tex', '.m', '.py', '.r', '.html', '.htm', '.png', '.jpg', '.jpeg', '.webp'])
|
package/write-confirmation.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
2
|
export const toolRequestContext = new AsyncLocalStorage()
|
|
3
|
-
const writes = new Set('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(' '))
|
|
3
|
+
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
4
|
export function requiresWriteConfirmation(name) {
|
|
5
5
|
return writes.has(name) || name.startsWith('admin_') && !/^(admin_status|admin_inventory_|admin_list_|admin_estimate_)/.test(name)
|
|
6
6
|
}
|