canvas-parent-mcp 2.1.11 → 2.2.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Canvas LMS (Instructure) — courses, grades, assignments, announcements, planner, conversations, files",
10
- "version": "2.1.11"
10
+ "version": "2.2.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Canvas LMS",
16
16
  "source": "./",
17
17
  "description": "Canvas LMS (Instructure) MCP server for Claude — student/observer access via natural language",
18
- "version": "2.1.11",
18
+ "version": "2.2.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canvas-parent-mcp",
3
3
  "displayName": "Canvas LMS",
4
- "version": "2.1.11",
4
+ "version": "2.2.0",
5
5
  "description": "Canvas LMS (Instructure) MCP server for Claude — courses, grades, assignments, announcements, planner, conversations, files",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/README.md CHANGED
@@ -29,6 +29,16 @@ Mirrors the read-only parent-portal scope of sibling [`infinitecampus-mcp`](http
29
29
 
30
30
  Tools that the harness will gate as write/IO operations: `canvas_download_file`.
31
31
 
32
+ ### Planner: child names and a brief view
33
+
34
+ `canvas_list_planner_items` takes a child by name. At start the server lists your observees once (bounded to 5 s) and publishes their display names as the `observeeId` enum; the tool accepts either a name (any case) or a Canvas user id, and an unknown name is refused with the list of known ones. If the listing fails or you observe nobody, `observeeId` is published as a plain string and ids pass through as before. Two children with the same display name are listed as `Name (id)`.
35
+
36
+ `view: "brief"` returns one line per planner item, at most 120 characters: `title — due <ISO date or "no date"> — <submitted|not submitted|missing>`. Items with nothing to submit (planner notes, pages, events) carry no status. The default (`view: "full"`) is Canvas's payload, unchanged.
37
+
38
+ ### Context-group hints
39
+
40
+ Every tool except `canvas_healthcheck` carries a `_meta.mint` hint for hosts that group tools by context (mcp-host's context groups): `domain: "school"` plus an `intent` — `whats-due` for the planner and missing submissions, `whats-new` for announcements, `whats-on` for the calendar tools, `lookup` for the other reads, `other` for the download. The planner (`whats-due`) and announcements (`whats-new`) are marked `canonical`; the planner also names its subject argument (`observeeId`) and range (`startDate`/`endDate`). Hosts that do not read `_meta` see nothing different.
41
+
32
42
  `canvas_download_file` only fetches `/files/…` URLs on your `CANVAS_BASE_URL` host (https), so your Canvas credential is never sent anywhere else, and it only writes inside a download directory: `CANVAS_OUTPUT_DIR` if set, otherwise `~/Downloads`. A relative `destinationPath` is resolved against that directory. It never writes through a symlink at the destination, and new files are created owner-only (0600).
33
43
 
34
44
  ## Configuration
package/dist/bundle.js CHANGED
@@ -289,14 +289,14 @@ var require_util = __commonJS({
289
289
  }
290
290
  function randomString2(length = 10) {
291
291
  const chars = "abcdefghijklmnopqrstuvwxyz";
292
- let str = "";
292
+ let str2 = "";
293
293
  for (let i = 0; i < length; i++) {
294
- str += chars[Math.floor(Math.random() * chars.length)];
294
+ str2 += chars[Math.floor(Math.random() * chars.length)];
295
295
  }
296
- return str;
296
+ return str2;
297
297
  }
298
- function esc2(str) {
299
- return JSON.stringify(str);
298
+ function esc2(str2) {
299
+ return JSON.stringify(str2);
300
300
  }
301
301
  function slugify2(input2) {
302
302
  return input2.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -411,8 +411,8 @@ var require_util = __commonJS({
411
411
  "symbol",
412
412
  "undefined"
413
413
  ]);
414
- function escapeRegex2(str) {
415
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
414
+ function escapeRegex2(str2) {
415
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
416
416
  }
417
417
  function clone2(inst, def, params) {
418
418
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -678,13 +678,13 @@ var require_util = __commonJS({
678
678
  return "unknown";
679
679
  }
680
680
  var highSurrogate2 = /[\uD800-\uDBFF]/;
681
- function codePointLength2(str) {
682
- const units = str.length;
683
- if (!highSurrogate2.test(str))
681
+ function codePointLength2(str2) {
682
+ const units = str2.length;
683
+ if (!highSurrogate2.test(str2))
684
684
  return units;
685
685
  let count = units;
686
686
  for (let i = 0; i < units - 1; i++) {
687
- if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {
687
+ if ((str2.charCodeAt(i) & 64512) === 55296 && (str2.charCodeAt(i + 1) & 64512) === 56320) {
688
688
  count--;
689
689
  i++;
690
690
  }
@@ -32807,7 +32807,7 @@ var require_code$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
32807
32807
  }
32808
32808
  exports._ = _;
32809
32809
  const plus = new _Code("+");
32810
- function str(strs, ...args) {
32810
+ function str2(strs, ...args) {
32811
32811
  const expr = [safeStringify(strs[0])];
32812
32812
  let i = 0;
32813
32813
  while (i < args.length) {
@@ -32818,7 +32818,7 @@ var require_code$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
32818
32818
  optimize(expr);
32819
32819
  return new _Code(expr);
32820
32820
  }
32821
- exports.str = str;
32821
+ exports.str = str2;
32822
32822
  function addCodeArg(code, arg) {
32823
32823
  if (arg instanceof _Code) code.push(...arg._items);
32824
32824
  else if (arg instanceof Name) code.push(arg);
@@ -32851,7 +32851,7 @@ var require_code$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
32851
32851
  if (typeof b == "string" && b[0] === '"' && !(a instanceof Name)) return `"${a}${b.slice(1)}`;
32852
32852
  }
32853
32853
  function strConcat(c1, c2) {
32854
- return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`;
32854
+ return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str2`${c1}${c2}`;
32855
32855
  }
32856
32856
  exports.strConcat = strConcat;
32857
32857
  function interpolate(x) {
@@ -33730,21 +33730,21 @@ var require_util2 = /* @__PURE__ */ __commonJSMin(((exports) => {
33730
33730
  return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
33731
33731
  }
33732
33732
  exports.schemaRefOrVal = schemaRefOrVal;
33733
- function unescapeFragment(str) {
33734
- return unescapeJsonPointer(decodeURIComponent(str));
33733
+ function unescapeFragment(str2) {
33734
+ return unescapeJsonPointer(decodeURIComponent(str2));
33735
33735
  }
33736
33736
  exports.unescapeFragment = unescapeFragment;
33737
- function escapeFragment(str) {
33738
- return encodeURIComponent(escapeJsonPointer(str));
33737
+ function escapeFragment(str2) {
33738
+ return encodeURIComponent(escapeJsonPointer(str2));
33739
33739
  }
33740
33740
  exports.escapeFragment = escapeFragment;
33741
- function escapeJsonPointer(str) {
33742
- if (typeof str == "number") return `${str}`;
33743
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
33741
+ function escapeJsonPointer(str2) {
33742
+ if (typeof str2 == "number") return `${str2}`;
33743
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
33744
33744
  }
33745
33745
  exports.escapeJsonPointer = escapeJsonPointer;
33746
- function unescapeJsonPointer(str) {
33747
- return str.replace(/~1/g, "/").replace(/~0/g, "~");
33746
+ function unescapeJsonPointer(str2) {
33747
+ return str2.replace(/~1/g, "/").replace(/~0/g, "~");
33748
33748
  }
33749
33749
  exports.unescapeJsonPointer = unescapeJsonPointer;
33750
33750
  function eachItem(xs, f) {
@@ -34630,8 +34630,8 @@ var require_json_schema_traverse = /* @__PURE__ */ __commonJSMin(((exports, modu
34630
34630
  post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
34631
34631
  }
34632
34632
  }
34633
- function escapeJsonPtr(str) {
34634
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
34633
+ function escapeJsonPtr(str2) {
34634
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
34635
34635
  }
34636
34636
  }));
34637
34637
  var require_resolve = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -35576,9 +35576,9 @@ var require_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
35576
35576
  isIPV6: true
35577
35577
  };
35578
35578
  }
35579
- function findToken(str, token) {
35579
+ function findToken(str2, token) {
35580
35580
  let ind = 0;
35581
- for (let i = 0; i < str.length; i++) if (str[i] === token) ind++;
35581
+ for (let i = 0; i < str2.length; i++) if (str2[i] === token) ind++;
35582
35582
  return ind;
35583
35583
  }
35584
35584
  function removeDotSegments(path) {
@@ -36393,7 +36393,7 @@ var require_core$3 = /* @__PURE__ */ __commonJSMin(((exports) => {
36393
36393
  const util_1 = require_util2();
36394
36394
  const $dataRefSchema = require_data();
36395
36395
  const uri_1 = require_uri();
36396
- const defaultRegExp = (str, flags) => new RegExp(str, flags);
36396
+ const defaultRegExp = (str2, flags) => new RegExp(str2, flags);
36397
36397
  defaultRegExp.code = "new RegExp";
36398
36398
  const META_IGNORE_OPTIONS = [
36399
36399
  "removeAdditional",
@@ -37099,16 +37099,16 @@ var require_multipleOf = /* @__PURE__ */ __commonJSMin(((exports) => {
37099
37099
  }));
37100
37100
  var require_ucs2length = /* @__PURE__ */ __commonJSMin(((exports) => {
37101
37101
  Object.defineProperty(exports, "__esModule", { value: true });
37102
- function ucs2length(str) {
37103
- const len = str.length;
37102
+ function ucs2length(str2) {
37103
+ const len = str2.length;
37104
37104
  let length = 0;
37105
37105
  let pos = 0;
37106
37106
  let value;
37107
37107
  while (pos < len) {
37108
37108
  length++;
37109
- value = str.charCodeAt(pos++);
37109
+ value = str2.charCodeAt(pos++);
37110
37110
  if (value >= 55296 && value <= 56319 && pos < len) {
37111
- value = str.charCodeAt(pos);
37111
+ value = str2.charCodeAt(pos);
37112
37112
  if ((value & 64512) === 56320) pos++;
37113
37113
  }
37114
37114
  }
@@ -39819,8 +39819,8 @@ var require_formats = /* @__PURE__ */ __commonJSMin(((exports) => {
39819
39819
  30,
39820
39820
  31
39821
39821
  ];
39822
- function date5(str) {
39823
- const matches = DATE.exec(str);
39822
+ function date5(str2) {
39823
+ const matches = DATE.exec(str2);
39824
39824
  if (!matches) return false;
39825
39825
  const year = +matches[1];
39826
39826
  const month = +matches[2];
@@ -39835,8 +39835,8 @@ var require_formats = /* @__PURE__ */ __commonJSMin(((exports) => {
39835
39835
  }
39836
39836
  const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
39837
39837
  function getTime(strictTimeZone) {
39838
- return function time3(str) {
39839
- const matches = TIME.exec(str);
39838
+ return function time3(str2) {
39839
+ const matches = TIME.exec(str2);
39840
39840
  if (!matches) return false;
39841
39841
  const hr = +matches[1];
39842
39842
  const min = +matches[2];
@@ -39873,8 +39873,8 @@ var require_formats = /* @__PURE__ */ __commonJSMin(((exports) => {
39873
39873
  const DATE_TIME_SEPARATOR = /t|\s/i;
39874
39874
  function getDateTime(strictTimeZone) {
39875
39875
  const time3 = getTime(strictTimeZone);
39876
- return function date_time(str) {
39877
- const dateTime = str.split(DATE_TIME_SEPARATOR);
39876
+ return function date_time(str2) {
39877
+ const dateTime = str2.split(DATE_TIME_SEPARATOR);
39878
39878
  return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
39879
39879
  };
39880
39880
  }
@@ -39895,13 +39895,13 @@ var require_formats = /* @__PURE__ */ __commonJSMin(((exports) => {
39895
39895
  }
39896
39896
  const NOT_URI_FRAGMENT = /\/|:/;
39897
39897
  const URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
39898
- function uri(str) {
39899
- return NOT_URI_FRAGMENT.test(str) && URI.test(str);
39898
+ function uri(str2) {
39899
+ return NOT_URI_FRAGMENT.test(str2) && URI.test(str2);
39900
39900
  }
39901
39901
  const BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
39902
- function byte(str) {
39902
+ function byte(str2) {
39903
39903
  BYTE.lastIndex = 0;
39904
- return BYTE.test(str);
39904
+ return BYTE.test(str2);
39905
39905
  }
39906
39906
  const MIN_INT32 = -(2 ** 31);
39907
39907
  const MAX_INT32 = 2 ** 31 - 1;
@@ -39915,10 +39915,10 @@ var require_formats = /* @__PURE__ */ __commonJSMin(((exports) => {
39915
39915
  return true;
39916
39916
  }
39917
39917
  const Z_ANCHOR = /[^\\]\\Z/;
39918
- function regex(str) {
39919
- if (Z_ANCHOR.test(str)) return false;
39918
+ function regex(str2) {
39919
+ if (Z_ANCHOR.test(str2)) return false;
39920
39920
  try {
39921
- new RegExp(str);
39921
+ new RegExp(str2);
39922
39922
  return true;
39923
39923
  } catch (e) {
39924
39924
  return false;
@@ -41531,17 +41531,17 @@ var McpServer = class {
41531
41531
  if (Array.isArray(variableNames) && variableNames.some((v) => !!template.completeCallback(v))) this.setCompletionRequestHandler();
41532
41532
  return registeredResourceTemplate;
41533
41533
  }
41534
- _createRegisteredPrompt(name, title, description, argsSchema7, callback, icons, scopeChallenge, _meta) {
41535
- let currentArgsSchema = argsSchema7;
41534
+ _createRegisteredPrompt(name, title, description, argsSchema6, callback, icons, scopeChallenge, _meta) {
41535
+ let currentArgsSchema = argsSchema6;
41536
41536
  let currentCallback = callback;
41537
41537
  const registeredPrompt = {
41538
41538
  title,
41539
41539
  description,
41540
- argsSchema: argsSchema7,
41540
+ argsSchema: argsSchema6,
41541
41541
  icons,
41542
41542
  scopeChallenge,
41543
41543
  _meta,
41544
- handler: createPromptHandler(name, argsSchema7, callback),
41544
+ handler: createPromptHandler(name, argsSchema6, callback),
41545
41545
  enabled: true,
41546
41546
  disable: () => registeredPrompt.update({ enabled: false }),
41547
41547
  enable: () => registeredPrompt.update({ enabled: true }),
@@ -41572,8 +41572,8 @@ var McpServer = class {
41572
41572
  }
41573
41573
  };
41574
41574
  this._registeredPrompts[name] = registeredPrompt;
41575
- if (argsSchema7) {
41576
- const shape = getSchemaShape(argsSchema7);
41575
+ if (argsSchema6) {
41576
+ const shape = getSchemaShape(argsSchema6);
41577
41577
  if (shape) {
41578
41578
  if (Object.values(shape).some((field) => {
41579
41579
  return isCompletable(unwrapOptionalSchema(field));
@@ -41657,8 +41657,8 @@ var McpServer = class {
41657
41657
  }
41658
41658
  registerPrompt(name, config2, cb) {
41659
41659
  if (this._registeredPrompts[name]) throw new Error(`Prompt ${name} is already registered`);
41660
- const { title, description, argsSchema: argsSchema7, icons, scopeChallenge, _meta } = config2;
41661
- const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema7), cb, icons, scopeChallenge, _meta);
41660
+ const { title, description, argsSchema: argsSchema6, icons, scopeChallenge, _meta } = config2;
41661
+ const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema6), cb, icons, scopeChallenge, _meta);
41662
41662
  this.setPromptRequestHandlers();
41663
41663
  this.sendPromptListChanged();
41664
41664
  return registeredPrompt;
@@ -41737,11 +41737,11 @@ function convertOutputSchemaJson(outputSchema) {
41737
41737
  return;
41738
41738
  }
41739
41739
  }
41740
- function createPromptHandler(name, argsSchema7, callback) {
41741
- if (argsSchema7) {
41740
+ function createPromptHandler(name, argsSchema6, callback) {
41741
+ if (argsSchema6) {
41742
41742
  const typedCallback = callback;
41743
41743
  return async (args, ctx) => {
41744
- const parseResult = await validateStandardSchema(argsSchema7, args ?? {});
41744
+ const parseResult = await validateStandardSchema(argsSchema6, args ?? {});
41745
41745
  if (!parseResult.success) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${parseResult.error}`);
41746
41746
  return typedCallback(parseResult.data, ctx);
41747
41747
  };
@@ -42612,9 +42612,9 @@ function redactValues(text, values, opts = {}) {
42612
42612
  return out;
42613
42613
  }
42614
42614
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX, opts = {}) {
42615
- const str = text === null || text === void 0 ? "" : String(text);
42615
+ const str2 = text === null || text === void 0 ? "" : String(text);
42616
42616
  const cap = Math.max(ERROR_REDACTION_INPUT_MAX, max);
42617
- const capped = str.length > cap ? str.slice(0, cap) : str;
42617
+ const capped = str2.length > cap ? str2.slice(0, cap) : str2;
42618
42618
  const scrubbed = opts.values !== void 0 ? redactValues(capped, opts.values, opts.placeholder !== void 0 ? { placeholder: opts.placeholder } : {}) : capped;
42619
42619
  const redacted = redactSecrets(scrubbed);
42620
42620
  if (redacted.length <= max)
@@ -43462,14 +43462,14 @@ function promiseAllObject(promisesObj) {
43462
43462
  }
43463
43463
  function randomString(length = 10) {
43464
43464
  const chars = "abcdefghijklmnopqrstuvwxyz";
43465
- let str = "";
43465
+ let str2 = "";
43466
43466
  for (let i = 0; i < length; i++) {
43467
- str += chars[Math.floor(Math.random() * chars.length)];
43467
+ str2 += chars[Math.floor(Math.random() * chars.length)];
43468
43468
  }
43469
- return str;
43469
+ return str2;
43470
43470
  }
43471
- function esc(str) {
43472
- return JSON.stringify(str);
43471
+ function esc(str2) {
43472
+ return JSON.stringify(str2);
43473
43473
  }
43474
43474
  function slugify(input2) {
43475
43475
  return input2.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -43583,8 +43583,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
43583
43583
  "symbol",
43584
43584
  "undefined"
43585
43585
  ]);
43586
- function escapeRegex(str) {
43587
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43586
+ function escapeRegex(str2) {
43587
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43588
43588
  }
43589
43589
  function clone(inst, def, params) {
43590
43590
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -43850,13 +43850,13 @@ function getSizableOrigin(input2) {
43850
43850
  return "unknown";
43851
43851
  }
43852
43852
  var highSurrogate = /[\uD800-\uDBFF]/;
43853
- function codePointLength(str) {
43854
- const units = str.length;
43855
- if (!highSurrogate.test(str))
43853
+ function codePointLength(str2) {
43854
+ const units = str2.length;
43855
+ if (!highSurrogate.test(str2))
43856
43856
  return units;
43857
43857
  let count = units;
43858
43858
  for (let i = 0; i < units - 1; i++) {
43859
- if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {
43859
+ if ((str2.charCodeAt(i) & 64512) === 55296 && (str2.charCodeAt(i + 1) & 64512) === 56320) {
43860
43860
  count--;
43861
43861
  i++;
43862
43862
  }
@@ -69665,7 +69665,7 @@ function loadAccount(env = process.env) {
69665
69665
  // package.json
69666
69666
  var package_default = {
69667
69667
  name: "canvas-parent-mcp",
69668
- version: "2.1.11",
69668
+ version: "2.2.0",
69669
69669
  mcpName: "io.github.chrischall/canvas-parent-mcp",
69670
69670
  description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
69671
69671
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -71152,6 +71152,18 @@ var FileExistsError = class extends Error {
71152
71152
  path;
71153
71153
  };
71154
71154
 
71155
+ // src/tools/_shared.ts
71156
+ var textContent = minifiedResult;
71157
+ function buildPath(base, params = {}) {
71158
+ return `${base}${buildQueryString(params)}`;
71159
+ }
71160
+ function userSegment(observeeId) {
71161
+ return observeeId ? `users/${encodeURIComponent(observeeId)}` : "users/self";
71162
+ }
71163
+ function schoolMeta(intent, extra = {}) {
71164
+ return { mint: { domain: "school", intent, ...extra } };
71165
+ }
71166
+
71155
71167
  // src/view.ts
71156
71168
  var CV_VIEWS = ["compact", "full"];
71157
71169
  var NOTE = `compact removes the avatar URLs Canvas attaches to user objects (avatar_url, avatar_image_url); "full" returns Canvas's payload untouched. Link fields (url, html_url, preview_url) survive both rungs. No field projection: this server passes Canvas's payload through verbatim and has no verified record of which of its fields a caller needs.`;
@@ -71171,6 +71183,7 @@ function registerProfileTools(server, client2) {
71171
71183
  server.registerTool("canvas_get_profile", {
71172
71184
  description: "Get the logged-in user's Canvas profile (id, name, primary_email, login_id, locale, time_zone). Useful first call to confirm credentials.",
71173
71185
  annotations: { readOnlyHint: true, openWorldHint: true },
71186
+ _meta: schoolMeta("lookup"),
71174
71187
  inputSchema: argsSchema
71175
71188
  }, async (rawArgs) => {
71176
71189
  const args = argsSchema.parse(rawArgs);
@@ -71187,6 +71200,7 @@ function registerObserveeTools(server, client2) {
71187
71200
  server.registerTool("canvas_list_observees", {
71188
71201
  description: "List students linked to your Canvas observer account. Returns an empty array for plain student tokens.",
71189
71202
  annotations: { readOnlyHint: true, openWorldHint: true },
71203
+ _meta: schoolMeta("lookup"),
71190
71204
  inputSchema: argsSchema2
71191
71205
  }, async (rawArgs) => {
71192
71206
  const args = argsSchema2.parse(rawArgs);
@@ -71194,14 +71208,53 @@ function registerObserveeTools(server, client2) {
71194
71208
  return viewResponse(args.view, data);
71195
71209
  });
71196
71210
  }
71197
-
71198
- // src/tools/_shared.ts
71199
- var textContent = minifiedResult;
71200
- function buildPath(base, params = {}) {
71201
- return `${base}${buildQueryString(params)}`;
71211
+ var OBSERVEE_LIST_TIMEOUT_MS = 5e3;
71212
+ function readObservees(data) {
71213
+ if (!Array.isArray(data)) return [];
71214
+ const out = [];
71215
+ for (const row of data) {
71216
+ if (!row || typeof row !== "object") continue;
71217
+ const r = row;
71218
+ if (typeof r.id !== "string" && typeof r.id !== "number") continue;
71219
+ const id = String(r.id);
71220
+ const name = [r.name, r.short_name].find((v) => typeof v === "string" && v.trim() !== "");
71221
+ out.push({ id, label: name?.trim() ?? id });
71222
+ }
71223
+ return out;
71202
71224
  }
71203
- function userSegment(observeeId) {
71204
- return observeeId ? `users/${encodeURIComponent(observeeId)}` : "users/self";
71225
+ function buildDirectory(users) {
71226
+ if (users.length === 0) return null;
71227
+ const counts = /* @__PURE__ */ new Map();
71228
+ for (const u of users) counts.set(u.label.toLowerCase(), (counts.get(u.label.toLowerCase()) ?? 0) + 1);
71229
+ const labelled = users.map((u) => ({
71230
+ id: u.id,
71231
+ label: counts.get(u.label.toLowerCase()) > 1 ? `${u.label} (${u.id})` : u.label
71232
+ }));
71233
+ const byLabel = new Map(labelled.map((u) => [u.label.toLowerCase(), u.id]));
71234
+ const ids = new Set(labelled.map((u) => u.id));
71235
+ const labels = labelled.map((u) => u.label);
71236
+ return {
71237
+ labels,
71238
+ resolve(value) {
71239
+ const id = byLabel.get(value.trim().toLowerCase());
71240
+ if (id !== void 0) return id;
71241
+ if (ids.has(value.trim())) return value.trim();
71242
+ throw new Error(`Unknown observee "${value}". Known: ${labels.join(", ")}.`);
71243
+ }
71244
+ };
71245
+ }
71246
+ async function loadObserveeDirectory(client2, timeoutMs = OBSERVEE_LIST_TIMEOUT_MS) {
71247
+ let timer;
71248
+ const deadline = new Promise((resolve5) => {
71249
+ timer = setTimeout(() => resolve5(null), timeoutMs);
71250
+ timer.unref();
71251
+ });
71252
+ const listing = client2.requestPaginated("/api/v1/users/self/observees").then((data) => buildDirectory(readObservees(data))).catch(() => null);
71253
+ try {
71254
+ return await Promise.race([listing, deadline]);
71255
+ } finally {
71256
+ clearTimeout(timer);
71257
+ }
71205
71258
  }
71206
71259
 
71207
71260
  // src/tools/courses.ts
@@ -71216,6 +71269,7 @@ function registerCourseTools(server, client2) {
71216
71269
  server.registerTool("canvas_list_courses", {
71217
71270
  description: "List active Canvas courses (with course-level grades, total scores, current grading period scores, and term).",
71218
71271
  annotations: { readOnlyHint: true, openWorldHint: true },
71272
+ _meta: schoolMeta("lookup"),
71219
71273
  inputSchema: listArgs
71220
71274
  }, async (rawArgs) => {
71221
71275
  const args = listArgs.parse(rawArgs);
@@ -71230,6 +71284,7 @@ function registerCourseTools(server, client2) {
71230
71284
  server.registerTool("canvas_get_course", {
71231
71285
  description: "Get a single Canvas course with its syllabus, teachers, and term.",
71232
71286
  annotations: { readOnlyHint: true, openWorldHint: true },
71287
+ _meta: schoolMeta("lookup"),
71233
71288
  inputSchema: getArgs
71234
71289
  }, async (rawArgs) => {
71235
71290
  const args = getArgs.parse(rawArgs);
@@ -71254,6 +71309,7 @@ function registerAssignmentTools(server, client2) {
71254
71309
  server.registerTool("canvas_list_assignments", {
71255
71310
  description: "List a course's assignments \u2014 homework, quizzes and projects \u2014 with the user's (or child's) submission inline. Supports the standard Canvas `bucket` filter.",
71256
71311
  annotations: { readOnlyHint: true, openWorldHint: true },
71312
+ _meta: schoolMeta("lookup"),
71257
71313
  inputSchema: listArgs2
71258
71314
  }, async (rawArgs) => {
71259
71315
  const args = listArgs2.parse(rawArgs);
@@ -71268,6 +71324,7 @@ function registerAssignmentTools(server, client2) {
71268
71324
  server.registerTool("canvas_list_missing_submissions", {
71269
71325
  description: "List missing homework: past-due unsubmitted assignments for the user (or a linked observee, e.g. a child). For an observee, courseIds is required.",
71270
71326
  annotations: { readOnlyHint: true, openWorldHint: true },
71327
+ _meta: schoolMeta("whats-due"),
71271
71328
  inputSchema: missingArgs
71272
71329
  }, async (rawArgs) => {
71273
71330
  const args = missingArgs.parse(rawArgs);
@@ -71301,6 +71358,7 @@ function registerSubmissionTools(server, client2) {
71301
71358
  server.registerTool("canvas_get_submission", {
71302
71359
  description: "Get a single submission with rubric assessment and grader comments. userId defaults to 'self'.",
71303
71360
  annotations: { readOnlyHint: true, openWorldHint: true },
71361
+ _meta: schoolMeta("lookup"),
71304
71362
  inputSchema: getArgs2
71305
71363
  }, async (rawArgs) => {
71306
71364
  const args = getArgs2.parse(rawArgs);
@@ -71315,6 +71373,7 @@ function registerSubmissionTools(server, client2) {
71315
71373
  server.registerTool("canvas_list_recent_submissions", {
71316
71374
  description: "List recently graded submissions in a course. Defaults to a 14-day window for the calling user.",
71317
71375
  annotations: { readOnlyHint: true, openWorldHint: true },
71376
+ _meta: schoolMeta("lookup"),
71318
71377
  inputSchema: recentArgs
71319
71378
  }, async (rawArgs) => {
71320
71379
  const args = recentArgs.parse(rawArgs);
@@ -71339,6 +71398,7 @@ function registerGradeTools(server, client2) {
71339
71398
  server.registerTool("canvas_list_enrollments", {
71340
71399
  description: "List active student enrollments with per-course grades (current_score, final_score, current_grade, final_grade, current grading period info).",
71341
71400
  annotations: { readOnlyHint: true, openWorldHint: true },
71401
+ _meta: schoolMeta("lookup"),
71342
71402
  inputSchema: argsSchema3
71343
71403
  }, async (rawArgs) => {
71344
71404
  const args = argsSchema3.parse(rawArgs);
@@ -71364,6 +71424,7 @@ function registerCalendarTools(server, client2) {
71364
71424
  server.registerTool("canvas_list_calendar_events", {
71365
71425
  description: "List Canvas calendar events or assignments across selected contexts (courses/users).",
71366
71426
  annotations: { readOnlyHint: true, openWorldHint: true },
71427
+ _meta: schoolMeta("whats-on"),
71367
71428
  inputSchema: eventsArgs
71368
71429
  }, async (rawArgs) => {
71369
71430
  const args = eventsArgs.parse(rawArgs);
@@ -71379,7 +71440,8 @@ function registerCalendarTools(server, client2) {
71379
71440
  });
71380
71441
  server.registerTool("canvas_list_upcoming_events", {
71381
71442
  description: "List the calling user's upcoming events (Canvas's curated next-7-days view).",
71382
- annotations: { readOnlyHint: true, openWorldHint: true }
71443
+ annotations: { readOnlyHint: true, openWorldHint: true },
71444
+ _meta: schoolMeta("whats-on")
71383
71445
  }, async () => {
71384
71446
  const data = await client2.request("/api/v1/users/self/upcoming_events");
71385
71447
  return textContent(data);
@@ -71387,21 +71449,56 @@ function registerCalendarTools(server, client2) {
71387
71449
  }
71388
71450
 
71389
71451
  // src/tools/planner.ts
71390
- var argsSchema4 = external_exports.object({
71391
- observeeId: external_exports.string().optional(),
71392
- startDate: external_exports.string().optional(),
71393
- endDate: external_exports.string().optional(),
71394
- contextCodes: external_exports.array(external_exports.string()).optional(),
71395
- filter: external_exports.enum(["new_activity"]).optional()
71396
- });
71397
- function registerPlannerTools(server, client2) {
71452
+ var OBSERVEE_DESCRIPTION = "A linked observee (child): their display name as listed, or their Canvas user id. Omit for your own planner.";
71453
+ function observeeIdArg(observees2) {
71454
+ const base = external_exports.string();
71455
+ return (observees2 ? base.meta({ enum: observees2.labels }) : base).optional().describe(OBSERVEE_DESCRIPTION);
71456
+ }
71457
+ var BRIEF_MAX = 120;
71458
+ function str(v) {
71459
+ return typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
71460
+ }
71461
+ function briefLine(item) {
71462
+ const it = item && typeof item === "object" ? item : {};
71463
+ const p = it.plannable && typeof it.plannable === "object" ? it.plannable : {};
71464
+ const title = str(p.title) ?? str(p.name) ?? str(it.title) ?? "(untitled)";
71465
+ const due = str(p.due_at) ?? str(it.plannable_date) ?? "no date";
71466
+ const sub = it.submissions;
71467
+ let status = "";
71468
+ if (sub && typeof sub === "object") {
71469
+ const s = sub;
71470
+ status = s.missing === true ? "missing" : s.submitted === true ? "submitted" : "not submitted";
71471
+ }
71472
+ const tail = ` \u2014 due ${due}${status ? ` \u2014 ${status}` : ""}`;
71473
+ const room = BRIEF_MAX - tail.length;
71474
+ const shown = title.length > room ? `${title.slice(0, Math.max(room - 1, 0))}\u2026` : title;
71475
+ return `${shown}${tail}`;
71476
+ }
71477
+ function registerPlannerTools(server, client2, observees2) {
71478
+ const argsSchema6 = external_exports.object({
71479
+ observeeId: observeeIdArg(observees2),
71480
+ startDate: external_exports.string().optional(),
71481
+ endDate: external_exports.string().optional(),
71482
+ contextCodes: external_exports.array(external_exports.string()).optional(),
71483
+ filter: external_exports.enum(["new_activity"]).optional(),
71484
+ view: external_exports.enum(["full", "brief"]).optional().describe(
71485
+ `"full" (default) returns Canvas's planner items verbatim; "brief" returns one line per item: title \u2014 due <ISO date or "no date"> \u2014 <submitted|not submitted|missing>.`
71486
+ )
71487
+ });
71398
71488
  server.registerTool("canvas_list_planner_items", {
71399
71489
  description: "List planner items (assignments + announcements + planner notes + calendar events) for the user or a linked observee.",
71400
71490
  annotations: { readOnlyHint: true, openWorldHint: true },
71401
- inputSchema: argsSchema4
71491
+ _meta: schoolMeta("whats-due", {
71492
+ canonical: true,
71493
+ subject: "observeeId",
71494
+ when: { start: "startDate", end: "endDate" },
71495
+ aliases: ["homework", "assignments", "due", "kids", "school"]
71496
+ }),
71497
+ inputSchema: argsSchema6
71402
71498
  }, async (rawArgs) => {
71403
- const args = argsSchema4.parse(rawArgs);
71404
- const base = args.observeeId ? `/api/v1/users/${encodeURIComponent(args.observeeId)}/planner/items` : "/api/v1/planner/items";
71499
+ const args = argsSchema6.parse(rawArgs);
71500
+ const observeeId = args.observeeId !== void 0 && observees2 ? observees2.resolve(args.observeeId) : args.observeeId;
71501
+ const base = observeeId ? `/api/v1/users/${encodeURIComponent(observeeId)}/planner/items` : "/api/v1/planner/items";
71405
71502
  const path = buildPath(base, {
71406
71503
  start_date: args.startDate,
71407
71504
  end_date: args.endDate,
@@ -71409,12 +71506,15 @@ function registerPlannerTools(server, client2) {
71409
71506
  filter: args.filter
71410
71507
  });
71411
71508
  const data = await client2.requestPaginated(path);
71509
+ if (args.view === "brief") {
71510
+ return { content: [{ type: "text", text: data.map(briefLine).join("\n") }] };
71511
+ }
71412
71512
  return textContent(data);
71413
71513
  });
71414
71514
  }
71415
71515
 
71416
71516
  // src/tools/announcements.ts
71417
- var argsSchema5 = external_exports.object({
71517
+ var argsSchema4 = external_exports.object({
71418
71518
  contextCodes: external_exports.array(external_exports.string()).describe('Required. Array like ["course_123", "course_456"].'),
71419
71519
  startDate: external_exports.string().optional(),
71420
71520
  endDate: external_exports.string().optional(),
@@ -71425,9 +71525,10 @@ function registerAnnouncementTools(server, client2) {
71425
71525
  server.registerTool("canvas_list_announcements", {
71426
71526
  description: 'List announcements across one or more courses. `contextCodes` is required (e.g. ["course_123"]). Defaults to active-only.',
71427
71527
  annotations: { readOnlyHint: true, openWorldHint: true },
71428
- inputSchema: argsSchema5
71528
+ _meta: schoolMeta("whats-new", { canonical: true, aliases: ["teacher", "announcements", "news"] }),
71529
+ inputSchema: argsSchema4
71429
71530
  }, async (rawArgs) => {
71430
- const args = argsSchema5.parse(rawArgs);
71531
+ const args = argsSchema4.parse(rawArgs);
71431
71532
  const path = buildPath("/api/v1/announcements", {
71432
71533
  "context_codes[]": args.contextCodes,
71433
71534
  start_date: args.startDate,
@@ -71453,6 +71554,7 @@ function registerConversationTools(server, client2) {
71453
71554
  server.registerTool("canvas_list_conversations", {
71454
71555
  description: "List Canvas inbox conversations. Optional `scope` (unread/starred/archived/sent) and `filter` (array of context codes).",
71455
71556
  annotations: { readOnlyHint: true, openWorldHint: true },
71557
+ _meta: schoolMeta("lookup"),
71456
71558
  inputSchema: listArgs3
71457
71559
  }, async (rawArgs) => {
71458
71560
  const args = listArgs3.parse(rawArgs);
@@ -71467,6 +71569,7 @@ function registerConversationTools(server, client2) {
71467
71569
  server.registerTool("canvas_get_conversation", {
71468
71570
  description: "Get a full Canvas conversation thread with all messages. Read-only: does not mark the conversation as read.",
71469
71571
  annotations: { readOnlyHint: true, openWorldHint: true },
71572
+ _meta: schoolMeta("lookup"),
71470
71573
  inputSchema: getArgs3
71471
71574
  }, async (rawArgs) => {
71472
71575
  const args = getArgs3.parse(rawArgs);
@@ -71480,7 +71583,7 @@ function registerConversationTools(server, client2) {
71480
71583
  }
71481
71584
 
71482
71585
  // src/tools/discussions.ts
71483
- var argsSchema6 = external_exports.object({
71586
+ var argsSchema5 = external_exports.object({
71484
71587
  courseId: external_exports.string(),
71485
71588
  onlyAnnouncements: external_exports.boolean().optional(),
71486
71589
  orderBy: external_exports.enum(["position", "recent_activity", "title"]).optional(),
@@ -71490,9 +71593,10 @@ function registerDiscussionTools(server, client2) {
71490
71593
  server.registerTool("canvas_list_discussion_topics", {
71491
71594
  description: "List discussion topics for a course (read-only).",
71492
71595
  annotations: { readOnlyHint: true, openWorldHint: true },
71493
- inputSchema: argsSchema6
71596
+ _meta: schoolMeta("lookup"),
71597
+ inputSchema: argsSchema5
71494
71598
  }, async (rawArgs) => {
71495
- const args = argsSchema6.parse(rawArgs);
71599
+ const args = argsSchema5.parse(rawArgs);
71496
71600
  const path = buildPath(`/api/v1/courses/${encodeURIComponent(args.courseId)}/discussion_topics`, {
71497
71601
  only_announcements: args.onlyAnnouncements ?? false,
71498
71602
  order_by: args.orderBy
@@ -71517,6 +71621,7 @@ function registerFileTools(server, client2) {
71517
71621
  server.registerTool("canvas_list_course_files", {
71518
71622
  description: "List a course's files (metadata only \u2014 use canvas_download_file with the `url` field).",
71519
71623
  annotations: { readOnlyHint: true, openWorldHint: true },
71624
+ _meta: schoolMeta("lookup"),
71520
71625
  inputSchema: listArgs4
71521
71626
  }, async (rawArgs) => {
71522
71627
  const args = listArgs4.parse(rawArgs);
@@ -71530,6 +71635,7 @@ function registerFileTools(server, client2) {
71530
71635
  server.registerTool("canvas_download_file", {
71531
71636
  description: "Download a Canvas file to disk. `url` is the absolute URL from canvas_list_course_files (only file URLs on the configured Canvas host are accepted); `destinationPath` is required and must be inside the download directory (CANVAS_OUTPUT_DIR, default ~/Downloads).",
71532
71637
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
71638
+ _meta: schoolMeta("other"),
71533
71639
  inputSchema: downloadArgs
71534
71640
  }, async (rawArgs) => {
71535
71641
  const args = downloadArgs.parse(rawArgs);
@@ -71551,9 +71657,10 @@ try {
71551
71657
  configError = e;
71552
71658
  }
71553
71659
  var client = resolved ? new CanvasClient(resolved.account, { refreshSession: resolved.refresh }) : null;
71660
+ var observees = client ? await loadObserveeDirectory(client) : null;
71554
71661
  await runMcp({
71555
71662
  name: "canvas",
71556
- version: "2.1.11",
71663
+ version: "2.2.0",
71557
71664
  // x-release-please-version
71558
71665
  tools: [
71559
71666
  // Outside the `if (resolved)` on purpose: an unconfigured server must
@@ -71568,7 +71675,7 @@ await runMcp({
71568
71675
  registerSubmissionTools(server, client);
71569
71676
  registerGradeTools(server, client);
71570
71677
  registerCalendarTools(server, client);
71571
- registerPlannerTools(server, client);
71678
+ registerPlannerTools(server, client, observees);
71572
71679
  registerAnnouncementTools(server, client);
71573
71680
  registerConversationTools(server, client);
71574
71681
  registerDiscussionTools(server, client);
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { resolveAuth } from './auth.js';
11
11
  import { registerHealthcheckTools } from './tools/healthcheck.js';
12
12
  import { CanvasClient } from './client.js';
13
13
  import { registerProfileTools } from './tools/profile.js';
14
- import { registerObserveeTools } from './tools/observees.js';
14
+ import { registerObserveeTools, loadObserveeDirectory } from './tools/observees.js';
15
15
  import { registerCourseTools } from './tools/courses.js';
16
16
  import { registerAssignmentTools } from './tools/assignments.js';
17
17
  import { registerSubmissionTools } from './tools/submissions.js';
@@ -46,6 +46,11 @@ catch (e) {
46
46
  const client = resolved
47
47
  ? new CanvasClient(resolved.account, { refreshSession: resolved.refresh })
48
48
  : null;
49
+ // The planner's `observeeId` publishes the observees' display names as an
50
+ // enum (context groups, mcp-host spec 2026-11-03 D5), so list them ONCE here —
51
+ // registrars run per served instance — bounded by OBSERVEE_LIST_TIMEOUT_MS.
52
+ // Null (failure, timeout, nobody observed) publishes the property with no enum.
53
+ const observees = client ? await loadObserveeDirectory(client) : null;
49
54
  // `runMcp` (from @chrischall/mcp-utils) builds the McpServer, runs the
50
55
  // registrar, then connects the stdio transport and installs SIGINT/SIGTERM
51
56
  // shutdown handlers. The deferred-config-error pattern is preserved by
@@ -54,7 +59,7 @@ const client = resolved
54
59
  // host's install-time `tools/list` succeeds.
55
60
  await runMcp({
56
61
  name: 'canvas',
57
- version: '2.1.11', // x-release-please-version
62
+ version: '2.2.0', // x-release-please-version
58
63
  tools: [
59
64
  // Outside the `if (resolved)` on purpose: an unconfigured server must
60
65
  // still expose something that can say WHY (see the tool's docblock).
@@ -68,7 +73,7 @@ await runMcp({
68
73
  registerSubmissionTools(server, client);
69
74
  registerGradeTools(server, client);
70
75
  registerCalendarTools(server, client);
71
- registerPlannerTools(server, client);
76
+ registerPlannerTools(server, client, observees);
72
77
  registerAnnouncementTools(server, client);
73
78
  registerConversationTools(server, client);
74
79
  registerDiscussionTools(server, client);
@@ -29,3 +29,7 @@ export function buildPath(base, params = {}) {
29
29
  export function userSegment(observeeId) {
30
30
  return observeeId ? `users/${encodeURIComponent(observeeId)}` : 'users/self';
31
31
  }
32
+ /** `_meta` for a Canvas tool: every tool here is in the `school` domain. */
33
+ export function schoolMeta(intent, extra = {}) {
34
+ return { mint: { domain: 'school', intent, ...extra } };
35
+ }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { buildPath } from './_shared.js';
2
+ import { buildPath, schoolMeta } from './_shared.js';
3
3
  import { viewArg, viewResponse } from '../view.js';
4
4
  const argsSchema = z.object({
5
5
  contextCodes: z.array(z.string()).describe('Required. Array like ["course_123", "course_456"].'),
@@ -12,6 +12,7 @@ export function registerAnnouncementTools(server, client) {
12
12
  server.registerTool('canvas_list_announcements', {
13
13
  description: "List announcements across one or more courses. `contextCodes` is required (e.g. [\"course_123\"]). Defaults to active-only.",
14
14
  annotations: { readOnlyHint: true, openWorldHint: true },
15
+ _meta: schoolMeta('whats-new', { canonical: true, aliases: ['teacher', 'announcements', 'news'] }),
15
16
  inputSchema: argsSchema,
16
17
  }, async (rawArgs) => {
17
18
  const args = argsSchema.parse(rawArgs);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textContent, buildPath, userSegment } from './_shared.js';
2
+ import { textContent, buildPath, userSegment, schoolMeta } from './_shared.js';
3
3
  const listArgs = z.object({
4
4
  courseId: z.string(),
5
5
  bucket: z.enum(['past', 'overdue', 'undated', 'ungraded', 'unsubmitted', 'upcoming', 'future']).optional()
@@ -13,6 +13,7 @@ export function registerAssignmentTools(server, client) {
13
13
  server.registerTool('canvas_list_assignments', {
14
14
  description: "List a course's assignments — homework, quizzes and projects — with the user's (or child's) submission inline. Supports the standard Canvas `bucket` filter.",
15
15
  annotations: { readOnlyHint: true, openWorldHint: true },
16
+ _meta: schoolMeta('lookup'),
16
17
  inputSchema: listArgs,
17
18
  }, async (rawArgs) => {
18
19
  const args = listArgs.parse(rawArgs);
@@ -27,6 +28,7 @@ export function registerAssignmentTools(server, client) {
27
28
  server.registerTool('canvas_list_missing_submissions', {
28
29
  description: "List missing homework: past-due unsubmitted assignments for the user (or a linked observee, e.g. a child). For an observee, courseIds is required.",
29
30
  annotations: { readOnlyHint: true, openWorldHint: true },
31
+ _meta: schoolMeta('whats-due'),
30
32
  inputSchema: missingArgs,
31
33
  }, async (rawArgs) => {
32
34
  const args = missingArgs.parse(rawArgs);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textContent, buildPath } from './_shared.js';
2
+ import { textContent, buildPath, schoolMeta } from './_shared.js';
3
3
  const eventsArgs = z.object({
4
4
  contextCodes: z.array(z.string()).optional().describe('Array like ["course_123", "user_456"].'),
5
5
  type: z.enum(['event', 'assignment']).optional(),
@@ -11,6 +11,7 @@ export function registerCalendarTools(server, client) {
11
11
  server.registerTool('canvas_list_calendar_events', {
12
12
  description: 'List Canvas calendar events or assignments across selected contexts (courses/users).',
13
13
  annotations: { readOnlyHint: true, openWorldHint: true },
14
+ _meta: schoolMeta('whats-on'),
14
15
  inputSchema: eventsArgs,
15
16
  }, async (rawArgs) => {
16
17
  const args = eventsArgs.parse(rawArgs);
@@ -27,6 +28,7 @@ export function registerCalendarTools(server, client) {
27
28
  server.registerTool('canvas_list_upcoming_events', {
28
29
  description: "List the calling user's upcoming events (Canvas's curated next-7-days view).",
29
30
  annotations: { readOnlyHint: true, openWorldHint: true },
31
+ _meta: schoolMeta('whats-on'),
30
32
  }, async () => {
31
33
  const data = await client.request('/api/v1/users/self/upcoming_events');
32
34
  return textContent(data);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { buildPath } from './_shared.js';
2
+ import { buildPath, schoolMeta } from './_shared.js';
3
3
  import { viewArg, viewResponse } from '../view.js';
4
4
  const listArgs = z.object({
5
5
  scope: z.enum(['unread', 'starred', 'archived', 'sent']).optional(),
@@ -14,6 +14,7 @@ export function registerConversationTools(server, client) {
14
14
  server.registerTool('canvas_list_conversations', {
15
15
  description: "List Canvas inbox conversations. Optional `scope` (unread/starred/archived/sent) and `filter` (array of context codes).",
16
16
  annotations: { readOnlyHint: true, openWorldHint: true },
17
+ _meta: schoolMeta('lookup'),
17
18
  inputSchema: listArgs,
18
19
  }, async (rawArgs) => {
19
20
  const args = listArgs.parse(rawArgs);
@@ -28,6 +29,7 @@ export function registerConversationTools(server, client) {
28
29
  server.registerTool('canvas_get_conversation', {
29
30
  description: 'Get a full Canvas conversation thread with all messages. Read-only: does not mark the conversation as read.',
30
31
  annotations: { readOnlyHint: true, openWorldHint: true },
32
+ _meta: schoolMeta('lookup'),
31
33
  inputSchema: getArgs,
32
34
  }, async (rawArgs) => {
33
35
  const args = getArgs.parse(rawArgs);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textContent, buildPath, userSegment } from './_shared.js';
2
+ import { textContent, buildPath, userSegment, schoolMeta } from './_shared.js';
3
3
  import { viewArg, viewResponse } from '../view.js';
4
4
  const listArgs = z.object({
5
5
  observeeId: z.string().optional().describe("Observed student's user ID; omit for self."),
@@ -12,6 +12,7 @@ export function registerCourseTools(server, client) {
12
12
  server.registerTool('canvas_list_courses', {
13
13
  description: 'List active Canvas courses (with course-level grades, total scores, current grading period scores, and term).',
14
14
  annotations: { readOnlyHint: true, openWorldHint: true },
15
+ _meta: schoolMeta('lookup'),
15
16
  inputSchema: listArgs,
16
17
  }, async (rawArgs) => {
17
18
  const args = listArgs.parse(rawArgs);
@@ -26,6 +27,7 @@ export function registerCourseTools(server, client) {
26
27
  server.registerTool('canvas_get_course', {
27
28
  description: 'Get a single Canvas course with its syllabus, teachers, and term.',
28
29
  annotations: { readOnlyHint: true, openWorldHint: true },
30
+ _meta: schoolMeta('lookup'),
29
31
  inputSchema: getArgs,
30
32
  }, async (rawArgs) => {
31
33
  const args = getArgs.parse(rawArgs);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { buildPath } from './_shared.js';
2
+ import { buildPath, schoolMeta } from './_shared.js';
3
3
  import { viewArg, viewResponse } from '../view.js';
4
4
  const argsSchema = z.object({
5
5
  courseId: z.string(),
@@ -11,6 +11,7 @@ export function registerDiscussionTools(server, client) {
11
11
  server.registerTool('canvas_list_discussion_topics', {
12
12
  description: 'List discussion topics for a course (read-only).',
13
13
  annotations: { readOnlyHint: true, openWorldHint: true },
14
+ _meta: schoolMeta('lookup'),
14
15
  inputSchema: argsSchema,
15
16
  }, async (rawArgs) => {
16
17
  const args = argsSchema.parse(rawArgs);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textContent, buildPath } from './_shared.js';
2
+ import { textContent, buildPath, schoolMeta } from './_shared.js';
3
3
  const listArgs = z.object({
4
4
  courseId: z.string(),
5
5
  searchTerm: z.string().optional(),
@@ -14,6 +14,7 @@ export function registerFileTools(server, client) {
14
14
  server.registerTool('canvas_list_course_files', {
15
15
  description: "List a course's files (metadata only — use canvas_download_file with the `url` field).",
16
16
  annotations: { readOnlyHint: true, openWorldHint: true },
17
+ _meta: schoolMeta('lookup'),
17
18
  inputSchema: listArgs,
18
19
  }, async (rawArgs) => {
19
20
  const args = listArgs.parse(rawArgs);
@@ -27,6 +28,7 @@ export function registerFileTools(server, client) {
27
28
  server.registerTool('canvas_download_file', {
28
29
  description: "Download a Canvas file to disk. `url` is the absolute URL from canvas_list_course_files (only file URLs on the configured Canvas host are accepted); `destinationPath` is required and must be inside the download directory (CANVAS_OUTPUT_DIR, default ~/Downloads).",
29
30
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
31
+ _meta: schoolMeta('other'),
30
32
  inputSchema: downloadArgs,
31
33
  }, async (rawArgs) => {
32
34
  const args = downloadArgs.parse(rawArgs);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { textContent, buildPath, userSegment } from './_shared.js';
2
+ import { textContent, buildPath, userSegment, schoolMeta } from './_shared.js';
3
3
  const argsSchema = z.object({
4
4
  observeeId: z.string().optional(),
5
5
  });
@@ -7,6 +7,7 @@ export function registerGradeTools(server, client) {
7
7
  server.registerTool('canvas_list_enrollments', {
8
8
  description: 'List active student enrollments with per-course grades (current_score, final_score, current_grade, final_grade, current grading period info).',
9
9
  annotations: { readOnlyHint: true, openWorldHint: true },
10
+ _meta: schoolMeta('lookup'),
10
11
  inputSchema: argsSchema,
11
12
  }, async (rawArgs) => {
12
13
  const args = argsSchema.parse(rawArgs);
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { viewArg, viewResponse } from '../view.js';
3
+ import { schoolMeta } from './_shared.js';
3
4
  const argsSchema = z.object({
4
5
  view: viewArg(),
5
6
  });
@@ -7,6 +8,7 @@ export function registerObserveeTools(server, client) {
7
8
  server.registerTool('canvas_list_observees', {
8
9
  description: "List students linked to your Canvas observer account. Returns an empty array for plain student tokens.",
9
10
  annotations: { readOnlyHint: true, openWorldHint: true },
11
+ _meta: schoolMeta('lookup'),
10
12
  inputSchema: argsSchema,
11
13
  }, async (rawArgs) => {
12
14
  const args = argsSchema.parse(rawArgs);
@@ -14,3 +16,72 @@ export function registerObserveeTools(server, client) {
14
16
  return viewResponse(args.view, data);
15
17
  });
16
18
  }
19
+ /** Bound on the start-time listing: a host must not wait on it to list tools. */
20
+ export const OBSERVEE_LIST_TIMEOUT_MS = 5_000;
21
+ function readObservees(data) {
22
+ if (!Array.isArray(data))
23
+ return [];
24
+ const out = [];
25
+ for (const row of data) {
26
+ if (!row || typeof row !== 'object')
27
+ continue;
28
+ const r = row;
29
+ if (typeof r.id !== 'string' && typeof r.id !== 'number')
30
+ continue;
31
+ const id = String(r.id);
32
+ const name = [r.name, r.short_name].find((v) => typeof v === 'string' && v.trim() !== '');
33
+ out.push({ id, label: name?.trim() ?? id });
34
+ }
35
+ return out;
36
+ }
37
+ function buildDirectory(users) {
38
+ if (users.length === 0)
39
+ return null;
40
+ const counts = new Map();
41
+ for (const u of users)
42
+ counts.set(u.label.toLowerCase(), (counts.get(u.label.toLowerCase()) ?? 0) + 1);
43
+ // Two children with one display name would make the name ambiguous, so each
44
+ // carries its id; the label stays resolvable and never picks the wrong child.
45
+ const labelled = users.map((u) => ({
46
+ id: u.id,
47
+ label: counts.get(u.label.toLowerCase()) > 1 ? `${u.label} (${u.id})` : u.label,
48
+ }));
49
+ const byLabel = new Map(labelled.map((u) => [u.label.toLowerCase(), u.id]));
50
+ const ids = new Set(labelled.map((u) => u.id));
51
+ const labels = labelled.map((u) => u.label);
52
+ return {
53
+ labels,
54
+ resolve(value) {
55
+ const id = byLabel.get(value.trim().toLowerCase());
56
+ if (id !== undefined)
57
+ return id;
58
+ if (ids.has(value.trim()))
59
+ return value.trim();
60
+ throw new Error(`Unknown observee "${value}". Known: ${labels.join(', ')}.`);
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * List the observees ONCE, at server start, for the planner's `observeeId`
66
+ * enum. Answers null — so the property is published with no `enum` keyword,
67
+ * never an empty one — when the account observes nobody, when the listing
68
+ * fails (no browser for the fetchproxy path, a dead credential), or when it has
69
+ * not answered within `timeoutMs`. Never throws.
70
+ */
71
+ export async function loadObserveeDirectory(client, timeoutMs = OBSERVEE_LIST_TIMEOUT_MS) {
72
+ let timer;
73
+ const deadline = new Promise((resolve) => {
74
+ timer = setTimeout(() => resolve(null), timeoutMs);
75
+ timer.unref();
76
+ });
77
+ const listing = client
78
+ .requestPaginated('/api/v1/users/self/observees')
79
+ .then((data) => buildDirectory(readObservees(data)))
80
+ .catch(() => null);
81
+ try {
82
+ return await Promise.race([listing, deadline]);
83
+ }
84
+ finally {
85
+ clearTimeout(timer);
86
+ }
87
+ }
@@ -1,21 +1,72 @@
1
1
  import { z } from 'zod';
2
- import { textContent, buildPath } from './_shared.js';
3
- const argsSchema = z.object({
4
- observeeId: z.string().optional(),
5
- startDate: z.string().optional(),
6
- endDate: z.string().optional(),
7
- contextCodes: z.array(z.string()).optional(),
8
- filter: z.enum(['new_activity']).optional(),
9
- });
10
- export function registerPlannerTools(server, client) {
2
+ import { textContent, buildPath, schoolMeta } from './_shared.js';
3
+ const OBSERVEE_DESCRIPTION = "A linked observee (child): their display name as listed, or their Canvas user id. Omit for your own planner.";
4
+ /**
5
+ * `observeeId` publishes the observee LABELS as a JSON Schema `enum` when the
6
+ * server could list them at start (mcp-host spec D5), via `.meta()` — so the
7
+ * runtime schema is still any string and the handler accepts a label or an id.
8
+ * With no listing the property is published plain: an empty `enum` is invalid
9
+ * JSON Schema.
10
+ */
11
+ function observeeIdArg(observees) {
12
+ const base = z.string();
13
+ return (observees ? base.meta({ enum: observees.labels }) : base).optional().describe(OBSERVEE_DESCRIPTION);
14
+ }
15
+ const BRIEF_MAX = 120;
16
+ function str(v) {
17
+ return typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined;
18
+ }
19
+ /**
20
+ * One planner item as one line, `title — due <ISO date | no date> — <status>`,
21
+ * at most 120 characters (the title is shortened). Read only from the fields
22
+ * Canvas's planner item carries: `plannable.title`/`name`, `plannable.due_at`
23
+ * (else `plannable_date`), and `submissions`. An item with no submission at
24
+ * all (`submissions: false` — a note, a page, an event) gets no status, since
25
+ * "not submitted" would be untrue of something nobody submits.
26
+ */
27
+ export function briefLine(item) {
28
+ const it = (item && typeof item === 'object' ? item : {});
29
+ const p = (it.plannable && typeof it.plannable === 'object' ? it.plannable : {});
30
+ const title = str(p.title) ?? str(p.name) ?? str(it.title) ?? '(untitled)';
31
+ const due = str(p.due_at) ?? str(it.plannable_date) ?? 'no date';
32
+ const sub = it.submissions;
33
+ let status = '';
34
+ if (sub && typeof sub === 'object') {
35
+ const s = sub;
36
+ status = s.missing === true ? 'missing' : s.submitted === true ? 'submitted' : 'not submitted';
37
+ }
38
+ const tail = ` — due ${due}${status ? ` — ${status}` : ''}`;
39
+ const room = BRIEF_MAX - tail.length;
40
+ const shown = title.length > room ? `${title.slice(0, Math.max(room - 1, 0))}…` : title;
41
+ return `${shown}${tail}`;
42
+ }
43
+ export function registerPlannerTools(server, client, observees) {
44
+ const argsSchema = z.object({
45
+ observeeId: observeeIdArg(observees),
46
+ startDate: z.string().optional(),
47
+ endDate: z.string().optional(),
48
+ contextCodes: z.array(z.string()).optional(),
49
+ filter: z.enum(['new_activity']).optional(),
50
+ view: z.enum(['full', 'brief']).optional().describe('"full" (default) returns Canvas\'s planner items verbatim; "brief" returns one line per item: ' +
51
+ 'title — due <ISO date or "no date"> — <submitted|not submitted|missing>.'),
52
+ });
11
53
  server.registerTool('canvas_list_planner_items', {
12
54
  description: "List planner items (assignments + announcements + planner notes + calendar events) for the user or a linked observee.",
13
55
  annotations: { readOnlyHint: true, openWorldHint: true },
56
+ _meta: schoolMeta('whats-due', {
57
+ canonical: true,
58
+ subject: 'observeeId',
59
+ when: { start: 'startDate', end: 'endDate' },
60
+ aliases: ['homework', 'assignments', 'due', 'kids', 'school'],
61
+ }),
14
62
  inputSchema: argsSchema,
15
63
  }, async (rawArgs) => {
16
64
  const args = argsSchema.parse(rawArgs);
17
- const base = args.observeeId
18
- ? `/api/v1/users/${encodeURIComponent(args.observeeId)}/planner/items`
65
+ const observeeId = args.observeeId !== undefined && observees
66
+ ? observees.resolve(args.observeeId)
67
+ : args.observeeId;
68
+ const base = observeeId
69
+ ? `/api/v1/users/${encodeURIComponent(observeeId)}/planner/items`
19
70
  : '/api/v1/planner/items';
20
71
  const path = buildPath(base, {
21
72
  start_date: args.startDate,
@@ -24,6 +75,9 @@ export function registerPlannerTools(server, client) {
24
75
  filter: args.filter,
25
76
  });
26
77
  const data = await client.requestPaginated(path);
78
+ if (args.view === 'brief') {
79
+ return { content: [{ type: 'text', text: data.map(briefLine).join('\n') }] };
80
+ }
27
81
  return textContent(data);
28
82
  });
29
83
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { schoolMeta } from './_shared.js';
2
3
  import { viewArg, viewResponse } from '../view.js';
3
4
  const argsSchema = z.object({
4
5
  view: viewArg(),
@@ -7,6 +8,7 @@ export function registerProfileTools(server, client) {
7
8
  server.registerTool('canvas_get_profile', {
8
9
  description: "Get the logged-in user's Canvas profile (id, name, primary_email, login_id, locale, time_zone). Useful first call to confirm credentials.",
9
10
  annotations: { readOnlyHint: true, openWorldHint: true },
11
+ _meta: schoolMeta('lookup'),
10
12
  inputSchema: argsSchema,
11
13
  }, async (rawArgs) => {
12
14
  const args = argsSchema.parse(rawArgs);
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { buildPath } from './_shared.js';
2
+ import { buildPath, schoolMeta } from './_shared.js';
3
3
  import { viewArg, viewResponse } from '../view.js';
4
4
  const getArgs = z.object({
5
5
  courseId: z.string(),
@@ -17,6 +17,7 @@ export function registerSubmissionTools(server, client) {
17
17
  server.registerTool('canvas_get_submission', {
18
18
  description: "Get a single submission with rubric assessment and grader comments. userId defaults to 'self'.",
19
19
  annotations: { readOnlyHint: true, openWorldHint: true },
20
+ _meta: schoolMeta('lookup'),
20
21
  inputSchema: getArgs,
21
22
  }, async (rawArgs) => {
22
23
  const args = getArgs.parse(rawArgs);
@@ -28,6 +29,7 @@ export function registerSubmissionTools(server, client) {
28
29
  server.registerTool('canvas_list_recent_submissions', {
29
30
  description: "List recently graded submissions in a course. Defaults to a 14-day window for the calling user.",
30
31
  annotations: { readOnlyHint: true, openWorldHint: true },
32
+ _meta: schoolMeta('lookup'),
31
33
  inputSchema: recentArgs,
32
34
  }, async (rawArgs) => {
33
35
  const args = recentArgs.parse(rawArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-parent-mcp",
3
- "version": "2.1.11",
3
+ "version": "2.2.0",
4
4
  "mcpName": "io.github.chrischall/canvas-parent-mcp",
5
5
  "description": "Canvas LMS (Instructure) MCP server — student/observer read access with optional file download",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/canvas-parent-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "2.1.11",
9
+ "version": "2.2.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "canvas-parent-mcp",
14
- "version": "2.1.11",
14
+ "version": "2.2.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },