wicker-study-mcp 2.14.1 → 2.14.2

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
@@ -7,6 +7,29 @@ with an admin key — the whole editorial content workflow.
7
7
 
8
8
  Runs from anywhere. Nothing here needs a checkout of the application.
9
9
 
10
+ ## Finding published courses and Canvas materials
11
+
12
+ `list_courses` lists published study courses with chapters and progress; it is not
13
+ an inventory of Canvas enrolments or stored originals. `get_course` accepts the
14
+ exact published `id` from that list, not a Canvas numeric ID or course code.
15
+ An empty list or a missing published course does **not** establish that Canvas
16
+ material is absent.
17
+
18
+ For Canvas material, discover the stable course code and academic-year editions
19
+ with `canvas_corpus_status` (or find course codes in `get_academic_plan`). Call
20
+ `canvas_course_materials({courseCode, academicYear})` for the stored originals,
21
+ and `search_course({courseCode, query, academicYear})` for indexed passages.
22
+ `search_course` requires a nonblank query and at least one nonblank `courseId`,
23
+ `courseCode`, or `canonicalCourseId`; use `courseCode` even if the course has no
24
+ published study guide. Omit `academicYear` only when searching across editions.
25
+ If an inventory or search is empty, check collection status and sync logs before
26
+ claiming that Canvas itself has no material. A tool error is not an empty result.
27
+
28
+ MCP discovery includes explicit `readOnlyHint` annotations. These describe tool
29
+ behavior; they do not replace key scopes or the student's confirmation for
30
+ writes. Draft preparation, local file downloads and generation continuations
31
+ can still write even when no additional approval is needed for that step.
32
+
10
33
  ## Connect over HTTPS (recommended)
11
34
 
12
35
  For local agents and other compatible services, use **https://study.wicker.life/api/mcp**
@@ -92,7 +115,7 @@ saved key exists. No key needs to be pasted into the agent conversation.
92
115
  For a manually supplied key, the same secure bootstrap is available as:
93
116
 
94
117
  ```sh
95
- WICKER_STUDY_URL='https://study.wicker.life' WICKER_STUDY_API_KEY='wsk_…' npx -y wicker-study-mcp@2.14.1 configure
118
+ WICKER_STUDY_URL='https://study.wicker.life' WICKER_STUDY_API_KEY='wsk_…' npx -y wicker-study-mcp@2.14.2 configure
96
119
  ```
97
120
 
98
121
  ```jsonc
@@ -175,7 +198,7 @@ authoritative list of endpoints and scopes.
175
198
 
176
199
  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.
177
200
 
178
- Hosted users reconnect to discover the current tools. Optional package users update to `wicker-study-mcp@2.14.1` and restart their local MCP process. The matching workflow guide is served by `wicker_guidance` and `wicker://guidance/current`. No separate skill update is needed.
201
+ Hosted users reconnect to discover the current tools. Optional package users update to `wicker-study-mcp@2.14.2` and restart their local MCP process. The matching workflow guide is served by `wicker_guidance` and `wicker://guidance/current`. No separate skill update is needed.
179
202
 
180
203
  ### Complete original downloads and remembered context
181
204
 
@@ -237,7 +260,7 @@ Most users can switch to hosted MCP using the instructions above. If keeping the
237
260
 
238
261
  ```sh
239
262
  codex mcp remove wicker-study
240
- codex mcp add wicker-study -- npx -y wicker-study-mcp@2.14.1
263
+ codex mcp add wicker-study -- npx -y wicker-study-mcp@2.14.2
241
264
  ```
242
265
 
243
266
  Quit and reopen the Codex app, restart the CLI session, or reload the IDE extension window so the local MCP process restarts.
@@ -246,14 +269,14 @@ Quit and reopen the Codex app, restart the CLI session, or reload the IDE extens
246
269
 
247
270
  ```sh
248
271
  claude mcp remove --scope user wicker-study
249
- claude mcp add --scope user wicker-study -- npx -y wicker-study-mcp@2.14.1
272
+ claude mcp add --scope user wicker-study -- npx -y wicker-study-mcp@2.14.2
250
273
  ```
251
274
 
252
275
  Exit and restart Claude Code, then check `/mcp`. `claude mcp add` does not overwrite an existing registration. If you originally installed with `project` or `local` scope, use that same scope in both commands.
253
276
 
254
277
  ### Claude Desktop and custom configurations
255
278
 
256
- In Claude Desktop, use Settings → Developer → Edit Config. Change only the package argument in the existing `wicker-study` entry to `wicker-study-mcp@2.14.1`, preserve its other settings, then fully quit and reopen Claude Desktop. For custom Codex/Claude Code configurations with environment variables or a server URL, update the package argument in place instead of replacing the registration. Check project overrides if an older version still loads.
279
+ In Claude Desktop, use Settings → Developer → Edit Config. Change only the package argument in the existing `wicker-study` entry to `wicker-study-mcp@2.14.2`, preserve its other settings, then fully quit and reopen Claude Desktop. For custom Codex/Claude Code configurations with environment variables or a server URL, update the package argument in place instead of replacing the registration. Check project overrides if an older version still loads.
257
280
 
258
281
  The standard helper credentials stay in `~/.config/wicker-study/config.json`. These registration commands do not delete that file. After restarting, ask the agent to call `wicker_status` and `wicker_guidance` to verify connectivity and availability of the new tools. The optional companion skill is a stable discovery hint; the current guide comes from MCP.
259
282
 
@@ -261,7 +284,7 @@ Official client references: [Codex MCP configuration](https://developers.openai.
261
284
 
262
285
  ## Local study generation
263
286
 
264
- Both hosted MCP and package 2.14.1 expose `study_generation_contract`, `study_generation_sources`, `study_generation_start`, `study_generation_next`, `study_generation_submit`, `study_generation_refresh`, `study_generation_stop`, and `study_generation_add_notes`. The package is not required for this workflow; an agent can use its own model and file tools with the hosted connection.
287
+ Both hosted MCP and package 2.14.2 expose `study_generation_contract`, `study_generation_sources`, `study_generation_start`, `study_generation_next`, `study_generation_submit`, `study_generation_refresh`, `study_generation_stop`, and `study_generation_add_notes`. The package is not required for this workflow; an agent can use its own model and file tools with the hosted connection.
265
288
 
266
289
  1. Read the current contract and list the course edition’s sources. If needed, obtain originals through `prepare_original_download` and stream/verify them with the client’s file tools, or use the optional package’s download/import helpers. Inspect graphics and preserve page numbers. Add supplementary extraction as explicitly labelled local notes.
267
290
  2. Start the student-requested private run with its exact source selection, or continue a version prepared in the web UI.
package/core-tools.mjs CHANGED
@@ -1,25 +1,18 @@
1
+ import { searchCourseSchema } from './search-course-schema.mjs'
1
2
  // Shared by stdio and Streamable HTTP. Keep schemas and behavior in one place.
2
3
  export function registerCoreTools(server, { z, run, api, defaultCanvasUrl: DEFAULT_CANVAS_URL }) {
3
- const courseId = z.string().describe('Course id; discover using list_courses.')
4
+ const courseId = z.string().describe('Exact published-course id returned by list_courses; not a course code or Canvas numeric id.')
4
5
  const chapterId = z.string().describe('Chapter id.')
5
6
  server.tool('whoami', 'Who this key acts as, its scopes, programme memberships, and whether it is an administrator.', {}, run(() => api('/api/me')))
6
7
  server.tool('join_programme', 'Join a maintained programme (organisation). Only programmes whose institution domains match the student’s email can be joined.', { programmeId: z.string() }, run(({ programmeId }) => api('/api/account/programme', { method: 'POST', body: { programmeId } })))
7
- server.tool('list_courses', 'Courses with chapters and progress counts.', {}, run(() => api('/api/courses')))
8
- server.tool('get_course', 'One course: chapters, mastery items with the student’s mastery, exam papers.', { courseId }, run(({ courseId }) => api(`/api/courses/${encodeURIComponent(courseId)}`)))
8
+ server.tool('list_courses', 'List published study courses with chapters and progress counts. This is NOT the Canvas material inventory. A missing course does not mean its Canvas materials are absent. Use canvas_corpus_status to discover Canvas editions, then canvas_course_materials and search_course with courseCode.', {}, run(() => api('/api/courses')))
9
+ server.tool('get_course', 'Read one published study course: chapters, mastery items with the student’s mastery, and exam papers. courseId must be an exact id from list_courses. For Canvas-only courses or originals, use canvas_course_materials and search_course with courseCode; a missing published course does not imply missing Canvas materials.', { courseId }, run(({ courseId }) => api(`/api/courses/${encodeURIComponent(courseId)}`)))
9
10
  server.tool('get_chapter', 'Chapter markdown content. relPath opens a linked file or sub-page inside the chapter folder.', { courseId, chapterId, relPath: z.string().optional() },
10
11
  run(({ courseId, chapterId, relPath }) => api(`/api/chapter/${encodeURIComponent(courseId)}/${encodeURIComponent(chapterId)}${relPath ? '/' + relPath.split('/').map(encodeURIComponent).join('/') : ''}`)))
11
12
  server.tool('get_course_outline', 'Heading outline of every chapter in a course.', { courseId }, run(({ courseId }) => api(`/api/course-toc/${encodeURIComponent(courseId)}`)))
12
- server.tool('list_materials', 'Files in a course knowledge base (markdown, PDFs, images, code).', { courseId }, run(({ courseId }) => api('/api/materials', { query: { courseId } })))
13
- 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.', {
14
- courseId: courseId.optional(),
15
- courseCode: z.string().optional().describe('Stable course code, for example BCS1540. Use this when querying Canvas editions.'),
16
- canonicalCourseId: z.string().optional().describe('Stable corpus course identity returned by an earlier search.'),
17
- academicYear: z.string().optional().describe('Exact edition such as 2025-2026.'),
18
- sourceType: z.enum(['syllabus', 'requirements', 'slides', 'pages', 'assessments', 'activities', 'readings', 'materials']).optional(),
19
- includeHistorical: z.boolean().optional().describe('Search older editions when no exact year is requested; defaults to true.'),
20
- query: z.string(),
21
- limit: z.number().int().min(1).max(20).optional()
22
- }, run((args) => api('/api/retrieve', { method: 'POST', body: args })))
13
+ server.tool('list_materials', 'Files in a published course knowledge base (markdown, PDFs, images, code). For the separate Canvas original inventory, use canvas_course_materials with courseCode.', { courseId }, run(({ courseId }) => api('/api/materials', { query: { courseId } })))
14
+ server.tool('search_course', 'Search within a course across published material and authorised Canvas snapshots. Requires query and at least one of courseId, courseCode, or canonicalCourseId. Prefer courseCode for Canvas material, including courses absent from list_courses. Use canvas_corpus_status or get_academic_plan to discover codes. Results identify the 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.', searchCourseSchema,
15
+ run(args => api('/api/retrieve', { method: 'POST', body: args })))
23
16
  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.', {
24
17
  query: z.string(),
25
18
  academicYear: z.string().optional().describe('Exact academic year such as 2026-2027. Defaults to the active programme year.'),
package/guidance.md CHANGED
@@ -77,6 +77,29 @@ separately in Settings → API access when it should no longer work anywhere.
77
77
  - Discover everything with `GET /api/agent/manifest` — it lists every endpoint,
78
78
  its scope, and body shapes. Read it first when unsure.
79
79
 
80
+ ## Finding published courses and Canvas materials
81
+
82
+ `list_courses` lists published study courses with chapters and progress; it is not
83
+ an inventory of Canvas enrolments or stored originals. `get_course` accepts the
84
+ exact published `id` from that list, not a Canvas numeric ID or course code.
85
+ An empty list or a missing published course does **not** establish that Canvas
86
+ material is absent.
87
+
88
+ For Canvas material, discover the stable course code and academic-year editions
89
+ with `canvas_corpus_status` (or find course codes in `get_academic_plan`). Call
90
+ `canvas_course_materials({courseCode, academicYear})` for the stored originals,
91
+ and `search_course({courseCode, query, academicYear})` for indexed passages.
92
+ `search_course` requires a nonblank query and at least one nonblank `courseId`,
93
+ `courseCode`, or `canonicalCourseId`; use `courseCode` even if the course has no
94
+ published study guide. Omit `academicYear` only when searching across editions.
95
+ If an inventory or search is empty, check collection status and sync logs before
96
+ claiming that Canvas itself has no material. A tool error is not an empty result.
97
+
98
+ MCP discovery includes explicit `readOnlyHint` annotations. These describe tool
99
+ behavior; they do not replace key scopes or the student's confirmation for
100
+ writes. Draft preparation, local file downloads and generation continuations
101
+ can still write even when no additional approval is needed for that step.
102
+
80
103
  ## Answering a question about a current course
81
104
 
82
105
  Route the question to the source that actually holds the answer, and say when a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wicker-study-mcp",
3
- "version": "2.14.1",
3
+ "version": "2.14.2",
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",
@@ -40,7 +40,9 @@
40
40
  "vendor/",
41
41
  "scripts/",
42
42
  "README.md",
43
- "core-tools.mjs"
43
+ "core-tools.mjs",
44
+ "tool-annotations.mjs",
45
+ "search-course-schema.mjs"
44
46
  ],
45
47
  "dependencies": {
46
48
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod/v4'
2
+
3
+ const identifiers = ['courseId', 'courseCode', 'canonicalCourseId']
4
+ const identity = description => z.string().regex(/\S/, 'A course identifier must not be blank.').optional().describe(description)
5
+ // A refinement alone is not emitted into JSON Schema. Publish the matching
6
+ // anyOf alongside runtime validation so discovery and tools/call agree.
7
+ export const searchCourseSchema = z.object({
8
+ courseId: identity('Exact published-course id returned by list_courses; not a Canvas numeric id or a course code.'),
9
+ courseCode: identity('Stable course code, e.g. BCS1540. Preferred for Canvas material, even when list_courses has no entry.'),
10
+ canonicalCourseId: identity('Stable corpus course identity returned by an earlier search.'),
11
+ academicYear: z.string().optional().describe('Exact edition such as 2025-2026.'),
12
+ sourceType: z.enum(['syllabus', 'requirements', 'slides', 'pages', 'assessments', 'activities', 'readings', 'materials']).optional(),
13
+ includeHistorical: z.boolean().optional().describe('Search older editions when no exact year is requested; defaults to true.'),
14
+ query: z.string().regex(/\S/, 'A search query must not be blank.'),
15
+ limit: z.number().int().min(1).max(20).optional()
16
+ }).refine(args => identifiers.some(key => Boolean(args[key])), {
17
+ message: 'Provide at least one of courseId, courseCode, or canonicalCourseId. Use courseCode for Canvas material.'
18
+ }).meta({ anyOf: identifiers.map(key => ({ required: [key] })) })
package/study-tools.mjs CHANGED
@@ -20,7 +20,7 @@ export function registerStudyTools(server, { z, run, api, defaultCanvasUrl }) {
20
20
  tool('study_generation_add_notes', 'Save student-authorised local extraction or notes as a clearly labelled private source. Preserve original page numbers and identify source filenames in text. Describe relevant graphs, tables, diagrams and relationships accurately; distinguish inference from source facts. Does not alter the original Canvas document. Use its returned id in sourceKeys.', { ...courseIdentity, title: z.string().max(150), pages: z.array(z.object({page: z.number().int().positive().nullable(), text: z.string().max(300000)})).min(1).max(500), confirmed: z.literal(true) }, body => api('/api/study-versions/local/notes', { method: 'POST', body }))
21
21
  tool('read_course_source', 'Read 12 indexed passages from an authorised Canvas document returned by search_course. Follow nextOffset to read beyond the snippet, including later paper-list pages. Retains edition, path and page citations; never widens source access.', { assetId: id, courseCode: z.string().min(1).max(40), offset: z.number().int().min(0).max(100000).optional() }, args => api('/api/retrieve/source', { query: args }))
22
22
  tool('prepare_original_download', 'Prepare a short-lived direct HTTP download for one exact asset from canvas_course_materials. Returns a URL, file-scoped header, expiry, original size and SHA-256. Use your own HTTP/file tools to stream it to disk outside MCP and verify the complete file; no npm helper is required. The header is sensitive: keep it out of chat/logs/history and never forward it to another host. Supports resumable Range requests; does not execute or change material.', { assetId: z.string().min(1).max(160) }, ({ assetId }) => api(`/api/corpus/assets/${encodeURIComponent(assetId)}/download-ticket`))
23
- tool('canvas_course_materials', 'List indexed originals and source editions for one course. Select an exact academic year for a retake; use read_course_source for extracted text or prepare_original_download for a direct transfer of the complete unchanged file.', { courseCode: z.string().min(1).max(40), academicYear: z.string().max(20).optional() }, args => api('/api/corpus/materials', { query: args }))
23
+ tool('canvas_course_materials', 'List the separate Canvas original-material inventory and source editions for a stable courseCode, including courses absent from list_courses. Discover codes and editions with canvas_corpus_status or get_academic_plan. Select an exact academic year for a retake; use read_course_source for extracted text or prepare_original_download for a direct transfer of the complete unchanged file.', { courseCode: z.string().min(1).max(40), academicYear: z.string().max(20).optional() }, args => api('/api/corpus/materials', { query: args }))
24
24
  tool('canvas_groups', 'Read your Canvas group memberships across courses and global groups. Filter by courseCode and academicYear to keep retake teams separate. Pass a groupId from these memberships to read teammates; include its canvasUrl if several Canvas hosts are connected. A failed roster is unknown, not an empty team. Read-only: never joins or changes groups.', {canvasUrl,courseCode:code,academicYear:z.string().max(20).optional(),scope:z.enum(['all','course','global']).optional(),groupId:z.string().regex(/^\d{1,20}$/).optional(),refresh:z.boolean().optional()}, ({refresh,...args}) => api('/api/integrations/canvas/groups',{query:{...args,refresh:refresh?'1':undefined}}))
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 }))
@@ -0,0 +1,33 @@
1
+ // Transport-independent MCP behavior hints. Confirmation policy is separate:
2
+ // drafting, local downloads and generation continuations can write without
3
+ // requiring a new confirmation on every step. Never infer reads from that policy.
4
+ const reads = new Set(`
5
+ whoami wicker_status wicker_guidance
6
+ list_courses get_course get_chapter get_course_outline list_materials search_course
7
+ search_regulations list_regulation_sources canvas_corpus_status
8
+ list_questions get_practice_queue get_progress list_flashcards list_due_cards
9
+ list_mistakes list_mock_sessions get_mock_session get_academic_plan get_planning_context
10
+ list_known_programmes get_calendar get_activity get_account_summary get_study_briefing
11
+ canvas_updates preview_calendar study_generation_contract study_generation_sources
12
+ read_course_source read_original_chunk canvas_course_materials
13
+ canvas_groups canvas_assignment_detail canvas_sync_logs tutor_history tutor_conversation
14
+ tutor_sources get_study_work get_attendance get_course_obligations get_study_readiness
15
+ get_weekly_review canvas_search_announcements get_study_diagnostic feedback_list feedback_read
16
+ canvas_course_requirements canvas_list_remote_courses canvas_list_remote_course_modules
17
+ admin_status admin_inventory_course_folder admin_list_canvas_courses
18
+ admin_list_canvas_course_modules admin_list_editorial_workspace admin_estimate_course_generation
19
+ admin_list_members admin_list_materials admin_list_flashcards admin_list_questions admin_list_programmes
20
+ `.trim().split(/\s+/))
21
+
22
+ const externalReads = new Set(`get_calendar get_study_briefing canvas_updates preview_calendar
23
+ canvas_groups canvas_assignment_detail canvas_course_requirements canvas_list_remote_courses
24
+ canvas_list_remote_course_modules admin_list_canvas_courses admin_list_canvas_course_modules`.split(/\s+/))
25
+ const additiveWrites = new Set(`feedback_prepare feedback_submit feedback_reply
26
+ study_generation_start study_generation_add_notes tutor_prepare_context tutor_prepare_attendance_update
27
+ tutor_add_source create_flashcard record_chapter_read download_course_original prepare_original_download`.split(/\s+/))
28
+
29
+ export function toolAnnotations(name) {
30
+ if (reads.has(name)) return { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: externalReads.has(name) }
31
+ // Unclassified additions retain MCP's conservative mutation defaults.
32
+ return { readOnlyHint: false, destructiveHint: !additiveWrites.has(name), idempotentHint: false, openWorldHint: true }
33
+ }
@@ -1,3 +1,4 @@
1
+ import { toolAnnotations } from './tool-annotations.mjs'
1
2
  import { AsyncLocalStorage } from 'node:async_hooks'
2
3
  export const toolRequestContext = new AsyncLocalStorage()
3
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(' '))
@@ -5,13 +6,13 @@ export function requiresWriteConfirmation(name) {
5
6
  return writes.has(name) || name.startsWith('admin_') && !/^(admin_status|admin_inventory_|admin_list_|admin_estimate_)/.test(name)
6
7
  }
7
8
  export function installWriteConfirmation(server, z) {
8
- const register = server.tool.bind(server)
9
+ const register = (name, description, inputSchema, annotations, handler) => server.registerTool(name, { description, inputSchema, annotations }, handler)
9
10
  server.tool = (name, description, schema, handler) => {
10
11
  const handle = handler
11
12
  handler = (args, extra) => toolRequestContext.run({ tool: name, confirmed: args.confirmed === true }, () => handle(args, extra))
12
- if (!requiresWriteConfirmation(name)) return register(name, description, schema, handler)
13
+ if (!requiresWriteConfirmation(name)) return register(name, description, schema, toolAnnotations(name), handler)
13
14
  const dryRun = name === 'admin_sync_course_folder'
14
- return register(name, `${description} Requires explicit student confirmation for this individual write; prior approvals do not authorise later writes.`, { ...schema, confirmed: dryRun ? z.literal(true).optional() : z.literal(true) }, (args, extra) => {
15
+ return register(name, `${description} Requires explicit student confirmation for this individual write; prior approvals do not authorise later writes.`, { ...schema, confirmed: dryRun ? z.literal(true).optional() : z.literal(true) }, toolAnnotations(name), (args, extra) => {
15
16
  if (!(dryRun && args.dryRun !== false) && args.confirmed !== true) return { isError: true, content: [{ type: 'text', text: 'Show the exact change and obtain explicit confirmation before this write.' }] }
16
17
  return handler(args, extra)
17
18
  })