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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +10 -0
- package/dist/bundle.js +204 -97
- package/dist/index.js +8 -3
- package/dist/tools/_shared.js +4 -0
- package/dist/tools/announcements.js +2 -1
- package/dist/tools/assignments.js +3 -1
- package/dist/tools/calendar.js +3 -1
- package/dist/tools/conversations.js +3 -1
- package/dist/tools/courses.js +3 -1
- package/dist/tools/discussions.js +2 -1
- package/dist/tools/files.js +3 -1
- package/dist/tools/grades.js +2 -1
- package/dist/tools/observees.js +71 -0
- package/dist/tools/planner.js +65 -11
- package/dist/tools/profile.js +2 -0
- package/dist/tools/submissions.js +3 -1
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
292
|
+
let str2 = "";
|
|
293
293
|
for (let i = 0; i < length; i++) {
|
|
294
|
-
|
|
294
|
+
str2 += chars[Math.floor(Math.random() * chars.length)];
|
|
295
295
|
}
|
|
296
|
-
return
|
|
296
|
+
return str2;
|
|
297
297
|
}
|
|
298
|
-
function esc2(
|
|
299
|
-
return JSON.stringify(
|
|
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(
|
|
415
|
-
return
|
|
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(
|
|
682
|
-
const units =
|
|
683
|
-
if (!highSurrogate2.test(
|
|
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 ((
|
|
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
|
|
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 =
|
|
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 :
|
|
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(
|
|
33734
|
-
return unescapeJsonPointer(decodeURIComponent(
|
|
33733
|
+
function unescapeFragment(str2) {
|
|
33734
|
+
return unescapeJsonPointer(decodeURIComponent(str2));
|
|
33735
33735
|
}
|
|
33736
33736
|
exports.unescapeFragment = unescapeFragment;
|
|
33737
|
-
function escapeFragment(
|
|
33738
|
-
return encodeURIComponent(escapeJsonPointer(
|
|
33737
|
+
function escapeFragment(str2) {
|
|
33738
|
+
return encodeURIComponent(escapeJsonPointer(str2));
|
|
33739
33739
|
}
|
|
33740
33740
|
exports.escapeFragment = escapeFragment;
|
|
33741
|
-
function escapeJsonPointer(
|
|
33742
|
-
if (typeof
|
|
33743
|
-
return
|
|
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(
|
|
33747
|
-
return
|
|
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(
|
|
34634
|
-
return
|
|
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(
|
|
35579
|
+
function findToken(str2, token) {
|
|
35580
35580
|
let ind = 0;
|
|
35581
|
-
for (let i = 0; i <
|
|
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 = (
|
|
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(
|
|
37103
|
-
const len =
|
|
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 =
|
|
37109
|
+
value = str2.charCodeAt(pos++);
|
|
37110
37110
|
if (value >= 55296 && value <= 56319 && pos < len) {
|
|
37111
|
-
value =
|
|
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(
|
|
39823
|
-
const matches = DATE.exec(
|
|
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(
|
|
39839
|
-
const matches = TIME.exec(
|
|
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(
|
|
39877
|
-
const dateTime =
|
|
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(
|
|
39899
|
-
return NOT_URI_FRAGMENT.test(
|
|
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(
|
|
39902
|
+
function byte(str2) {
|
|
39903
39903
|
BYTE.lastIndex = 0;
|
|
39904
|
-
return BYTE.test(
|
|
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(
|
|
39919
|
-
if (Z_ANCHOR.test(
|
|
39918
|
+
function regex(str2) {
|
|
39919
|
+
if (Z_ANCHOR.test(str2)) return false;
|
|
39920
39920
|
try {
|
|
39921
|
-
new RegExp(
|
|
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,
|
|
41535
|
-
let currentArgsSchema =
|
|
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:
|
|
41540
|
+
argsSchema: argsSchema6,
|
|
41541
41541
|
icons,
|
|
41542
41542
|
scopeChallenge,
|
|
41543
41543
|
_meta,
|
|
41544
|
-
handler: createPromptHandler(name,
|
|
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 (
|
|
41576
|
-
const shape = getSchemaShape(
|
|
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:
|
|
41661
|
-
const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(
|
|
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,
|
|
41741
|
-
if (
|
|
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(
|
|
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
|
|
42615
|
+
const str2 = text === null || text === void 0 ? "" : String(text);
|
|
42616
42616
|
const cap = Math.max(ERROR_REDACTION_INPUT_MAX, max);
|
|
42617
|
-
const capped =
|
|
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
|
|
43465
|
+
let str2 = "";
|
|
43466
43466
|
for (let i = 0; i < length; i++) {
|
|
43467
|
-
|
|
43467
|
+
str2 += chars[Math.floor(Math.random() * chars.length)];
|
|
43468
43468
|
}
|
|
43469
|
-
return
|
|
43469
|
+
return str2;
|
|
43470
43470
|
}
|
|
43471
|
-
function esc(
|
|
43472
|
-
return JSON.stringify(
|
|
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(
|
|
43587
|
-
return
|
|
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(
|
|
43854
|
-
const units =
|
|
43855
|
-
if (!highSurrogate.test(
|
|
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 ((
|
|
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.
|
|
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
|
-
|
|
71199
|
-
|
|
71200
|
-
|
|
71201
|
-
|
|
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
|
|
71204
|
-
|
|
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
|
|
71391
|
-
|
|
71392
|
-
|
|
71393
|
-
|
|
71394
|
-
|
|
71395
|
-
|
|
71396
|
-
|
|
71397
|
-
|
|
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
|
-
|
|
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 =
|
|
71404
|
-
const
|
|
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
|
|
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
|
-
|
|
71528
|
+
_meta: schoolMeta("whats-new", { canonical: true, aliases: ["teacher", "announcements", "news"] }),
|
|
71529
|
+
inputSchema: argsSchema4
|
|
71429
71530
|
}, async (rawArgs) => {
|
|
71430
|
-
const args =
|
|
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
|
|
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
|
-
|
|
71596
|
+
_meta: schoolMeta("lookup"),
|
|
71597
|
+
inputSchema: argsSchema5
|
|
71494
71598
|
}, async (rawArgs) => {
|
|
71495
|
-
const args =
|
|
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.
|
|
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.
|
|
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);
|
package/dist/tools/_shared.js
CHANGED
|
@@ -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);
|
package/dist/tools/calendar.js
CHANGED
|
@@ -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);
|
package/dist/tools/courses.js
CHANGED
|
@@ -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);
|
package/dist/tools/files.js
CHANGED
|
@@ -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);
|
package/dist/tools/grades.js
CHANGED
|
@@ -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);
|
package/dist/tools/observees.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/tools/planner.js
CHANGED
|
@@ -1,21 +1,72 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { textContent, buildPath } from './_shared.js';
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
18
|
-
?
|
|
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
|
}
|
package/dist/tools/profile.js
CHANGED
|
@@ -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.
|
|
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.
|
|
9
|
+
"version": "2.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "canvas-parent-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.2.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|