canvas-parent-mcp 2.1.2 → 2.1.3
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 +2 -0
- package/dist/bundle.js +246 -125
- package/dist/client.js +87 -7
- package/dist/index.js +1 -1
- package/dist/tools/files.js +4 -4
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/canvas-parent/SKILL.md +1 -1
- package/skills/canvas-parent-api/references/canvas-endpoints.md +3 -1
|
@@ -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.
|
|
10
|
+
"version": "2.1.3"
|
|
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.
|
|
18
|
+
"version": "2.1.3",
|
|
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.
|
|
4
|
+
"version": "2.1.3",
|
|
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,8 @@ 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
|
+
`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.
|
|
33
|
+
|
|
32
34
|
## Configuration
|
|
33
35
|
|
|
34
36
|
Set `CANVAS_BASE_URL` plus one of four auth modes. `canvas-parent-mcp` tries them in priority order:
|
package/dist/bundle.js
CHANGED
|
@@ -4858,7 +4858,7 @@ var require_memoizer = __commonJS({
|
|
|
4858
4858
|
var NONE2 = 0;
|
|
4859
4859
|
var ASSUMED2 = 1;
|
|
4860
4860
|
var PROVEN2 = 2;
|
|
4861
|
-
function isRecursive2(inst, stack,
|
|
4861
|
+
function isRecursive2(inst, stack, resolve5) {
|
|
4862
4862
|
const cached2 = recursive2.get(inst);
|
|
4863
4863
|
if (cached2 !== void 0)
|
|
4864
4864
|
return cached2 ? PROVEN2 : NONE2;
|
|
@@ -4868,7 +4868,7 @@ var require_memoizer = __commonJS({
|
|
|
4868
4868
|
let result = NONE2;
|
|
4869
4869
|
const check2 = (child) => {
|
|
4870
4870
|
if (result !== PROVEN2 && child?._zod) {
|
|
4871
|
-
const answer = isRecursive2(child, stack,
|
|
4871
|
+
const answer = isRecursive2(child, stack, resolve5);
|
|
4872
4872
|
if (answer > result)
|
|
4873
4873
|
result = answer;
|
|
4874
4874
|
}
|
|
@@ -4879,7 +4879,7 @@ var require_memoizer = __commonJS({
|
|
|
4879
4879
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
4880
4880
|
if (spread && !desc.enumerable)
|
|
4881
4881
|
continue;
|
|
4882
|
-
const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack,
|
|
4882
|
+
const child = desc.get ? ASSUMED2 : desc.value?._zod ? isRecursive2(desc.value, stack, resolve5) : NONE2;
|
|
4883
4883
|
if (child > answer)
|
|
4884
4884
|
answer = child;
|
|
4885
4885
|
}
|
|
@@ -4943,7 +4943,7 @@ var require_memoizer = __commonJS({
|
|
|
4943
4943
|
break;
|
|
4944
4944
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
4945
4945
|
case "lazy": {
|
|
4946
|
-
const inner = def._cachedInner ?? (
|
|
4946
|
+
const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
|
|
4947
4947
|
merge2(inner ? isRecursive2(inner, stack, false) : ASSUMED2);
|
|
4948
4948
|
break;
|
|
4949
4949
|
}
|
|
@@ -26980,7 +26980,7 @@ var require_websocket_server = __commonJS({
|
|
|
26980
26980
|
});
|
|
26981
26981
|
|
|
26982
26982
|
// src/index.ts
|
|
26983
|
-
import { dirname as
|
|
26983
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
26984
26984
|
import { fileURLToPath } from "url";
|
|
26985
26985
|
|
|
26986
26986
|
// node_modules/@chrischall/mcp-utils/dist/cancel/index.js
|
|
@@ -31399,14 +31399,14 @@ function inputRequiredRoundsExceededMessage(method, maxRounds) {
|
|
|
31399
31399
|
return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
|
|
31400
31400
|
}
|
|
31401
31401
|
function sleep(ms, signal) {
|
|
31402
|
-
return new Promise((
|
|
31402
|
+
return new Promise((resolve5, reject) => {
|
|
31403
31403
|
if (signal?.aborted) {
|
|
31404
31404
|
reject(signal.reason instanceof SdkError ? signal.reason : new SdkError(SdkErrorCode.RequestTimeout, String(signal.reason)));
|
|
31405
31405
|
return;
|
|
31406
31406
|
}
|
|
31407
31407
|
const timer = setTimeout(() => {
|
|
31408
31408
|
signal?.removeEventListener("abort", onAbort);
|
|
31409
|
-
|
|
31409
|
+
resolve5();
|
|
31410
31410
|
}, ms);
|
|
31411
31411
|
const onAbort = () => {
|
|
31412
31412
|
clearTimeout(timer);
|
|
@@ -32200,7 +32200,7 @@ var Protocol = class {
|
|
|
32200
32200
|
const flowStartedAt = Date.now();
|
|
32201
32201
|
let onAbort;
|
|
32202
32202
|
let cleanupMessageId;
|
|
32203
|
-
return new Promise((
|
|
32203
|
+
return new Promise((resolve5, reject) => {
|
|
32204
32204
|
const earlyReject = (error62) => {
|
|
32205
32205
|
reject(error62);
|
|
32206
32206
|
};
|
|
@@ -32268,7 +32268,7 @@ var Protocol = class {
|
|
|
32268
32268
|
}
|
|
32269
32269
|
if (decoded.kind === "invalid") return reject(decoded.error);
|
|
32270
32270
|
if (decoded.kind === "input_required") {
|
|
32271
|
-
if (options?.allowInputRequired === true) return
|
|
32271
|
+
if (options?.allowInputRequired === true) return resolve5(manualInputRequiredValue(decoded));
|
|
32272
32272
|
const flow = {
|
|
32273
32273
|
codec: codec2,
|
|
32274
32274
|
request,
|
|
@@ -32280,11 +32280,11 @@ var Protocol = class {
|
|
|
32280
32280
|
params
|
|
32281
32281
|
}, resultSchema, legOptions)
|
|
32282
32282
|
};
|
|
32283
|
-
return
|
|
32283
|
+
return resolve5(this._resolveNonCompleteResult(decoded, flow));
|
|
32284
32284
|
}
|
|
32285
32285
|
const result = decoded.result;
|
|
32286
32286
|
validateStandardSchema(resultSchema, result).then((parseResult) => {
|
|
32287
|
-
if (parseResult.success)
|
|
32287
|
+
if (parseResult.success) resolve5(parseResult.data);
|
|
32288
32288
|
else reject(new SdkError(SdkErrorCode.InvalidResult, `Invalid result for ${request.method}: ${parseResult.error}`));
|
|
32289
32289
|
}, reject);
|
|
32290
32290
|
});
|
|
@@ -35196,7 +35196,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
35196
35196
|
ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref);
|
|
35197
35197
|
const schOrFunc = root.refs[ref];
|
|
35198
35198
|
if (schOrFunc) return schOrFunc;
|
|
35199
|
-
let _sch =
|
|
35199
|
+
let _sch = resolve5.call(this, root, ref);
|
|
35200
35200
|
if (_sch === void 0) {
|
|
35201
35201
|
const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
|
|
35202
35202
|
const { schemaId } = this.opts;
|
|
@@ -35222,7 +35222,7 @@ var require_compile2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
35222
35222
|
function sameSchemaEnv(s1, s2) {
|
|
35223
35223
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
35224
35224
|
}
|
|
35225
|
-
function
|
|
35225
|
+
function resolve5(root, ref) {
|
|
35226
35226
|
let sch;
|
|
35227
35227
|
while (typeof (sch = this.refs[ref]) == "string") ref = sch;
|
|
35228
35228
|
return sch || this.schemas[ref] || resolveSchema.call(this, root, ref);
|
|
@@ -35672,47 +35672,47 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
35672
35672
|
else if (typeof uri === "object") uri = parse3(serialize(uri, options), options);
|
|
35673
35673
|
return uri;
|
|
35674
35674
|
}
|
|
35675
|
-
function
|
|
35675
|
+
function resolve5(baseURI, relativeURI, options) {
|
|
35676
35676
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
35677
35677
|
const resolved2 = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
35678
35678
|
schemelessOptions.skipEscape = true;
|
|
35679
35679
|
return serialize(resolved2, schemelessOptions);
|
|
35680
35680
|
}
|
|
35681
|
-
function resolveComponent(base,
|
|
35681
|
+
function resolveComponent(base, relative2, options, skipNormalization) {
|
|
35682
35682
|
const target = {};
|
|
35683
35683
|
if (!skipNormalization) {
|
|
35684
35684
|
base = parse3(serialize(base, options), options);
|
|
35685
|
-
|
|
35685
|
+
relative2 = parse3(serialize(relative2, options), options);
|
|
35686
35686
|
}
|
|
35687
35687
|
options = options || {};
|
|
35688
|
-
if (!options.tolerant &&
|
|
35689
|
-
target.scheme =
|
|
35690
|
-
target.userinfo =
|
|
35691
|
-
target.host =
|
|
35692
|
-
target.port =
|
|
35693
|
-
target.path = removeDotSegments(
|
|
35694
|
-
target.query =
|
|
35688
|
+
if (!options.tolerant && relative2.scheme) {
|
|
35689
|
+
target.scheme = relative2.scheme;
|
|
35690
|
+
target.userinfo = relative2.userinfo;
|
|
35691
|
+
target.host = relative2.host;
|
|
35692
|
+
target.port = relative2.port;
|
|
35693
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
35694
|
+
target.query = relative2.query;
|
|
35695
35695
|
} else {
|
|
35696
|
-
if (
|
|
35697
|
-
target.userinfo =
|
|
35698
|
-
target.host =
|
|
35699
|
-
target.port =
|
|
35700
|
-
target.path = removeDotSegments(
|
|
35701
|
-
target.query =
|
|
35696
|
+
if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
|
|
35697
|
+
target.userinfo = relative2.userinfo;
|
|
35698
|
+
target.host = relative2.host;
|
|
35699
|
+
target.port = relative2.port;
|
|
35700
|
+
target.path = removeDotSegments(relative2.path || "");
|
|
35701
|
+
target.query = relative2.query;
|
|
35702
35702
|
} else {
|
|
35703
|
-
if (!
|
|
35703
|
+
if (!relative2.path) {
|
|
35704
35704
|
target.path = base.path;
|
|
35705
|
-
if (
|
|
35705
|
+
if (relative2.query !== void 0) target.query = relative2.query;
|
|
35706
35706
|
else target.query = base.query;
|
|
35707
35707
|
} else {
|
|
35708
|
-
if (
|
|
35708
|
+
if (relative2.path[0] === "/") target.path = removeDotSegments(relative2.path);
|
|
35709
35709
|
else {
|
|
35710
|
-
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" +
|
|
35711
|
-
else if (!base.path) target.path =
|
|
35712
|
-
else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) +
|
|
35710
|
+
if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) target.path = "/" + relative2.path;
|
|
35711
|
+
else if (!base.path) target.path = relative2.path;
|
|
35712
|
+
else target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
|
|
35713
35713
|
target.path = removeDotSegments(target.path);
|
|
35714
35714
|
}
|
|
35715
|
-
target.query =
|
|
35715
|
+
target.query = relative2.query;
|
|
35716
35716
|
}
|
|
35717
35717
|
target.userinfo = base.userinfo;
|
|
35718
35718
|
target.host = base.host;
|
|
@@ -35720,7 +35720,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
35720
35720
|
}
|
|
35721
35721
|
target.scheme = base.scheme;
|
|
35722
35722
|
}
|
|
35723
|
-
target.fragment =
|
|
35723
|
+
target.fragment = relative2.fragment;
|
|
35724
35724
|
return target;
|
|
35725
35725
|
}
|
|
35726
35726
|
function equal(uriA, uriB, options) {
|
|
@@ -35846,7 +35846,7 @@ var require_fast_uri = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
35846
35846
|
const fastUri = {
|
|
35847
35847
|
SCHEMES,
|
|
35848
35848
|
normalize,
|
|
35849
|
-
resolve:
|
|
35849
|
+
resolve: resolve5,
|
|
35850
35850
|
resolveComponent,
|
|
35851
35851
|
equal,
|
|
35852
35852
|
serialize,
|
|
@@ -41219,7 +41219,7 @@ var StdioServerTransport = class {
|
|
|
41219
41219
|
}
|
|
41220
41220
|
send(message) {
|
|
41221
41221
|
if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
|
|
41222
|
-
return new Promise((
|
|
41222
|
+
return new Promise((resolve5, reject) => {
|
|
41223
41223
|
const json2 = serializeMessage(message);
|
|
41224
41224
|
let settled = false;
|
|
41225
41225
|
const onError = (error62) => {
|
|
@@ -41234,14 +41234,14 @@ var StdioServerTransport = class {
|
|
|
41234
41234
|
settled = true;
|
|
41235
41235
|
this._stdout.off("error", onError);
|
|
41236
41236
|
this._stdout.off("drain", onDrain);
|
|
41237
|
-
|
|
41237
|
+
resolve5();
|
|
41238
41238
|
};
|
|
41239
41239
|
this._stdout.once("error", onError);
|
|
41240
41240
|
if (this._stdout.write(json2)) {
|
|
41241
41241
|
if (settled) return;
|
|
41242
41242
|
settled = true;
|
|
41243
41243
|
this._stdout.off("error", onError);
|
|
41244
|
-
|
|
41244
|
+
resolve5();
|
|
41245
41245
|
} else if (!settled) this._stdout.once("drain", onDrain);
|
|
41246
41246
|
});
|
|
41247
41247
|
}
|
|
@@ -41295,14 +41295,14 @@ var StdioConnectionChannel = class {
|
|
|
41295
41295
|
*/
|
|
41296
41296
|
async whenRequestsAnswered(timeoutMs) {
|
|
41297
41297
|
if (this._closed || this._pendingRequests.size === 0) return true;
|
|
41298
|
-
return await new Promise((
|
|
41298
|
+
return await new Promise((resolve5) => {
|
|
41299
41299
|
const waiter = () => {
|
|
41300
41300
|
clearTimeout(timer);
|
|
41301
|
-
|
|
41301
|
+
resolve5(true);
|
|
41302
41302
|
};
|
|
41303
41303
|
const timer = setTimeout(() => {
|
|
41304
41304
|
this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
|
|
41305
|
-
|
|
41305
|
+
resolve5(false);
|
|
41306
41306
|
}, timeoutMs);
|
|
41307
41307
|
this._drainWaiters.push(waiter);
|
|
41308
41308
|
});
|
|
@@ -46738,7 +46738,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
46738
46738
|
var NONE = 0;
|
|
46739
46739
|
var ASSUMED = 1;
|
|
46740
46740
|
var PROVEN = 2;
|
|
46741
|
-
function isRecursive(inst, stack,
|
|
46741
|
+
function isRecursive(inst, stack, resolve5) {
|
|
46742
46742
|
const cached2 = recursive.get(inst);
|
|
46743
46743
|
if (cached2 !== void 0)
|
|
46744
46744
|
return cached2 ? PROVEN : NONE;
|
|
@@ -46748,7 +46748,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
46748
46748
|
let result = NONE;
|
|
46749
46749
|
const check2 = (child) => {
|
|
46750
46750
|
if (result !== PROVEN && child?._zod) {
|
|
46751
|
-
const answer = isRecursive(child, stack,
|
|
46751
|
+
const answer = isRecursive(child, stack, resolve5);
|
|
46752
46752
|
if (answer > result)
|
|
46753
46753
|
result = answer;
|
|
46754
46754
|
}
|
|
@@ -46759,7 +46759,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
46759
46759
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
46760
46760
|
if (spread && !desc.enumerable)
|
|
46761
46761
|
continue;
|
|
46762
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
46762
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
|
|
46763
46763
|
if (child > answer)
|
|
46764
46764
|
answer = child;
|
|
46765
46765
|
}
|
|
@@ -46823,7 +46823,7 @@ function isRecursive(inst, stack, resolve2) {
|
|
|
46823
46823
|
break;
|
|
46824
46824
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
46825
46825
|
case "lazy": {
|
|
46826
|
-
const inner = def._cachedInner ?? (
|
|
46826
|
+
const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
|
|
46827
46827
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
46828
46828
|
break;
|
|
46829
46829
|
}
|
|
@@ -61639,6 +61639,8 @@ function runMcp(opts) {
|
|
|
61639
61639
|
}
|
|
61640
61640
|
|
|
61641
61641
|
// node_modules/@chrischall/mcp-utils/dist/config/index.js
|
|
61642
|
+
import { homedir } from "node:os";
|
|
61643
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
61642
61644
|
var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
|
|
61643
61645
|
function readEnvVar(key, opts = {}) {
|
|
61644
61646
|
const env = opts.env ?? process.env;
|
|
@@ -61665,6 +61667,15 @@ function parseBoolEnv(key, opts = {}) {
|
|
|
61665
61667
|
return false;
|
|
61666
61668
|
return fallback;
|
|
61667
61669
|
}
|
|
61670
|
+
function expandPath(p) {
|
|
61671
|
+
let expanded = p;
|
|
61672
|
+
if (p === "~") {
|
|
61673
|
+
expanded = homedir();
|
|
61674
|
+
} else if (p.startsWith("~/")) {
|
|
61675
|
+
expanded = join(homedir(), p.slice(2));
|
|
61676
|
+
}
|
|
61677
|
+
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
61678
|
+
}
|
|
61668
61679
|
async function loadDotenvSafely(opts = {}) {
|
|
61669
61680
|
try {
|
|
61670
61681
|
const mod = await import(
|
|
@@ -61682,6 +61693,41 @@ async function loadDotenvSafely(opts = {}) {
|
|
|
61682
61693
|
}
|
|
61683
61694
|
}
|
|
61684
61695
|
|
|
61696
|
+
// node_modules/@chrischall/mcp-utils/dist/fs/confine.js
|
|
61697
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
61698
|
+
import { dirname, isAbsolute as isAbsolute2, relative, resolve as resolve2, sep } from "node:path";
|
|
61699
|
+
function isInside(root, target) {
|
|
61700
|
+
const rel = relative(root, target);
|
|
61701
|
+
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute2(rel);
|
|
61702
|
+
}
|
|
61703
|
+
function realPathAllowingMissing(p) {
|
|
61704
|
+
const abs = resolve2(p);
|
|
61705
|
+
let existing = abs;
|
|
61706
|
+
const tail = [];
|
|
61707
|
+
while (!existsSync(existing)) {
|
|
61708
|
+
const parent = dirname(existing);
|
|
61709
|
+
if (parent === existing)
|
|
61710
|
+
break;
|
|
61711
|
+
tail.unshift(existing.slice(parent.length).replace(/^[/\\]+/, ""));
|
|
61712
|
+
existing = parent;
|
|
61713
|
+
}
|
|
61714
|
+
let real;
|
|
61715
|
+
try {
|
|
61716
|
+
real = realpathSync(existing);
|
|
61717
|
+
} catch {
|
|
61718
|
+
real = existing;
|
|
61719
|
+
}
|
|
61720
|
+
return tail.length > 0 ? resolve2(real, ...tail) : real;
|
|
61721
|
+
}
|
|
61722
|
+
function assertPathWithinRoots(path, roots) {
|
|
61723
|
+
const target = realPathAllowingMissing(expandPath(path));
|
|
61724
|
+
for (const root of roots) {
|
|
61725
|
+
if (isInside(realPathAllowingMissing(expandPath(root)), target))
|
|
61726
|
+
return target;
|
|
61727
|
+
}
|
|
61728
|
+
throw new Error(`Path is outside the allowed directories: ${path}`);
|
|
61729
|
+
}
|
|
61730
|
+
|
|
61685
61731
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
61686
61732
|
function buildQueryString(params) {
|
|
61687
61733
|
const parts = [];
|
|
@@ -63495,7 +63541,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
|
|
|
63495
63541
|
async function electRole(opts) {
|
|
63496
63542
|
const server = createServer();
|
|
63497
63543
|
const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
|
|
63498
|
-
return new Promise((
|
|
63544
|
+
return new Promise((resolve5, reject) => {
|
|
63499
63545
|
let timer;
|
|
63500
63546
|
const clearBindTimer = () => {
|
|
63501
63547
|
if (timer) {
|
|
@@ -63511,7 +63557,7 @@ async function electRole(opts) {
|
|
|
63511
63557
|
server.close();
|
|
63512
63558
|
} catch {
|
|
63513
63559
|
}
|
|
63514
|
-
|
|
63560
|
+
resolve5({ role: "peer" });
|
|
63515
63561
|
} else {
|
|
63516
63562
|
reject(e);
|
|
63517
63563
|
}
|
|
@@ -63519,7 +63565,7 @@ async function electRole(opts) {
|
|
|
63519
63565
|
const onListening = () => {
|
|
63520
63566
|
clearBindTimer();
|
|
63521
63567
|
server.removeListener("error", onError);
|
|
63522
|
-
|
|
63568
|
+
resolve5({ role: "host", server });
|
|
63523
63569
|
};
|
|
63524
63570
|
server.once("error", onError);
|
|
63525
63571
|
server.once("listening", onListening);
|
|
@@ -63798,13 +63844,13 @@ async function awaitSessionReady(ready, opts) {
|
|
|
63798
63844
|
// node_modules/@fetchproxy/server/dist/extension-trust.js
|
|
63799
63845
|
import { readFile as readFile2, writeFile as writeFile2, rename as rename2, unlink as unlink2, mkdir as mkdir2, chmod as chmod2, rm as rm2 } from "node:fs/promises";
|
|
63800
63846
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
63801
|
-
import { isAbsolute as
|
|
63847
|
+
import { isAbsolute as isAbsolute4, join as join3 } from "node:path";
|
|
63802
63848
|
|
|
63803
63849
|
// node_modules/@fetchproxy/server/dist/identity.js
|
|
63804
63850
|
import { readFile, readdir, writeFile, mkdir, chmod, rename, rm, unlink, link, lstat } from "node:fs/promises";
|
|
63805
63851
|
import { randomBytes } from "node:crypto";
|
|
63806
|
-
import { isAbsolute, join } from "node:path";
|
|
63807
|
-
import { homedir } from "node:os";
|
|
63852
|
+
import { isAbsolute as isAbsolute3, join as join2 } from "node:path";
|
|
63853
|
+
import { homedir as homedir2 } from "node:os";
|
|
63808
63854
|
var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
|
|
63809
63855
|
var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
63810
63856
|
function envIdentityDir() {
|
|
@@ -63812,12 +63858,12 @@ function envIdentityDir() {
|
|
|
63812
63858
|
if (raw === void 0)
|
|
63813
63859
|
return void 0;
|
|
63814
63860
|
const dir = raw.trim();
|
|
63815
|
-
if (dir === "" || !
|
|
63861
|
+
if (dir === "" || !isAbsolute3(dir))
|
|
63816
63862
|
return void 0;
|
|
63817
63863
|
return dir;
|
|
63818
63864
|
}
|
|
63819
63865
|
function defaultIdentityDir() {
|
|
63820
|
-
return envIdentityDir() ??
|
|
63866
|
+
return envIdentityDir() ?? join2(homedir2(), ".fetchproxy", "identity");
|
|
63821
63867
|
}
|
|
63822
63868
|
function safeIdentityFileBase(serverName) {
|
|
63823
63869
|
if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
|
|
@@ -63826,7 +63872,7 @@ function safeIdentityFileBase(serverName) {
|
|
|
63826
63872
|
return serverName.replace(/\//g, "_");
|
|
63827
63873
|
}
|
|
63828
63874
|
function identityFilePath(dir, serverName) {
|
|
63829
|
-
return
|
|
63875
|
+
return join2(dir, `${safeIdentityFileBase(serverName)}.json`);
|
|
63830
63876
|
}
|
|
63831
63877
|
async function generateIdentity() {
|
|
63832
63878
|
const x = await generateX25519();
|
|
@@ -63880,7 +63926,7 @@ async function sweepStagingFiles(dir, serverName, opts = {}) {
|
|
|
63880
63926
|
}
|
|
63881
63927
|
const cutoff = opts.olderThanMs === void 0 ? null : Date.now() - opts.olderThanMs;
|
|
63882
63928
|
await Promise.all(names.filter((name) => staging.test(name)).map(async (name) => {
|
|
63883
|
-
const path =
|
|
63929
|
+
const path = join2(dir, name);
|
|
63884
63930
|
if (cutoff !== null) {
|
|
63885
63931
|
const st = await lstat(path).catch(() => null);
|
|
63886
63932
|
if (st === null || st.mtimeMs > cutoff)
|
|
@@ -63961,7 +64007,7 @@ function envTrustDir(env = process.env) {
|
|
|
63961
64007
|
if (raw === void 0)
|
|
63962
64008
|
return void 0;
|
|
63963
64009
|
const dir = raw.trim();
|
|
63964
|
-
if (dir !== "" &&
|
|
64010
|
+
if (dir !== "" && isAbsolute4(dir))
|
|
63965
64011
|
return dir;
|
|
63966
64012
|
console.warn(`[fetchproxy] ignoring ${TRUST_DIR_ENV}=${JSON.stringify(raw)}: it is not an absolute path. The extension pin will be written beside the identity instead, which is not where you pointed it \u2014 if that directory is read-only, no pin is ever kept.`);
|
|
63967
64013
|
return void 0;
|
|
@@ -64009,13 +64055,13 @@ function decideExtensionTrust(args) {
|
|
|
64009
64055
|
};
|
|
64010
64056
|
}
|
|
64011
64057
|
function extensionTrustPath(serverName, dir = defaultTrustDir()) {
|
|
64012
|
-
return
|
|
64058
|
+
return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
|
|
64013
64059
|
}
|
|
64014
64060
|
function extensionTrustPathHint(serverName) {
|
|
64015
64061
|
try {
|
|
64016
64062
|
return extensionTrustPath(serverName);
|
|
64017
64063
|
} catch {
|
|
64018
|
-
return
|
|
64064
|
+
return join3(defaultTrustDir(), "<server-name>.extension-trust.json");
|
|
64019
64065
|
}
|
|
64020
64066
|
}
|
|
64021
64067
|
function isPin(x) {
|
|
@@ -64168,8 +64214,8 @@ async function startHost(opts) {
|
|
|
64168
64214
|
let rejectOwnSession;
|
|
64169
64215
|
let ownSessionReady;
|
|
64170
64216
|
function resetSessionPromise() {
|
|
64171
|
-
ownSessionReady = new Promise((
|
|
64172
|
-
resolveOwnSession =
|
|
64217
|
+
ownSessionReady = new Promise((resolve5, reject) => {
|
|
64218
|
+
resolveOwnSession = resolve5;
|
|
64173
64219
|
rejectOwnSession = reject;
|
|
64174
64220
|
});
|
|
64175
64221
|
ownSessionReady.catch(() => {
|
|
@@ -64571,7 +64617,7 @@ async function startHost(opts) {
|
|
|
64571
64617
|
});
|
|
64572
64618
|
});
|
|
64573
64619
|
return {
|
|
64574
|
-
close: () => new Promise((
|
|
64620
|
+
close: () => new Promise((resolve5) => {
|
|
64575
64621
|
for (const client of wss.clients) {
|
|
64576
64622
|
try {
|
|
64577
64623
|
client.terminate();
|
|
@@ -64579,7 +64625,7 @@ async function startHost(opts) {
|
|
|
64579
64625
|
}
|
|
64580
64626
|
}
|
|
64581
64627
|
wss.close(() => {
|
|
64582
|
-
opts.httpServer.close(() =>
|
|
64628
|
+
opts.httpServer.close(() => resolve5());
|
|
64583
64629
|
});
|
|
64584
64630
|
}),
|
|
64585
64631
|
sendOwnInner: async (inner) => {
|
|
@@ -64619,11 +64665,11 @@ async function startPeer(opts) {
|
|
|
64619
64665
|
maxPayload: opts.maxPayloadBytes ?? MAX_PAYLOAD_BYTES
|
|
64620
64666
|
});
|
|
64621
64667
|
const dialTimeoutMs = opts.dialTimeoutMs ?? PEER_DIAL_TIMEOUT_MS;
|
|
64622
|
-
await new Promise((
|
|
64668
|
+
await new Promise((resolve5, reject) => {
|
|
64623
64669
|
let timer;
|
|
64624
64670
|
const onOpen = () => {
|
|
64625
64671
|
cleanup();
|
|
64626
|
-
|
|
64672
|
+
resolve5();
|
|
64627
64673
|
};
|
|
64628
64674
|
const onError = (e) => {
|
|
64629
64675
|
cleanup();
|
|
@@ -64711,10 +64757,10 @@ async function startPeer(opts) {
|
|
|
64711
64757
|
let sessionPromiseSettled = false;
|
|
64712
64758
|
function resetSessionPromise() {
|
|
64713
64759
|
sessionPromiseSettled = false;
|
|
64714
|
-
sessionPromise = new Promise((
|
|
64760
|
+
sessionPromise = new Promise((resolve5, reject) => {
|
|
64715
64761
|
resolveFirstReady = (st) => {
|
|
64716
64762
|
sessionPromiseSettled = true;
|
|
64717
|
-
|
|
64763
|
+
resolve5(st);
|
|
64718
64764
|
};
|
|
64719
64765
|
rejectFirstReady = (e) => {
|
|
64720
64766
|
sessionPromiseSettled = true;
|
|
@@ -65864,8 +65910,8 @@ var FetchproxyServer = class {
|
|
|
65864
65910
|
async _fetchOnceWithTimeout(init, fetchOpts = {}) {
|
|
65865
65911
|
const id = this.nextRequestId++;
|
|
65866
65912
|
const inner = { type: "request", id, op: "fetch", init };
|
|
65867
|
-
const pending = this.guardPending(new Promise((
|
|
65868
|
-
this.pending.set(id,
|
|
65913
|
+
const pending = this.guardPending(new Promise((resolve5) => {
|
|
65914
|
+
this.pending.set(id, resolve5);
|
|
65869
65915
|
}));
|
|
65870
65916
|
try {
|
|
65871
65917
|
await this.sendInnerFrame(inner, {
|
|
@@ -65888,12 +65934,12 @@ var FetchproxyServer = class {
|
|
|
65888
65934
|
try {
|
|
65889
65935
|
return await Promise.race([
|
|
65890
65936
|
pending,
|
|
65891
|
-
new Promise((
|
|
65937
|
+
new Promise((resolve5) => {
|
|
65892
65938
|
timer = setTimeout(() => {
|
|
65893
65939
|
this.pending.delete(id);
|
|
65894
65940
|
const elapsedMs = Date.now() - start;
|
|
65895
65941
|
const error62 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
|
|
65896
|
-
|
|
65942
|
+
resolve5({
|
|
65897
65943
|
ok: false,
|
|
65898
65944
|
error: error62,
|
|
65899
65945
|
kind: "timeout",
|
|
@@ -66037,9 +66083,9 @@ var FetchproxyServer = class {
|
|
|
66037
66083
|
if (opts.subdomain !== void 0)
|
|
66038
66084
|
assertSubdomainLabel(opts.subdomain);
|
|
66039
66085
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
66040
|
-
const
|
|
66086
|
+
const isAbsolute6 = path.startsWith("http://") || path.startsWith("https://");
|
|
66041
66087
|
let host;
|
|
66042
|
-
if (
|
|
66088
|
+
if (isAbsolute6) {
|
|
66043
66089
|
try {
|
|
66044
66090
|
host = new URL(path).host;
|
|
66045
66091
|
} catch {
|
|
@@ -66048,7 +66094,7 @@ var FetchproxyServer = class {
|
|
|
66048
66094
|
} else {
|
|
66049
66095
|
host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
|
|
66050
66096
|
}
|
|
66051
|
-
const url4 =
|
|
66097
|
+
const url4 = isAbsolute6 ? path : `https://${host}${path}`;
|
|
66052
66098
|
assertUrlInDomains("request url", url4, this.opts.domains);
|
|
66053
66099
|
let tabUrl = `https://${host}/`;
|
|
66054
66100
|
if (opts.viaTab !== void 0) {
|
|
@@ -66313,8 +66359,8 @@ var FetchproxyServer = class {
|
|
|
66313
66359
|
const tabUrl = `https://${host}/`;
|
|
66314
66360
|
inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
|
|
66315
66361
|
}
|
|
66316
|
-
const pending = this.guardPending(new Promise((
|
|
66317
|
-
this.pendingReadCookies.set(id,
|
|
66362
|
+
const pending = this.guardPending(new Promise((resolve5) => {
|
|
66363
|
+
this.pendingReadCookies.set(id, resolve5);
|
|
66318
66364
|
}));
|
|
66319
66365
|
await this.sendInnerFrame(inner);
|
|
66320
66366
|
const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
|
|
@@ -66372,8 +66418,8 @@ var FetchproxyServer = class {
|
|
|
66372
66418
|
...cookiePath !== void 0 ? { path: cookiePath } : {}
|
|
66373
66419
|
}
|
|
66374
66420
|
};
|
|
66375
|
-
const pending = this.guardPending(new Promise((
|
|
66376
|
-
this.pendingWriteCookies.set(id, { resolve:
|
|
66421
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66422
|
+
this.pendingWriteCookies.set(id, { resolve: resolve5, reject });
|
|
66377
66423
|
}));
|
|
66378
66424
|
await this.sendInnerFrame(inner);
|
|
66379
66425
|
return this._withVerbTimeout(
|
|
@@ -66443,8 +66489,8 @@ var FetchproxyServer = class {
|
|
|
66443
66489
|
...opts.pointers ? { pointers: { ...opts.pointers } } : {}
|
|
66444
66490
|
}
|
|
66445
66491
|
};
|
|
66446
|
-
const pending = this.guardPending(new Promise((
|
|
66447
|
-
this.pendingStorage.set(id, { resolve:
|
|
66492
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66493
|
+
this.pendingStorage.set(id, { resolve: resolve5, reject });
|
|
66448
66494
|
}));
|
|
66449
66495
|
await this.sendInnerFrame(inner);
|
|
66450
66496
|
return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
|
|
@@ -66502,8 +66548,8 @@ var FetchproxyServer = class {
|
|
|
66502
66548
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
66503
66549
|
}
|
|
66504
66550
|
};
|
|
66505
|
-
const pending = this.guardPending(new Promise((
|
|
66506
|
-
this.pendingCapture.set(id, { resolve:
|
|
66551
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66552
|
+
this.pendingCapture.set(id, { resolve: resolve5, reject });
|
|
66507
66553
|
}));
|
|
66508
66554
|
await this.sendInnerFrame(inner);
|
|
66509
66555
|
return this._withVerbTimeout(
|
|
@@ -66550,8 +66596,8 @@ var FetchproxyServer = class {
|
|
|
66550
66596
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
66551
66597
|
}
|
|
66552
66598
|
};
|
|
66553
|
-
const pending = this.guardPending(new Promise((
|
|
66554
|
-
this.pendingRedirect.set(id, { resolve:
|
|
66599
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66600
|
+
this.pendingRedirect.set(id, { resolve: resolve5, reject });
|
|
66555
66601
|
}));
|
|
66556
66602
|
await this.sendInnerFrame(inner);
|
|
66557
66603
|
return this._withVerbTimeout(
|
|
@@ -66646,8 +66692,8 @@ var FetchproxyServer = class {
|
|
|
66646
66692
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
66647
66693
|
}
|
|
66648
66694
|
};
|
|
66649
|
-
const pending = this.guardPending(new Promise((
|
|
66650
|
-
this.pendingDownload.set(id, { resolve:
|
|
66695
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66696
|
+
this.pendingDownload.set(id, { resolve: resolve5, reject });
|
|
66651
66697
|
}));
|
|
66652
66698
|
await this.sendInnerFrame(inner);
|
|
66653
66699
|
return this._withVerbTimeout(
|
|
@@ -66707,8 +66753,8 @@ var FetchproxyServer = class {
|
|
|
66707
66753
|
keys: [...opts.keys]
|
|
66708
66754
|
}
|
|
66709
66755
|
};
|
|
66710
|
-
const pending = this.guardPending(new Promise((
|
|
66711
|
-
this.pendingIdb.set(id, { resolve:
|
|
66756
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66757
|
+
this.pendingIdb.set(id, { resolve: resolve5, reject });
|
|
66712
66758
|
}));
|
|
66713
66759
|
await this.sendInnerFrame(inner);
|
|
66714
66760
|
return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
|
|
@@ -66747,8 +66793,8 @@ var FetchproxyServer = class {
|
|
|
66747
66793
|
op: "read_dom",
|
|
66748
66794
|
init: { origin, names: [...opts.names] }
|
|
66749
66795
|
};
|
|
66750
|
-
const pending = this.guardPending(new Promise((
|
|
66751
|
-
this.pendingStorage.set(id, { resolve:
|
|
66796
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66797
|
+
this.pendingStorage.set(id, { resolve: resolve5, reject });
|
|
66752
66798
|
}));
|
|
66753
66799
|
await this.sendInnerFrame(inner);
|
|
66754
66800
|
return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
|
|
@@ -66793,8 +66839,8 @@ var FetchproxyServer = class {
|
|
|
66793
66839
|
op: "read_dom_list",
|
|
66794
66840
|
init: { origin, name: opts.name }
|
|
66795
66841
|
};
|
|
66796
|
-
const pending = this.guardPending(new Promise((
|
|
66797
|
-
this.pendingDomList.set(id, { resolve:
|
|
66842
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66843
|
+
this.pendingDomList.set(id, { resolve: resolve5, reject });
|
|
66798
66844
|
}));
|
|
66799
66845
|
await this.sendInnerFrame(inner);
|
|
66800
66846
|
return this._withVerbTimeout(pending, this.pendingDomList, id, origin);
|
|
@@ -66839,8 +66885,8 @@ var FetchproxyServer = class {
|
|
|
66839
66885
|
...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
|
|
66840
66886
|
}
|
|
66841
66887
|
};
|
|
66842
|
-
const pending = this.guardPending(new Promise((
|
|
66843
|
-
this.pendingGraphql.set(id, { resolve:
|
|
66888
|
+
const pending = this.guardPending(new Promise((resolve5, reject) => {
|
|
66889
|
+
this.pendingGraphql.set(id, { resolve: resolve5, reject });
|
|
66844
66890
|
}));
|
|
66845
66891
|
const retrySafe = opts.retryOnTimeout === true;
|
|
66846
66892
|
await this.sendInnerFrame(inner, { resendable: retrySafe });
|
|
@@ -67535,7 +67581,7 @@ function loadAccount(env = process.env) {
|
|
|
67535
67581
|
// package.json
|
|
67536
67582
|
var package_default = {
|
|
67537
67583
|
name: "canvas-parent-mcp",
|
|
67538
|
-
version: "2.1.
|
|
67584
|
+
version: "2.1.3",
|
|
67539
67585
|
mcpName: "io.github.chrischall/canvas-parent-mcp",
|
|
67540
67586
|
description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
|
|
67541
67587
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -67844,9 +67890,9 @@ function registerCredentialHealthcheckTool(args) {
|
|
|
67844
67890
|
import { writeFile as writeFile3, stat } from "fs/promises";
|
|
67845
67891
|
|
|
67846
67892
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
67847
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
67848
|
-
import { dirname, join as
|
|
67849
|
-
import { homedir as
|
|
67893
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
|
|
67894
|
+
import { dirname as dirname2, join as join4, resolve as resolve3 } from "node:path";
|
|
67895
|
+
import { homedir as homedir3 } from "node:os";
|
|
67850
67896
|
import { randomBytes as randomBytes3, createHmac } from "node:crypto";
|
|
67851
67897
|
function bindingDigest(secret, salt) {
|
|
67852
67898
|
return createHmac("sha256", salt).update(secret).digest("hex").slice(0, 32);
|
|
@@ -67868,7 +67914,7 @@ function createFileStatePersistence(opts) {
|
|
|
67868
67914
|
const secret = opts.boundTo;
|
|
67869
67915
|
return {
|
|
67870
67916
|
load() {
|
|
67871
|
-
if (!
|
|
67917
|
+
if (!existsSync2(filePath))
|
|
67872
67918
|
return null;
|
|
67873
67919
|
try {
|
|
67874
67920
|
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
@@ -67898,9 +67944,9 @@ function createFileStatePersistence(opts) {
|
|
|
67898
67944
|
}
|
|
67899
67945
|
},
|
|
67900
67946
|
save(state) {
|
|
67901
|
-
const dir =
|
|
67947
|
+
const dir = dirname2(filePath);
|
|
67902
67948
|
const tmp = `${filePath}.tmp-${randomBytes3(6).toString("hex")}`;
|
|
67903
|
-
const dirExisted =
|
|
67949
|
+
const dirExisted = existsSync2(dir);
|
|
67904
67950
|
try {
|
|
67905
67951
|
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
67906
67952
|
if (!dirExisted)
|
|
@@ -67917,7 +67963,7 @@ function createFileStatePersistence(opts) {
|
|
|
67917
67963
|
chmodSync(filePath, 384);
|
|
67918
67964
|
} catch (err) {
|
|
67919
67965
|
try {
|
|
67920
|
-
if (
|
|
67966
|
+
if (existsSync2(tmp))
|
|
67921
67967
|
unlinkSync(tmp);
|
|
67922
67968
|
} catch {
|
|
67923
67969
|
}
|
|
@@ -67926,7 +67972,7 @@ function createFileStatePersistence(opts) {
|
|
|
67926
67972
|
},
|
|
67927
67973
|
clear() {
|
|
67928
67974
|
try {
|
|
67929
|
-
if (
|
|
67975
|
+
if (existsSync2(filePath))
|
|
67930
67976
|
unlinkSync(filePath);
|
|
67931
67977
|
} catch {
|
|
67932
67978
|
}
|
|
@@ -67934,11 +67980,11 @@ function createFileStatePersistence(opts) {
|
|
|
67934
67980
|
};
|
|
67935
67981
|
}
|
|
67936
67982
|
function homeOf(env) {
|
|
67937
|
-
return readEnvVar("HOME", { env }) ??
|
|
67983
|
+
return readEnvVar("HOME", { env }) ?? homedir3();
|
|
67938
67984
|
}
|
|
67939
67985
|
function resolveStateDir(opts = {}) {
|
|
67940
67986
|
const env = opts.env;
|
|
67941
|
-
return
|
|
67987
|
+
return join4(readEnvVar("MCP_DATA_DIR", { env }) ?? homeOf(env), opts.subdir ?? "");
|
|
67942
67988
|
}
|
|
67943
67989
|
function resolveStateFile(opts) {
|
|
67944
67990
|
const override = opts.envVar !== void 0 ? readEnvVar(opts.envVar, { env: opts.env }) : void 0;
|
|
@@ -67946,13 +67992,13 @@ function resolveStateFile(opts) {
|
|
|
67946
67992
|
if (override === "~")
|
|
67947
67993
|
return homeOf(opts.env);
|
|
67948
67994
|
if (override.startsWith("~/"))
|
|
67949
|
-
return
|
|
67950
|
-
return
|
|
67995
|
+
return join4(homeOf(opts.env), override.slice(2));
|
|
67996
|
+
return resolve3(override);
|
|
67951
67997
|
}
|
|
67952
67998
|
const dirOpts = { env: opts.env };
|
|
67953
67999
|
if (opts.subdir !== void 0)
|
|
67954
68000
|
dirOpts.subdir = opts.subdir;
|
|
67955
|
-
return
|
|
68001
|
+
return join4(resolveStateDir(dirOpts), opts.fileName);
|
|
67956
68002
|
}
|
|
67957
68003
|
var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
|
|
67958
68004
|
function isPersistedCookieSession(raw) {
|
|
@@ -68242,7 +68288,8 @@ function reportCacheWriteFailure(err) {
|
|
|
68242
68288
|
}
|
|
68243
68289
|
|
|
68244
68290
|
// src/client.ts
|
|
68245
|
-
import { dirname as
|
|
68291
|
+
import { dirname as dirname3, isAbsolute as isAbsolute5, join as join5, resolve as resolve4 } from "path";
|
|
68292
|
+
import { homedir as homedir4 } from "os";
|
|
68246
68293
|
|
|
68247
68294
|
// src/session-login.ts
|
|
68248
68295
|
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36";
|
|
@@ -68340,6 +68387,8 @@ var CanvasClient = class {
|
|
|
68340
68387
|
* what keeps the path alive once the browser cookie lapses.
|
|
68341
68388
|
*/
|
|
68342
68389
|
refreshSession;
|
|
68390
|
+
/** Explicit download root; when unset, resolved per call (see {@link downloadRoot}). */
|
|
68391
|
+
outputDir;
|
|
68343
68392
|
auth;
|
|
68344
68393
|
/** Lazily-built shared refresh_token exchanger (oauth mode only). */
|
|
68345
68394
|
oauthRefresh = null;
|
|
@@ -68355,6 +68404,7 @@ var CanvasClient = class {
|
|
|
68355
68404
|
*/
|
|
68356
68405
|
constructor(account, opts = {}) {
|
|
68357
68406
|
this.account = account;
|
|
68407
|
+
this.outputDir = opts.outputDir;
|
|
68358
68408
|
this.sessionLoginFn = opts.sessionLogin ?? sessionLogin;
|
|
68359
68409
|
this.refreshSession = opts.refreshSession ?? null;
|
|
68360
68410
|
this.auth = new CookieSessionManager({
|
|
@@ -68396,33 +68446,84 @@ var CanvasClient = class {
|
|
|
68396
68446
|
}
|
|
68397
68447
|
return out;
|
|
68398
68448
|
}
|
|
68449
|
+
/**
|
|
68450
|
+
* Download a Canvas file to disk.
|
|
68451
|
+
*
|
|
68452
|
+
* Both inputs arrive from tool arguments, which a prompt injection in
|
|
68453
|
+
* classmate/teacher-authored Canvas content can steer, so both are pinned
|
|
68454
|
+
* BEFORE any credential is attached:
|
|
68455
|
+
* - `path` must resolve to the configured Canvas origin (https, same host —
|
|
68456
|
+
* the `https://canvas@evil/` userinfo trick parses to a foreign host and is
|
|
68457
|
+
* refused) and name a `/files/<id>` resource. Only the initial request is
|
|
68458
|
+
* pinned: Canvas legitimately redirects to its file store, and fetch
|
|
68459
|
+
* strips Authorization/Cookie on a cross-origin redirect.
|
|
68460
|
+
* - `destinationPath` must lie inside the download root (see
|
|
68461
|
+
* {@link downloadRoot}), checked through symlinks; a relative path is
|
|
68462
|
+
* resolved against that root.
|
|
68463
|
+
*/
|
|
68399
68464
|
async download(path, destinationPath, opts = {}) {
|
|
68465
|
+
const url4 = this.pinDownloadUrl(path);
|
|
68466
|
+
const dest = this.confineDestination(destinationPath);
|
|
68400
68467
|
let destStat = null;
|
|
68401
68468
|
try {
|
|
68402
|
-
destStat = await stat(
|
|
68469
|
+
destStat = await stat(dest);
|
|
68403
68470
|
} catch {
|
|
68404
68471
|
}
|
|
68405
68472
|
if (destStat?.isDirectory()) throw new InvalidPathError(destinationPath);
|
|
68406
68473
|
if (destStat && !opts.overwrite) throw new FileExistsError(destinationPath);
|
|
68407
|
-
const parent =
|
|
68474
|
+
const parent = dirname3(dest);
|
|
68408
68475
|
try {
|
|
68409
68476
|
await stat(parent);
|
|
68410
68477
|
} catch {
|
|
68411
68478
|
throw new ParentDirectoryMissingError(parent);
|
|
68412
68479
|
}
|
|
68413
|
-
const url4 = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
|
|
68414
68480
|
const res = await this.authedFetch(url4, {});
|
|
68415
68481
|
if (res.status === 401) throw new TokenExpiredError(this.account.mode);
|
|
68416
68482
|
if (res.status === 404) throw new Error(`Canvas download 404 for ${path}`);
|
|
68417
68483
|
if (!res.ok) throw new Error(`Canvas download ${res.status} for ${path}`);
|
|
68418
68484
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
68419
|
-
await writeFile3(
|
|
68485
|
+
await writeFile3(dest, buf);
|
|
68420
68486
|
return {
|
|
68421
|
-
path:
|
|
68487
|
+
path: dest,
|
|
68422
68488
|
bytes: buf.byteLength,
|
|
68423
68489
|
contentType: res.headers.get("content-type") ?? "application/octet-stream"
|
|
68424
68490
|
};
|
|
68425
68491
|
}
|
|
68492
|
+
/**
|
|
68493
|
+
* The directory downloads are confined to: the constructor's `outputDir`,
|
|
68494
|
+
* else `CANVAS_OUTPUT_DIR`, else `~/Downloads`. Read per call so an env
|
|
68495
|
+
* change needs no client rebuild.
|
|
68496
|
+
*/
|
|
68497
|
+
downloadRoot() {
|
|
68498
|
+
return expandPath(this.outputDir ?? readEnvVar("CANVAS_OUTPUT_DIR") ?? join5(homedir4(), "Downloads"));
|
|
68499
|
+
}
|
|
68500
|
+
confineDestination(destinationPath) {
|
|
68501
|
+
const root = this.downloadRoot();
|
|
68502
|
+
const candidate = destinationPath.startsWith("~") || isAbsolute5(destinationPath) ? expandPath(destinationPath) : resolve4(root, destinationPath);
|
|
68503
|
+
try {
|
|
68504
|
+
assertPathWithinRoots(candidate, [root]);
|
|
68505
|
+
} catch {
|
|
68506
|
+
throw new DownloadDestinationRejectedError(destinationPath, root);
|
|
68507
|
+
}
|
|
68508
|
+
return candidate;
|
|
68509
|
+
}
|
|
68510
|
+
pinDownloadUrl(path) {
|
|
68511
|
+
const raw = /^[a-z][a-z0-9+.-]*:/i.test(path) ? path : `${this.account.baseUrl}${path}`;
|
|
68512
|
+
let parsed;
|
|
68513
|
+
try {
|
|
68514
|
+
parsed = new URL(raw);
|
|
68515
|
+
} catch {
|
|
68516
|
+
throw new DownloadUrlRejectedError(path, "not a valid URL");
|
|
68517
|
+
}
|
|
68518
|
+
const origin = new URL(this.account.baseUrl).origin;
|
|
68519
|
+
if (parsed.protocol !== "https:" || parsed.origin !== origin || parsed.username || parsed.password) {
|
|
68520
|
+
throw new DownloadUrlRejectedError(path, `must be an https URL on ${origin}`);
|
|
68521
|
+
}
|
|
68522
|
+
if (!/\/files\/[^/]+/.test(parsed.pathname)) {
|
|
68523
|
+
throw new DownloadUrlRejectedError(path, "must name a Canvas file (a /files/<id> path)");
|
|
68524
|
+
}
|
|
68525
|
+
return parsed.href;
|
|
68526
|
+
}
|
|
68426
68527
|
async doRawRequest(path, opts) {
|
|
68427
68528
|
const url4 = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
|
|
68428
68529
|
const accept = opts.responseType === "text" ? "text/html, text/plain, */*" : "application/json+canvas-string-ids, application/json";
|
|
@@ -68535,8 +68636,8 @@ function parseJsonBody2(text) {
|
|
|
68535
68636
|
}
|
|
68536
68637
|
function injectPerPage(pathOrUrl, perPage) {
|
|
68537
68638
|
if (/[?&]per_page=/.test(pathOrUrl)) return pathOrUrl;
|
|
68538
|
-
const
|
|
68539
|
-
return `${pathOrUrl}${
|
|
68639
|
+
const sep2 = pathOrUrl.includes("?") ? "&" : "?";
|
|
68640
|
+
return `${pathOrUrl}${sep2}per_page=${perPage}`;
|
|
68540
68641
|
}
|
|
68541
68642
|
var TokenExpiredError = class extends Error {
|
|
68542
68643
|
constructor(mode, detail) {
|
|
@@ -68557,6 +68658,26 @@ var CanvasUnreachableError = class extends Error {
|
|
|
68557
68658
|
}
|
|
68558
68659
|
status;
|
|
68559
68660
|
};
|
|
68661
|
+
var DownloadUrlRejectedError = class extends Error {
|
|
68662
|
+
constructor(url4, reason) {
|
|
68663
|
+
super(`DownloadUrlRejected: ${reason}; use the \`url\` from canvas_list_course_files. Got: ${url4}`);
|
|
68664
|
+
this.url = url4;
|
|
68665
|
+
this.name = "DownloadUrlRejectedError";
|
|
68666
|
+
}
|
|
68667
|
+
url;
|
|
68668
|
+
};
|
|
68669
|
+
var DownloadDestinationRejectedError = class extends Error {
|
|
68670
|
+
constructor(path, root) {
|
|
68671
|
+
super(
|
|
68672
|
+
`DownloadDestinationRejected: destinationPath must be inside ${root} (set CANVAS_OUTPUT_DIR to change it). Got: ${path}`
|
|
68673
|
+
);
|
|
68674
|
+
this.path = path;
|
|
68675
|
+
this.root = root;
|
|
68676
|
+
this.name = "DownloadDestinationRejectedError";
|
|
68677
|
+
}
|
|
68678
|
+
path;
|
|
68679
|
+
root;
|
|
68680
|
+
};
|
|
68560
68681
|
var InvalidPathError = class extends Error {
|
|
68561
68682
|
constructor(path) {
|
|
68562
68683
|
super(`InvalidPath: destinationPath must be a filename, not a directory: ${path}`);
|
|
@@ -68963,8 +69084,8 @@ var listArgs4 = external_exports.object({
|
|
|
68963
69084
|
contentTypes: external_exports.array(external_exports.string()).optional()
|
|
68964
69085
|
});
|
|
68965
69086
|
var downloadArgs = external_exports.object({
|
|
68966
|
-
url: external_exports.string().describe("The url field returned by canvas_list_course_files (
|
|
68967
|
-
destinationPath: external_exports.string().describe("
|
|
69087
|
+
url: external_exports.string().describe("The url field returned by canvas_list_course_files (an https /files/ URL on the configured Canvas host; anything else is refused)."),
|
|
69088
|
+
destinationPath: external_exports.string().describe("Where to write the file. Must be inside the download directory (CANVAS_OUTPUT_DIR, default ~/Downloads); a relative path is resolved against it."),
|
|
68968
69089
|
overwrite: external_exports.boolean().optional()
|
|
68969
69090
|
});
|
|
68970
69091
|
function registerFileTools(server, client) {
|
|
@@ -68982,8 +69103,8 @@ function registerFileTools(server, client) {
|
|
|
68982
69103
|
return textContent(data);
|
|
68983
69104
|
});
|
|
68984
69105
|
server.registerTool("canvas_download_file", {
|
|
68985
|
-
description: "Download a Canvas file to disk. `url` is the absolute URL from canvas_list_course_files; `destinationPath` is required.",
|
|
68986
|
-
annotations: { destructiveHint: true },
|
|
69106
|
+
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).",
|
|
69107
|
+
annotations: { destructiveHint: true, openWorldHint: true },
|
|
68987
69108
|
inputSchema: downloadArgs
|
|
68988
69109
|
}, async (rawArgs) => {
|
|
68989
69110
|
const args = downloadArgs.parse(rawArgs);
|
|
@@ -68995,8 +69116,8 @@ function registerFileTools(server, client) {
|
|
|
68995
69116
|
}
|
|
68996
69117
|
|
|
68997
69118
|
// src/index.ts
|
|
68998
|
-
var __dirname =
|
|
68999
|
-
await loadDotenvSafely({ path:
|
|
69119
|
+
var __dirname = dirname4(fileURLToPath(import.meta.url));
|
|
69120
|
+
await loadDotenvSafely({ path: join6(__dirname, "..", ".env"), override: false });
|
|
69000
69121
|
var resolved = null;
|
|
69001
69122
|
var configError = null;
|
|
69002
69123
|
try {
|
|
@@ -69006,7 +69127,7 @@ try {
|
|
|
69006
69127
|
}
|
|
69007
69128
|
await runMcp({
|
|
69008
69129
|
name: "canvas",
|
|
69009
|
-
version: "2.1.
|
|
69130
|
+
version: "2.1.3",
|
|
69010
69131
|
// x-release-please-version
|
|
69011
69132
|
tools: [
|
|
69012
69133
|
// Outside the `if (resolved)` on purpose: an unconfigured server must
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { writeFile, stat } from 'fs/promises';
|
|
2
2
|
import { createSessionCache, reportCacheWriteFailure } from './session-cache.js';
|
|
3
|
-
import { dirname } from 'path';
|
|
4
|
-
import {
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { assertPathWithinRoots, createOAuth2Refresher, expandPath, parseLinkHeader, readEnvVar, } from '@chrischall/mcp-utils';
|
|
5
6
|
import { CookieSessionManager } from '@chrischall/mcp-utils/session';
|
|
6
7
|
import { sessionLogin as defaultSessionLogin } from './session-login.js';
|
|
7
8
|
// Re-export the fleet-shared RFC 5988 Link parser so existing importers
|
|
@@ -19,6 +20,8 @@ export class CanvasClient {
|
|
|
19
20
|
* what keeps the path alive once the browser cookie lapses.
|
|
20
21
|
*/
|
|
21
22
|
refreshSession;
|
|
23
|
+
/** Explicit download root; when unset, resolved per call (see {@link downloadRoot}). */
|
|
24
|
+
outputDir;
|
|
22
25
|
auth;
|
|
23
26
|
/** Lazily-built shared refresh_token exchanger (oauth mode only). */
|
|
24
27
|
oauthRefresh = null;
|
|
@@ -34,6 +37,7 @@ export class CanvasClient {
|
|
|
34
37
|
*/
|
|
35
38
|
constructor(account, opts = {}) {
|
|
36
39
|
this.account = account;
|
|
40
|
+
this.outputDir = opts.outputDir;
|
|
37
41
|
this.sessionLoginFn = opts.sessionLogin ?? defaultSessionLogin;
|
|
38
42
|
this.refreshSession = opts.refreshSession ?? null;
|
|
39
43
|
this.auth = new CookieSessionManager({
|
|
@@ -78,24 +82,40 @@ export class CanvasClient {
|
|
|
78
82
|
}
|
|
79
83
|
return out;
|
|
80
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Download a Canvas file to disk.
|
|
87
|
+
*
|
|
88
|
+
* Both inputs arrive from tool arguments, which a prompt injection in
|
|
89
|
+
* classmate/teacher-authored Canvas content can steer, so both are pinned
|
|
90
|
+
* BEFORE any credential is attached:
|
|
91
|
+
* - `path` must resolve to the configured Canvas origin (https, same host —
|
|
92
|
+
* the `https://canvas@evil/` userinfo trick parses to a foreign host and is
|
|
93
|
+
* refused) and name a `/files/<id>` resource. Only the initial request is
|
|
94
|
+
* pinned: Canvas legitimately redirects to its file store, and fetch
|
|
95
|
+
* strips Authorization/Cookie on a cross-origin redirect.
|
|
96
|
+
* - `destinationPath` must lie inside the download root (see
|
|
97
|
+
* {@link downloadRoot}), checked through symlinks; a relative path is
|
|
98
|
+
* resolved against that root.
|
|
99
|
+
*/
|
|
81
100
|
async download(path, destinationPath, opts = {}) {
|
|
101
|
+
const url = this.pinDownloadUrl(path);
|
|
102
|
+
const dest = this.confineDestination(destinationPath);
|
|
82
103
|
let destStat = null;
|
|
83
104
|
try {
|
|
84
|
-
destStat = await stat(
|
|
105
|
+
destStat = await stat(dest);
|
|
85
106
|
}
|
|
86
107
|
catch { /* not present, ok */ }
|
|
87
108
|
if (destStat?.isDirectory())
|
|
88
109
|
throw new InvalidPathError(destinationPath);
|
|
89
110
|
if (destStat && !opts.overwrite)
|
|
90
111
|
throw new FileExistsError(destinationPath);
|
|
91
|
-
const parent = dirname(
|
|
112
|
+
const parent = dirname(dest);
|
|
92
113
|
try {
|
|
93
114
|
await stat(parent);
|
|
94
115
|
}
|
|
95
116
|
catch {
|
|
96
117
|
throw new ParentDirectoryMissingError(parent);
|
|
97
118
|
}
|
|
98
|
-
const url = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
|
|
99
119
|
const res = await this.authedFetch(url, {});
|
|
100
120
|
if (res.status === 401)
|
|
101
121
|
throw new TokenExpiredError(this.account.mode);
|
|
@@ -104,13 +124,54 @@ export class CanvasClient {
|
|
|
104
124
|
if (!res.ok)
|
|
105
125
|
throw new Error(`Canvas download ${res.status} for ${path}`);
|
|
106
126
|
const buf = new Uint8Array(await res.arrayBuffer());
|
|
107
|
-
await writeFile(
|
|
127
|
+
await writeFile(dest, buf);
|
|
108
128
|
return {
|
|
109
|
-
path:
|
|
129
|
+
path: dest,
|
|
110
130
|
bytes: buf.byteLength,
|
|
111
131
|
contentType: res.headers.get('content-type') ?? 'application/octet-stream',
|
|
112
132
|
};
|
|
113
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* The directory downloads are confined to: the constructor's `outputDir`,
|
|
136
|
+
* else `CANVAS_OUTPUT_DIR`, else `~/Downloads`. Read per call so an env
|
|
137
|
+
* change needs no client rebuild.
|
|
138
|
+
*/
|
|
139
|
+
downloadRoot() {
|
|
140
|
+
return expandPath(this.outputDir ?? readEnvVar('CANVAS_OUTPUT_DIR') ?? join(homedir(), 'Downloads'));
|
|
141
|
+
}
|
|
142
|
+
confineDestination(destinationPath) {
|
|
143
|
+
const root = this.downloadRoot();
|
|
144
|
+
// `~` expands to home; any other relative path is anchored at the root
|
|
145
|
+
// (not the process cwd, which for a desktop-launched server is often `/`).
|
|
146
|
+
const candidate = destinationPath.startsWith('~') || isAbsolute(destinationPath)
|
|
147
|
+
? expandPath(destinationPath)
|
|
148
|
+
: resolve(root, destinationPath);
|
|
149
|
+
try {
|
|
150
|
+
assertPathWithinRoots(candidate, [root]);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new DownloadDestinationRejectedError(destinationPath, root);
|
|
154
|
+
}
|
|
155
|
+
return candidate;
|
|
156
|
+
}
|
|
157
|
+
pinDownloadUrl(path) {
|
|
158
|
+
const raw = /^[a-z][a-z0-9+.-]*:/i.test(path) ? path : `${this.account.baseUrl}${path}`;
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = new URL(raw);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new DownloadUrlRejectedError(path, 'not a valid URL');
|
|
165
|
+
}
|
|
166
|
+
const origin = new URL(this.account.baseUrl).origin;
|
|
167
|
+
if (parsed.protocol !== 'https:' || parsed.origin !== origin || parsed.username || parsed.password) {
|
|
168
|
+
throw new DownloadUrlRejectedError(path, `must be an https URL on ${origin}`);
|
|
169
|
+
}
|
|
170
|
+
if (!/\/files\/[^/]+/.test(parsed.pathname)) {
|
|
171
|
+
throw new DownloadUrlRejectedError(path, 'must name a Canvas file (a /files/<id> path)');
|
|
172
|
+
}
|
|
173
|
+
return parsed.href;
|
|
174
|
+
}
|
|
114
175
|
async doRawRequest(path, opts) {
|
|
115
176
|
const url = /^https?:\/\//i.test(path) ? path : `${this.account.baseUrl}${path}`;
|
|
116
177
|
const accept = opts.responseType === 'text'
|
|
@@ -269,6 +330,25 @@ export class CanvasUnreachableError extends Error {
|
|
|
269
330
|
this.name = 'CanvasUnreachableError';
|
|
270
331
|
}
|
|
271
332
|
}
|
|
333
|
+
export class DownloadUrlRejectedError extends Error {
|
|
334
|
+
url;
|
|
335
|
+
constructor(url, reason) {
|
|
336
|
+
super(`DownloadUrlRejected: ${reason}; use the \`url\` from canvas_list_course_files. Got: ${url}`);
|
|
337
|
+
this.url = url;
|
|
338
|
+
this.name = 'DownloadUrlRejectedError';
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
export class DownloadDestinationRejectedError extends Error {
|
|
342
|
+
path;
|
|
343
|
+
root;
|
|
344
|
+
constructor(path, root) {
|
|
345
|
+
super(`DownloadDestinationRejected: destinationPath must be inside ${root} ` +
|
|
346
|
+
`(set CANVAS_OUTPUT_DIR to change it). Got: ${path}`);
|
|
347
|
+
this.path = path;
|
|
348
|
+
this.root = root;
|
|
349
|
+
this.name = 'DownloadDestinationRejectedError';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
272
352
|
export class InvalidPathError extends Error {
|
|
273
353
|
path;
|
|
274
354
|
constructor(path) {
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ catch (e) {
|
|
|
47
47
|
// host's install-time `tools/list` succeeds.
|
|
48
48
|
await runMcp({
|
|
49
49
|
name: 'canvas',
|
|
50
|
-
version: '2.1.
|
|
50
|
+
version: '2.1.3', // x-release-please-version
|
|
51
51
|
tools: [
|
|
52
52
|
// Outside the `if (resolved)` on purpose: an unconfigured server must
|
|
53
53
|
// still expose something that can say WHY (see the tool's docblock).
|
package/dist/tools/files.js
CHANGED
|
@@ -6,8 +6,8 @@ const listArgs = z.object({
|
|
|
6
6
|
contentTypes: z.array(z.string()).optional(),
|
|
7
7
|
});
|
|
8
8
|
const downloadArgs = z.object({
|
|
9
|
-
url: z.string().describe('The url field returned by canvas_list_course_files (
|
|
10
|
-
destinationPath: z.string().describe('
|
|
9
|
+
url: z.string().describe('The url field returned by canvas_list_course_files (an https /files/ URL on the configured Canvas host; anything else is refused).'),
|
|
10
|
+
destinationPath: z.string().describe('Where to write the file. Must be inside the download directory (CANVAS_OUTPUT_DIR, default ~/Downloads); a relative path is resolved against it.'),
|
|
11
11
|
overwrite: z.boolean().optional(),
|
|
12
12
|
});
|
|
13
13
|
export function registerFileTools(server, client) {
|
|
@@ -25,8 +25,8 @@ export function registerFileTools(server, client) {
|
|
|
25
25
|
return textContent(data);
|
|
26
26
|
});
|
|
27
27
|
server.registerTool('canvas_download_file', {
|
|
28
|
-
description: "Download a Canvas file to disk. `url` is the absolute URL from canvas_list_course_files; `destinationPath` is required.",
|
|
29
|
-
annotations: { destructiveHint: true },
|
|
28
|
+
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
|
+
annotations: { destructiveHint: true, openWorldHint: true },
|
|
30
30
|
inputSchema: downloadArgs,
|
|
31
31
|
}, async (rawArgs) => {
|
|
32
32
|
const args = downloadArgs.parse(rawArgs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-parent-mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
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.
|
|
9
|
+
"version": "2.1.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "canvas-parent-mcp",
|
|
14
|
-
"version": "2.1.
|
|
14
|
+
"version": "2.1.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -85,7 +85,7 @@ Precedence when multiple are set: `CANVAS_TOKEN` > username/password > OAuth > f
|
|
|
85
85
|
|
|
86
86
|
### Files
|
|
87
87
|
- `canvas_list_course_files(courseId)` — file metadata
|
|
88
|
-
- `canvas_download_file(url, destinationPath)` — download a file to disk
|
|
88
|
+
- `canvas_download_file(url, destinationPath)` — download a file to disk. `url` must be a `/files/` URL on the configured Canvas host; `destinationPath` must be inside `CANVAS_OUTPUT_DIR` (default `~/Downloads`), and a relative path is resolved against it
|
|
89
89
|
|
|
90
90
|
## Response shape (`view`)
|
|
91
91
|
|
|
@@ -217,7 +217,9 @@ curl -sL -H "Authorization: Bearer $CANVAS_TOKEN" "$FILE_URL" -o /path/to/destin
|
|
|
217
217
|
```
|
|
218
218
|
|
|
219
219
|
`-L` follows Canvas's redirect to the actual file storage backend. The MCP's
|
|
220
|
-
`canvas_download_file` tool additionally
|
|
220
|
+
`canvas_download_file` tool additionally only fetches `/files/` URLs on the
|
|
221
|
+
`CANVAS_BASE_URL` host, only writes inside `CANVAS_OUTPUT_DIR` (default
|
|
222
|
+
`~/Downloads`), refuses to overwrite an existing
|
|
221
223
|
destination unless told to and validates the parent directory exists —
|
|
222
224
|
replicate that yourself in a script if it matters (`[ -f dest ] && exit 1`,
|
|
223
225
|
`[ -d "$(dirname dest)" ] || exit 1`).
|