somtoday-mcp 0.3.0 → 0.3.1

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 (2) hide show
  1. package/dist/server.js +70 -19
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -43,6 +43,13 @@ const numericId = () => z
43
43
  .transform(String);
44
44
  /** Free-text or numeric reference (subject name/abbreviation/uuid, page slug). */
45
45
  const textOrNumber = () => z.union([z.string(), z.number()]).transform(String);
46
+ /** Every tool only reads the parent portal; nothing is changed in Somtoday. */
47
+ const READ_ONLY = {
48
+ readOnlyHint: true,
49
+ destructiveHint: false,
50
+ idempotentHint: true,
51
+ openWorldHint: true,
52
+ };
46
53
  const leerlingArg = () => numericId()
47
54
  .optional()
48
55
  .describe("Numeric leerling id. Required when more than one child is linked.");
@@ -53,16 +60,19 @@ export function createServer(session = createDefaultSession()) {
53
60
  version: packageVersion(),
54
61
  });
55
62
  server.registerTool("whoami", {
63
+ annotations: READ_ONLY,
56
64
  title: "Who am I",
57
- description: "School label, numeric leerling ids, and which parent-portal sections are visible. No tokens.",
65
+ description: "Start here: the school label, the numeric leerling ids to pass as leerlingId to other tools, and which parent-portal sections are visible. Use children for full profiles. No tokens.",
58
66
  inputSchema: {},
59
67
  }, async () => whoamiTool(ctx));
60
68
  server.registerTool("lessons", {
69
+ annotations: READ_ONLY,
61
70
  title: "Lessons",
62
71
  description: "Timetable for one Amsterdam date, plus whether that date is a holiday. Each lesson has type (ROOSTERTOETS or EXAMEN marks a test in the timetable) and uitgevallen=true when cancelled. Homework and the test topic per lesson are not here: call homework for the same date.",
63
72
  inputSchema: { date: dateArg, leerlingId: leerlingArg() },
64
73
  }, async ({ date, leerlingId }) => lessonsTool(ctx, { date, leerlingId }));
65
74
  server.registerTool("schedule", {
75
+ annotations: READ_ONLY,
66
76
  title: "Schedule",
67
77
  description: "Timetable for a date range, per day: lessons, tests and other appointments with time, lesson hour, subject, room, teachers, type (e.g. ROOSTERTOETS, EXAMEN, OUDERAVOND), lesson notes, changes and cancellations (wijziging, uitgevallen=true, meldingen with real warnings only; the routine 'Inschrijving in orde.' notice is dropped), attachments, and holidays. Every day has items (empty array on a free day). Defaults to today plus 6 days; at most 42 days.",
68
78
  inputSchema: {
@@ -71,6 +81,7 @@ export function createServer(session = createDefaultSession()) {
71
81
  },
72
82
  }, async (args) => scheduleTool(ctx, args));
73
83
  server.registerTool("homework", {
84
+ annotations: READ_ONLY,
74
85
  title: "Homework / study planner",
75
86
  description: "What to do and learn on which day: every studiewijzer item (lesson, day and week items) for a date range, grouped by day, with description, learning goals, time estimate, test type, teacher note, attachments (file links), external links, hand-in window and status, and the done tick. Also lists hand-in deadlines per day. Defaults to today plus 6 days; at most 42 days." +
76
87
  " Every item lists bijlagen (fileUrl, readable with download_attachment) and externeMaterialen; empty means the teacher attached nothing (lesmateriaal=true is only a flag, there is no separate material call).",
@@ -80,6 +91,7 @@ export function createServer(session = createDefaultSession()) {
80
91
  },
81
92
  }, async (args) => homeworkTool(ctx, args));
82
93
  server.registerTool("assignments", {
94
+ annotations: READ_ONLY,
83
95
  title: "Assignments (hand-ins)",
84
96
  description: "All hand-in assignments with their window, status (AANKOMEND, IN_TE_LEVEREN, IN_AFWACHTING, AKKOORD), teLaat (always a boolean: true only for an open hand-in past its deadline), description, attachments and links. status=open keeps upcoming and to-do ones, and by default hides past-due items where no digital hand-in is expected (inleveringenVerwacht 0, e.g. on paper; count in verborgenZonderInlevering); includeNoHandIn=true keeps them. Near-duplicates (same subject, topic, window and status, e.g. published to two groups) are merged by default, with the other ids in duplicaatToekenningIds; dedupe=false lists them separately. Use toekenningId with assignment_detail." +
85
97
  " Every item lists bijlagen (fileUrl, readable with download_attachment) and externeMaterialen; empty means the teacher attached nothing (lesmateriaal=true is only a flag, there is no separate material call).",
@@ -97,6 +109,7 @@ export function createServer(session = createDefaultSession()) {
97
109
  },
98
110
  }, async (args) => assignmentsTool(ctx, args));
99
111
  server.registerTool("assignment_detail", {
112
+ annotations: READ_ONLY,
100
113
  title: "Assignment detail",
101
114
  description: "One hand-in assignment as the parent portal shows it: instruction text, learning goals, files (fileUrl) and links, hand-in window, status, late flag and group. Submitted files and teacher reactions are only visible to the student in Somtoday, so they are not returned.",
102
115
  inputSchema: {
@@ -105,6 +118,7 @@ export function createServer(session = createDefaultSession()) {
105
118
  },
106
119
  }, async (args) => assignmentDetailTool(ctx, args));
107
120
  server.registerTool("study_material", {
121
+ annotations: READ_ONLY,
108
122
  title: "Study material",
109
123
  description: "Studiemateriaal per subject: lesson material from the study planner (by date), year attachments by folder (file links), external links, and methods/publisher links (book fund). Without arguments: the subjects that have material, plus personal learning products when the school allows it. vak = name, abbreviation or uuid; all=true fetches every subject." +
110
124
  " Every item lists bijlagen (fileUrl, readable with download_attachment) and externeMaterialen; empty means the teacher attached nothing (lesmateriaal=true is only a flag, there is no separate material call).",
@@ -120,15 +134,23 @@ export function createServer(session = createDefaultSession()) {
120
134
  },
121
135
  }, async (args) => studyMaterialTool(ctx, args));
122
136
  server.registerTool("grades", {
137
+ annotations: READ_ONLY,
123
138
  title: "Grades",
124
- description: "Newest progress-dossier results (capped at 30) and subject averages.",
139
+ description: "Quick grade summary: the newest progress-dossier results (capped at 30) plus the average per subject. Use grades_latest for more or exam (SE) results, grades_overview for a whole school year, subject_grades for one subject in depth, and exam_grades for the PTA/SE dossier.",
125
140
  inputSchema: { leerlingId: leerlingArg() },
126
141
  }, async ({ leerlingId }) => gradesTool(ctx, { leerlingId }));
127
142
  server.registerTool("grades_latest", {
143
+ annotations: READ_ONLY,
128
144
  title: "Latest grades",
129
- description: "Newest results first, with subject, column, description, grade, weight, test type, period, date, remarks and resit info. limit defaults to 50 (max 1000, paged). dossier=examen gives school-exam (SE) results. Also the next publication moment when the school uses one.",
145
+ description: 'Newest results first, with subject, column, description, grade, weight, test type, period, date, remarks and resit info. limit defaults to 50 (max 1000, paged). dossier=examen gives school-exam (SE) results. Also the next publication moment when the school uses one. Use this for "what grades came in recently"; use grades for a short summary with averages, subject_grades for all columns of one subject.',
130
146
  inputSchema: {
131
- limit: z.number().int().min(1).max(1000).optional(),
147
+ limit: z
148
+ .number()
149
+ .int()
150
+ .min(1)
151
+ .max(1000)
152
+ .optional()
153
+ .describe("Number of newest results to return. Default 50, max 1000."),
132
154
  dossier: z
133
155
  .enum(["voortgang", "examen"])
134
156
  .optional()
@@ -137,8 +159,9 @@ export function createServer(session = createDefaultSession()) {
137
159
  },
138
160
  }, async (args) => gradesLatestTool(ctx, args));
139
161
  server.registerTool("grades_overview", {
162
+ annotations: READ_ONLY,
140
163
  title: "Grade overview",
141
- description: "The full grade overview (cijferoverzicht) for a school year: every subject with every result per period, report grades, weights and remarks. Defaults to the current placement; schooljaar like 2025/2026 picks another year.",
164
+ description: "The full grade overview (cijferoverzicht) for a school year: every subject with every result per period, report grades, weights and remarks. Defaults to the current placement; schooljaar like 2025/2026 picks another year. Use this for a whole-year report view or earlier years; use subject_grades for one subject, grades_latest for recent results.",
142
165
  inputSchema: {
143
166
  schooljaar: z
144
167
  .string()
@@ -148,19 +171,24 @@ export function createServer(session = createDefaultSession()) {
148
171
  },
149
172
  }, async (args) => gradesOverviewTool(ctx, args));
150
173
  server.registerTool("subject_grades", {
174
+ annotations: READ_ONLY,
151
175
  title: "Subject grades",
152
- description: "Everything for one subject: every result column (tests, assignments, period and report averages, SE averages) with weights and remarks, column definitions (also not-yet-graded ones), and the parts of composite tests. Both progress and exam dossier unless dossier is set. Without vak: the list of subjects.",
176
+ description: "Everything for one subject: every result column (tests, assignments, period and report averages, SE averages) with weights and remarks, column definitions (also not-yet-graded ones), and the parts of composite tests. Both progress and exam dossier unless dossier is set. Without vak: the list of subjects. Use this to drill into one subject; use grades_overview for all subjects at once.",
153
177
  inputSchema: {
154
178
  vak: textOrNumber()
155
179
  .optional()
156
180
  .describe("Subject name, abbreviation or uuid."),
157
- dossier: z.enum(["voortgang", "examen"]).optional(),
181
+ dossier: z
182
+ .enum(["voortgang", "examen"])
183
+ .optional()
184
+ .describe("voortgang = progress dossier, examen = school-exam (SE) dossier. Default both."),
158
185
  leerlingId: leerlingArg(),
159
186
  },
160
187
  }, async (args) => subjectGradesTool(ctx, args));
161
188
  server.registerTool("exam_grades", {
189
+ annotations: READ_ONLY,
162
190
  title: "Exam dossier (SE)",
163
- description: "School-exam dossier (PTA/SE) overview: per subject the SE average, every SE result, tests still to come, and test-type averages. Says so when the child has no exam dossier yet.",
191
+ description: "School-exam dossier (PTA/SE) overview: per subject the SE average, every SE result, tests still to come, and test-type averages. Says so when the child has no exam dossier yet. Use this for exam-year progress and upcoming SE tests; use grades_latest with dossier=examen for only the newest SE results.",
164
192
  inputSchema: {
165
193
  examenjaar: z
166
194
  .number()
@@ -171,6 +199,7 @@ export function createServer(session = createDefaultSession()) {
171
199
  },
172
200
  }, async (args) => examGradesTool(ctx, args));
173
201
  server.registerTool("absence", {
202
+ annotations: READ_ONLY,
174
203
  title: "Absence and registrations",
175
204
  description: "Registrations for the school year (or current grading period): unexcused and excused absence, late, removed from class, homework not done, materials missing — counts and every entry with date, lesson and remark — plus active measures (e.g. detention).",
176
205
  inputSchema: {
@@ -182,10 +211,13 @@ export function createServer(session = createDefaultSession()) {
182
211
  },
183
212
  }, async (args) => absenceTool(ctx, args));
184
213
  server.registerTool("messages", {
214
+ annotations: READ_ONLY,
185
215
  title: "Messages",
186
- description: "Somtoday messages (berichten), read-only: newest conversations with subject, sender, date, unread flag and a preview; filter with zoek or ongelezen. Pass conversatieId for the full thread (text, sender, recipients, attachments). Never marks anything as read.",
216
+ description: "Somtoday messages (berichten), read-only: newest conversations with subject, sender, date, unread flag and a preview; filter with zoek or ongelezen. Pass conversatieId for the full thread (text, sender, recipients, attachments). Never marks anything as read. Use this for personal messages from teachers and the school; school-wide news is in news, Somtoday-wide notices in announcements.",
187
217
  inputSchema: {
188
- conversatieId: numericId().optional(),
218
+ conversatieId: numericId()
219
+ .optional()
220
+ .describe("Conversation id from a previous messages call; returns that full thread."),
189
221
  limit: z
190
222
  .number()
191
223
  .int()
@@ -204,8 +236,9 @@ export function createServer(session = createDefaultSession()) {
204
236
  },
205
237
  }, async (args) => messagesTool(ctx, args));
206
238
  server.registerTool("news", {
239
+ annotations: READ_ONLY,
207
240
  title: "School news",
208
- description: "News items from the school (nieuwsberichten), newest first, as text, plus the unread count.",
241
+ description: "News items the school publishes to all parents (nieuwsberichten), newest first, as text, plus the unread count. Use messages for personal conversations and announcements for Somtoday-wide notices from the platform itself.",
209
242
  inputSchema: {
210
243
  limit: z
211
244
  .number()
@@ -217,33 +250,42 @@ export function createServer(session = createDefaultSession()) {
217
250
  },
218
251
  }, async (args) => newsTool(ctx, args));
219
252
  server.registerTool("announcements", {
253
+ annotations: READ_ONLY,
220
254
  title: "Announcements",
221
- description: "Somtoday-wide announcements (landelijke mededelingen) shown in the portal, with publication window.",
255
+ description: "Somtoday-wide announcements (landelijke mededelingen) from the platform itself, such as maintenance notices, with their publication window. Not school news (use news) or personal messages (use messages).",
222
256
  inputSchema: {},
223
257
  }, async () => announcementsTool(ctx));
224
258
  server.registerTool("children", {
259
+ annotations: READ_ONLY,
225
260
  title: "Children",
226
- description: "Linked children with profile (name, student number, class), school data (school, location, mentor(s), year, programme), current placement, and which portal sections the school shows.",
261
+ description: "Linked children with profile (name, student number, class), school data (school, location, mentor(s), year, programme), current placement, and which portal sections the school shows. Use whoami when you only need leerling ids.",
227
262
  inputSchema: {},
228
263
  }, async () => childrenTool(ctx));
229
264
  server.registerTool("school_info", {
265
+ annotations: READ_ONLY,
230
266
  title: "School information",
231
- description: "School contact data per child and the school's published information pages. Pass paginaId for one page with its topics and documents.",
267
+ description: "School contact data per child and the school's published information pages. Pass paginaId for one page with its topics and documents. Use teachers for staff and children for mentors.",
232
268
  inputSchema: {
233
- paginaId: numericId().transform(Number).optional(),
269
+ paginaId: numericId()
270
+ .transform(Number)
271
+ .optional()
272
+ .describe("Information page id from the page list of a previous school_info call."),
234
273
  },
235
274
  }, async (args) => schoolInfoTool(ctx, args));
236
275
  server.registerTool("teachers", {
276
+ annotations: READ_ONLY,
237
277
  title: "Teachers",
238
- description: "Staff you can message, with the subjects they teach your child.",
278
+ description: "Teachers and other staff you can message in Somtoday, with the subjects they teach your child. Use children for the mentor(s) and school_info for general school contact data.",
239
279
  inputSchema: {},
240
280
  }, async () => teachersTool(ctx));
241
281
  server.registerTool("signing_requests", {
282
+ annotations: READ_ONLY,
242
283
  title: "Signing requests",
243
284
  description: "Digital signing requests (ondertekenverzoeken) waiting for you or done. Each item has document {tool:'signing_document', id, available}: pass id to signing_document to read the (signed) PDF. Read-only; nothing is signed.",
244
285
  inputSchema: {},
245
286
  }, async () => signingRequestsTool(ctx));
246
287
  server.registerTool("signing_document", {
288
+ annotations: READ_ONLY,
247
289
  title: "Signing document",
248
290
  description: "Read the document (PDF) of one signing request, as the portal's download button does: pass document.id from signing_requests. Returns extracted text (max 10 MB). The document needs your login, so it is not a fileUrl for download_attachment. Usually available once signed; not for requests still to sign, withdrawn or expired.",
249
291
  inputSchema: {
@@ -253,25 +295,34 @@ export function createServer(session = createDefaultSession()) {
253
295
  },
254
296
  }, async (args) => signingDocumentTool(ctx, args));
255
297
  server.registerTool("parent_evening", {
298
+ annotations: READ_ONLY,
256
299
  title: "Parent evening",
257
300
  description: "Parent-evening (ouderavond) details: invitation, time slots and chosen teachers. Without id, finds invitations linked in your messages. Read-only; nothing is booked.",
258
301
  inputSchema: {
259
302
  id: textOrNumber()
260
303
  .pipe(z.string().regex(/^[A-Za-z0-9-]+$/))
261
- .optional(),
304
+ .optional()
305
+ .describe("Parent-evening id from a previous call or invitation; omit to find invitations in messages."),
262
306
  },
263
307
  }, async (args) => parentEveningTool(ctx, args));
264
308
  server.registerTool("consents", {
309
+ annotations: READ_ONLY,
265
310
  title: "Consents",
266
- description: "Consents (toestemmingen, e.g. use of photos) as set in the portal. Read-only.",
311
+ description: "Consents (toestemmingen) as set in the portal: each consent item (e.g. use of photos) with the answer recorded in the portal. Read-only; answers cannot be changed here.",
267
312
  inputSchema: {},
268
313
  }, async () => consentsTool(ctx));
269
314
  server.registerTool("download_attachment", {
315
+ annotations: READ_ONLY,
270
316
  title: "Download attachment",
271
317
  description: "Read an attachment (fileUrl from homework, assignments, study material or messages). Only https Somtoday file hosts; max 10 MB; returns extracted text for PDF/text/HTML/CSV/JSON, metadata otherwise.",
272
- inputSchema: { url: z.url() },
318
+ inputSchema: {
319
+ url: z
320
+ .url()
321
+ .describe("fileUrl of an attachment from homework, assignments, study_material or messages."),
322
+ },
273
323
  }, async (args) => downloadAttachmentTool(ctx, args));
274
324
  server.registerTool("auth_status", {
325
+ annotations: READ_ONLY,
275
326
  title: "Auth status",
276
327
  description: "Whether a Somtoday session is stored, where (keychain or file), and the outcome of the last token renewal. Never returns tokens. Set check=true to renew the token and call the API once.",
277
328
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "somtoday-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.barzoj/somtoday-mcp",
5
5
  "description": "Unofficial read-only MCP server for the Somtoday parent portal",
6
6
  "type": "module",