canvas-lms-mcp 2.7.0 → 2.8.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/bin/canvas-lms-mcp.js +2 -1
- package/dist/auth/credential-gate.d.ts +117 -0
- package/dist/auth/credential-gate.d.ts.map +1 -0
- package/dist/canvas/client.d.ts.map +1 -1
- package/dist/canvas/index.cjs +4 -3
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.js +3 -2
- package/dist/{chunk-YYISGQML.js → chunk-4YHJF3CC.js} +14 -4
- package/dist/chunk-4YHJF3CC.js.map +1 -0
- package/dist/{chunk-3ED6EUGQ.js → chunk-5EZSBL5J.js} +9 -2
- package/dist/{chunk-3ED6EUGQ.js.map → chunk-5EZSBL5J.js.map} +1 -1
- package/dist/chunk-AMZAZNZZ.cjs +34 -0
- package/dist/chunk-AMZAZNZZ.cjs.map +1 -0
- package/dist/{chunk-RPY5WO74.js → chunk-EXBMBM75.js} +17 -7
- package/dist/chunk-EXBMBM75.js.map +1 -0
- package/dist/{chunk-X3F3MYF4.cjs → chunk-EZIJWWVQ.cjs} +10 -3
- package/dist/chunk-EZIJWWVQ.cjs.map +1 -0
- package/dist/{chunk-CDOKT4BS.js → chunk-FRCOQ7JD.js} +2 -2
- package/dist/{chunk-D7UCPSSH.cjs → chunk-GCB3RDOE.cjs} +359 -349
- package/dist/chunk-GCB3RDOE.cjs.map +1 -0
- package/dist/{chunk-ZXJSKVKS.cjs → chunk-GIQ4KDRJ.cjs} +5 -5
- package/dist/{chunk-ZXJSKVKS.cjs.map → chunk-GIQ4KDRJ.cjs.map} +1 -1
- package/dist/{chunk-55HSTOYO.cjs → chunk-PQWAKAIH.cjs} +17 -7
- package/dist/chunk-PQWAKAIH.cjs.map +1 -0
- package/dist/chunk-XLORAICV.js +34 -0
- package/dist/chunk-XLORAICV.js.map +1 -0
- package/dist/cli.cjs +3 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/http.cjs +9 -8
- package/dist/http.cjs.map +1 -1
- package/dist/http.js +5 -4
- package/dist/http.js.map +1 -1
- package/dist/init.cjs +4 -3
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +2 -1
- package/dist/init.js.map +1 -1
- package/dist/server.cjs +5 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +4 -3
- package/dist/stdio.cjs +16 -7
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.js +14 -5
- package/dist/stdio.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-55HSTOYO.cjs.map +0 -1
- package/dist/chunk-D7UCPSSH.cjs.map +0 -1
- package/dist/chunk-RPY5WO74.js.map +0 -1
- package/dist/chunk-X3F3MYF4.cjs.map +0 -1
- package/dist/chunk-YYISGQML.js.map +0 -1
- /package/dist/{chunk-CDOKT4BS.js.map → chunk-FRCOQ7JD.js.map} +0 -0
package/bin/canvas-lms-mcp.js
CHANGED
|
@@ -15,7 +15,8 @@ Commands:
|
|
|
15
15
|
doctor Check setup without starting the server
|
|
16
16
|
auth status Same as doctor
|
|
17
17
|
|
|
18
|
-
Credentials (
|
|
18
|
+
Credentials (serve needs them at startup; stdio starts without them so MCP
|
|
19
|
+
directories can read its tool list, but then every tool call fails):
|
|
19
20
|
--token <t> Canvas API token (or CANVAS_API_TOKEN)
|
|
20
21
|
--base-url <u> Canvas base URL, origin only (or CANVAS_BASE_URL)
|
|
21
22
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
/** Shared by the fatal startup errors and the gated tool-call error. */
|
|
3
|
+
export declare const DOCTOR_HINT = "Run `canvas-lms-mcp doctor` to check your setup.";
|
|
4
|
+
/**
|
|
5
|
+
* The error text every tool call returns while the gate is installed.
|
|
6
|
+
*
|
|
7
|
+
* Stable on purpose: a directory, a client and an acceptance test all read it.
|
|
8
|
+
* It names the token first (so it matches the fatal startup error a configured
|
|
9
|
+
* deployer sees), both configuration surfaces for each credential, and the
|
|
10
|
+
* doctor hint.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MISSING_CREDENTIALS_TOOL_CALL_ERROR: string;
|
|
13
|
+
/**
|
|
14
|
+
* The single startup notice. One line, stderr only: stdout is the MCP protocol
|
|
15
|
+
* channel on the stdio transport, and one stray byte there breaks the session
|
|
16
|
+
* for every client.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CREDENTIALLESS_STDIO_WARNING: string;
|
|
19
|
+
/**
|
|
20
|
+
* Installed on a server whose tool calls must all be refused before they run.
|
|
21
|
+
*
|
|
22
|
+
* A value rather than a boolean flag so the refusal message travels with the
|
|
23
|
+
* decision: the tool layer never has to know *why* it is gated, and there is no
|
|
24
|
+
* second place where the message could drift. Absent = no gate, which is every
|
|
25
|
+
* configured deployment byte-for-byte.
|
|
26
|
+
*/
|
|
27
|
+
export interface ToolCallGate {
|
|
28
|
+
readonly message: string;
|
|
29
|
+
}
|
|
30
|
+
/** The gate for a server started with no Canvas credentials. */
|
|
31
|
+
export declare function canvasCredentialGate(): ToolCallGate;
|
|
32
|
+
/** The refusal, in `CallToolResult` shape. */
|
|
33
|
+
export type GatedToolCallResult = {
|
|
34
|
+
content: {
|
|
35
|
+
type: 'text';
|
|
36
|
+
text: string;
|
|
37
|
+
}[];
|
|
38
|
+
isError: true;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The one refusal value. Both gate layers return *this*, so the message, the
|
|
42
|
+
* `isError` flag and the absence of `structuredContent` cannot drift apart
|
|
43
|
+
* between a well-formed call and a malformed one.
|
|
44
|
+
*
|
|
45
|
+
* `isError: true` rather than a JSON-RPC error: a tool-level error is what the
|
|
46
|
+
* MCP spec reserves for "the tool ran and failed", it reaches the model as
|
|
47
|
+
* readable text in every client, and it carries no structured payload that
|
|
48
|
+
* could satisfy a tool's success schema.
|
|
49
|
+
*/
|
|
50
|
+
export declare function gatedToolCallResult(gate: ToolCallGate): GatedToolCallResult;
|
|
51
|
+
/**
|
|
52
|
+
* The `tools/call` method name, taken from the SDK's own schema rather than
|
|
53
|
+
* retyped, so a gate and a dispatcher can never name different methods.
|
|
54
|
+
*/
|
|
55
|
+
export declare const CALL_TOOL_METHOD: string;
|
|
56
|
+
/**
|
|
57
|
+
* Make **every** `tools/call` request answer with the gate refusal.
|
|
58
|
+
*
|
|
59
|
+
* This is the wire-level half of the gate. The other half is the early return
|
|
60
|
+
* in `buildHandler` (`src/tools/index.ts`), which covers a tool invoked through
|
|
61
|
+
* a path that never goes over the protocol — an embedder calling
|
|
62
|
+
* `tool.handler` directly, or a server whose protocol object is a test double.
|
|
63
|
+
* Neither layer shadows the other: this one answers requests that never reach a
|
|
64
|
+
* registered handler, that one answers invocations that never reach dispatch.
|
|
65
|
+
*
|
|
66
|
+
* ## Why the registration path cannot be used
|
|
67
|
+
*
|
|
68
|
+
* A `tools/call` request is validated **twice** before any registered handler
|
|
69
|
+
* runs, and both rejections happen inside the SDK:
|
|
70
|
+
*
|
|
71
|
+
* 1. `Protocol.setRequestHandler` wraps the handler in
|
|
72
|
+
* `requestSchema.safeParse(request)` and rethrows the `ZodError`, so with
|
|
73
|
+
* the SDK's `CallToolRequestSchema` a request with no `params`, a
|
|
74
|
+
* non-string `name` or a non-object `arguments` is answered with a raw Zod
|
|
75
|
+
* dump under JSON-RPC `-32603`.
|
|
76
|
+
* 2. `Server.setRequestHandler` **overrides** that method and, for
|
|
77
|
+
* `tools/call` specifically, wraps *any* handler — whatever schema it was
|
|
78
|
+
* registered under — in an unconditional
|
|
79
|
+
* `safeParse(CallToolRequestSchema, request)` that throws
|
|
80
|
+
* `McpError(InvalidParams, 'Invalid tools/call request: …')`.
|
|
81
|
+
*
|
|
82
|
+
* Layer 2 means re-registering under a permissive schema does not work: it was
|
|
83
|
+
* measured, and it only changed `-32603` to `-32602`. There is no option to
|
|
84
|
+
* disable it (`ServerOptions` has no such flag), and the SDK exposes no way to
|
|
85
|
+
* read a registered handler back.
|
|
86
|
+
*
|
|
87
|
+
* ## The seam used instead
|
|
88
|
+
*
|
|
89
|
+
* `Protocol._onrequest` resolves a handler as
|
|
90
|
+
* `this._requestHandlers.get(method) ?? this.fallbackRequestHandler` and calls
|
|
91
|
+
* the fallback with the **raw** request — no schema parse, and outside
|
|
92
|
+
* `Server`'s `tools/call` wrapper, which only decorates handlers passed to
|
|
93
|
+
* `setRequestHandler`. Both `removeRequestHandler` and
|
|
94
|
+
* `fallbackRequestHandler` are public, documented API, so this patches
|
|
95
|
+
* nothing and reaches into no private internals.
|
|
96
|
+
*
|
|
97
|
+
* Removing the entry rather than shadowing it is the stronger arrangement: the
|
|
98
|
+
* dispatch table then holds *no* `tools/call` handler at all, so there is no
|
|
99
|
+
* path from a request to a tool handler to a Canvas request. The cost is that
|
|
100
|
+
* installing a fallback replaces `Protocol`'s built-in "Method not found"
|
|
101
|
+
* branch for every other unhandled method, so this reproduces it exactly.
|
|
102
|
+
*
|
|
103
|
+
* This loosens no validation that protects anything: a gated server registers
|
|
104
|
+
* no usable Canvas credential and can run no tool, so there is no execution
|
|
105
|
+
* for argument validation to guard. A configured server is untouched — the
|
|
106
|
+
* gate object does not exist in its config.
|
|
107
|
+
*
|
|
108
|
+
* ## Ordering
|
|
109
|
+
*
|
|
110
|
+
* Called from `registerAllTools` after the registration loop, because
|
|
111
|
+
* `McpServer.setToolRequestHandlers` runs on the first `registerTool` and is
|
|
112
|
+
* what installs the handler this removes. Its `_toolHandlersInitialized` latch
|
|
113
|
+
* means later `registerTool` calls do not reinstall it, so an embedder may
|
|
114
|
+
* still add tools afterwards without reopening the hole.
|
|
115
|
+
*/
|
|
116
|
+
export declare function installToolCallGateHandler(server: McpServer, gate: ToolCallGate): void;
|
|
117
|
+
//# sourceMappingURL=credential-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-gate.d.ts","sourceRoot":"","sources":["../../src/auth/credential-gate.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAOxE,wEAAwE;AACxE,eAAO,MAAM,WAAW,qDAAqD,CAAA;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,mCAAmC,QAGL,CAAA;AAE3C;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,QAGqB,CAAA;AAE9D;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,gEAAgE;AAChE,wBAAgB,oBAAoB,IAAI,YAAY,CAEnD;AAED,8CAA8C;AAI9C,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,EAAE,IAAI,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,mBAAmB,CAE3E;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAiD,CAAA;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAkBtF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/canvas/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/canvas/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAEnE,OAAO,KAAK,EAAE,kBAAkB,EAAuB,MAAM,SAAS,CAAA;AAEtE,qBAAa,cAAe,SAAQ,KAAK;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;gBAEJ,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAM9D;AAKD,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD;;;;OAIG;IACH,KAAK,CAAC,EAAE,iBAAiB,CAAA;CAC1B;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,kBAAkB,CAAQ;gBAEtB,MAAM,EAAE,kBAAkB;IAMtC,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;;;OAIG;IACG,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,CAAC,CAAC;IA2C5E,QAAQ,CAAC,CAAC,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,iBAAiB,EAC1B,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9B,OAAO,CAAC,CAAC,EAAE,CAAC;IA6CT,gBAAgB,CAAC,CAAC,EACtB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,CAAC,EAAE,CAAC;IAsCf;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;IAkBrB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,aAAa;CAKtB"}
|
package/dist/canvas/index.cjs
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var _chunkGIQ4KDRJcjs = require('../chunk-GIQ4KDRJ.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
var
|
|
15
|
+
var _chunkEZIJWWVQcjs = require('../chunk-EZIJWWVQ.cjs');
|
|
16
|
+
require('../chunk-AMZAZNZZ.cjs');
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
|
|
@@ -25,5 +26,5 @@ var _chunkX3F3MYF4cjs = require('../chunk-X3F3MYF4.cjs');
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
exports.CANVAS_MAX_ID =
|
|
29
|
+
exports.CANVAS_MAX_ID = _chunkGIQ4KDRJcjs.CANVAS_MAX_ID; exports.CanvasApiError = _chunkEZIJWWVQcjs.CanvasApiError; exports.CanvasClient = _chunkGIQ4KDRJcjs.CanvasClient; exports.CanvasHttpClient = _chunkEZIJWWVQcjs.CanvasHttpClient; exports.MAX_SAFE_CANVAS_NUMBER_ID = _chunkGIQ4KDRJcjs.MAX_SAFE_CANVAS_NUMBER_ID; exports.appendCanvasQuery = _chunkEZIJWWVQcjs.appendCanvasQuery; exports.canvasIdInput = _chunkGIQ4KDRJcjs.canvasIdInput; exports.canvasIdList = _chunkGIQ4KDRJcjs.canvasIdList; exports.compareCanvasIds = _chunkGIQ4KDRJcjs.compareCanvasIds; exports.normalizeCanvasIdInput = _chunkGIQ4KDRJcjs.normalizeCanvasIdInput; exports.toCanvasQuery = _chunkEZIJWWVQcjs.toCanvasQuery;
|
|
29
30
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/canvas/index.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,8qBAAC","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/canvas/index.cjs"}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/canvas/index.cjs"],"names":[],"mappings":"AAAA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,8qBAAC","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/canvas/index.cjs"}
|
package/dist/canvas/index.js
CHANGED
|
@@ -6,13 +6,14 @@ import {
|
|
|
6
6
|
canvasIdList,
|
|
7
7
|
compareCanvasIds,
|
|
8
8
|
normalizeCanvasIdInput
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-FRCOQ7JD.js";
|
|
10
10
|
import {
|
|
11
11
|
CanvasApiError,
|
|
12
12
|
CanvasHttpClient,
|
|
13
13
|
appendCanvasQuery,
|
|
14
14
|
toCanvasQuery
|
|
15
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-5EZSBL5J.js";
|
|
16
|
+
import "../chunk-XLORAICV.js";
|
|
16
17
|
export {
|
|
17
18
|
CANVAS_MAX_ID,
|
|
18
19
|
CanvasApiError,
|
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
isEnvTruthy
|
|
16
16
|
} from "./chunk-XVZ3T4Z4.js";
|
|
17
|
+
import {
|
|
18
|
+
DOCTOR_HINT
|
|
19
|
+
} from "./chunk-XLORAICV.js";
|
|
17
20
|
|
|
18
21
|
// src/cli.ts
|
|
19
22
|
var DESTRUCTIVE_FLAG = "--destructive-tools";
|
|
20
|
-
var DOCTOR_HINT = "Run `canvas-lms-mcp doctor` to check your setup.";
|
|
21
23
|
function fatal(message) {
|
|
22
24
|
console.error(`Error: ${message}`);
|
|
23
25
|
process.exit(1);
|
|
@@ -156,9 +158,16 @@ function parseArgs(args) {
|
|
|
156
158
|
host,
|
|
157
159
|
authProfile,
|
|
158
160
|
oauth,
|
|
159
|
-
destructiveTools
|
|
161
|
+
destructiveTools,
|
|
162
|
+
// The Canvas credential arrives per request from the OAuth flow, and
|
|
163
|
+
// `loadOAuthProfileConfig` above has already refused to start without
|
|
164
|
+
// the client credentials it needs.
|
|
165
|
+
credentials: "present"
|
|
160
166
|
};
|
|
161
167
|
}
|
|
168
|
+
if (authProfile === "local_static_token" && !config.token && !config.baseUrl) {
|
|
169
|
+
return { ...config, authProfile, destructiveTools, credentials: "absent" };
|
|
170
|
+
}
|
|
162
171
|
if (!config.token) {
|
|
163
172
|
fatal(`Canvas API token required. Use --token or set CANVAS_API_TOKEN. ${DOCTOR_HINT}`);
|
|
164
173
|
}
|
|
@@ -169,11 +178,12 @@ function parseArgs(args) {
|
|
|
169
178
|
...config,
|
|
170
179
|
...hostRaw !== void 0 && hostRaw.trim() !== "" ? { host: hostRaw.trim() } : {},
|
|
171
180
|
authProfile,
|
|
172
|
-
destructiveTools
|
|
181
|
+
destructiveTools,
|
|
182
|
+
credentials: "present"
|
|
173
183
|
};
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
export {
|
|
177
187
|
parseArgs
|
|
178
188
|
};
|
|
179
|
-
//# sourceMappingURL=chunk-
|
|
189
|
+
//# sourceMappingURL=chunk-4YHJF3CC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { isEnvTruthy } from './env'\nimport { DOCTOR_HINT } from './auth/credential-gate'\nimport { parseRole } from './tools/roles'\nimport { parseDestructiveToolsMode, type DestructiveToolsMode } from './tools/destructive-policy'\nimport type { CanvasRole } from './tools/types'\nimport { profileRequiresStaticToken, resolveAuthProfile, type AuthProfile } from './auth/profile'\nimport {\n OAuthConfigError,\n loadOAuthProfileConfig,\n resolveBindHost,\n type OAuthProfileConfig,\n} from './auth/oauth/config'\n\nexport interface CliConfig {\n /** Canvas personal access token. Empty in the `oauth_brokered` profile. */\n token: string\n baseUrl: string\n mode: 'stdio' | 'http'\n port: number\n /**\n * Bind address for the HTTP transport. `undefined` means every interface,\n * which is what `remote_static_token` has always done (Docker relies on it).\n * `oauth_brokered` always resolves a concrete host, loopback by default.\n */\n host?: string\n allowedOrigin: string\n /**\n * Auth profile (issue #302 §3). Resolved from `--auth-profile` /\n * `CANVAS_AUTH_PROFILE`, defaulting per transport, and checked against it.\n */\n authProfile: AuthProfile\n /** Present exactly when `authProfile === 'oauth_brokered'`. */\n oauth?: OAuthProfileConfig\n /** Canvas role for tool filtering; undefined = register all tools. */\n role?: CanvasRole\n /** Opt-in: register assignment submission tools when true. */\n enableAssignmentSubmission?: boolean\n /**\n * Destructive-tool policy. Always concrete: resolved from\n * `--destructive-tools` / `CANVAS_DESTRUCTIVE_TOOLS` here so that exactly one\n * place in the CLI path decides it. The flag wins outright when present, and\n * an invalid value *of the winning source* stops startup.\n */\n destructiveTools: DestructiveToolsMode\n /**\n * Whether this process holds a usable Canvas credential.\n *\n * `'absent'` is reachable only on the local stdio profile with *neither*\n * token nor base URL set: the credentialless discovery state an MCP\n * directory launches the package in (GH #442). Every other path either\n * resolves a credential or exits, so `'present'` there is a fact, not a\n * default. Callers must gate tool calls when this is `absent` — the\n * server starts and advertises its full surface, but it can authorize\n * nothing.\n */\n credentials: 'present' | 'absent'\n}\n\nconst DESTRUCTIVE_FLAG = '--destructive-tools'\n\n/** Print a startup error and exit. Never returns. */\nfunction fatal(message: string): never {\n console.error(`Error: ${message}`)\n process.exit(1)\n}\n\n/**\n * Parse a destructive-tools mode, converting the parser's thrown error into the\n * same `Error: …` + exit-1 shape the rest of this CLI uses for fatal config\n * problems. Exiting is deliberate: the alternative is starting with the delete\n * tools registered after the deployer asked for them to be blocked.\n */\nfunction parseDestructiveToolsModeOrExit(\n raw: string | undefined,\n source: string,\n): DestructiveToolsMode {\n try {\n return parseDestructiveToolsMode(raw, source)\n } catch (error) {\n return fatal(error instanceof Error ? error.message : String(error))\n }\n}\n\n/**\n * Read the value of a `--flag value` / `--flag=value` pair. Returns `undefined`\n * when `arg` is not this flag; exits when the flag is present without a value.\n */\nfunction takeFlagValue(\n flag: string,\n arg: string | undefined,\n args: string[],\n index: { i: number },\n): string | undefined {\n if (arg === flag) {\n const value = args[++index.i]\n if (value === undefined) fatal(`${flag} requires a value.`)\n return value\n }\n if (arg?.startsWith(`${flag}=`)) return arg.slice(flag.length + 1)\n return undefined\n}\n\nexport function parseArgs(args: string[]): CliConfig {\n const envRole = parseRole(process.env.CANVAS_ROLE)\n if (envRole.invalid) {\n console.warn(\n `Unknown CANVAS_ROLE '${process.env.CANVAS_ROLE}'; ignoring and registering all tools.`,\n )\n }\n\n // `destructiveTools`, `authProfile` and `credentials` are deliberately absent\n // from this object *and* from its type: all three are resolved at the\n // returns below, once the parser knows what the command line supplied.\n // Omitting the keys makes the compiler demand them at every `return`, so a\n // resolution cannot be dropped, and leaves no placeholder here that could\n // survive and fail *open*.\n const config: Omit<CliConfig, 'destructiveTools' | 'authProfile' | 'credentials'> = {\n token: process.env.CANVAS_API_TOKEN ?? '',\n baseUrl: process.env.CANVAS_BASE_URL ?? '',\n mode: 'stdio',\n port: 3001,\n allowedOrigin: process.env.CANVAS_ALLOWED_ORIGIN ?? 'http://localhost:3000',\n role: envRole.role,\n enableAssignmentSubmission: isEnvTruthy(process.env.CANVAS_ENABLE_ASSIGNMENT_SUBMISSION),\n }\n\n /** Set only when `--destructive-tools` appears in argv; `undefined` = no flag. */\n let destructiveToolsFromFlag: DestructiveToolsMode | undefined\n let authProfileFlag: string | undefined\n let hostFlag: string | undefined\n let issuerFlag: string | undefined\n\n const index = { i: 0 }\n for (index.i = 0; index.i < args.length; index.i++) {\n const arg = args[index.i]\n\n // `--destructive-tools=<mode>` (the documented form) and\n // `--destructive-tools <mode>` (the form every other flag in this parser\n // uses) are both accepted, because a flag that is silently ignored fails\n // *open* into `allow` — the one outcome a deployer typing this flag never\n // wants. A missing value is a startup error for the same reason.\n if (arg === DESTRUCTIVE_FLAG || arg?.startsWith(`${DESTRUCTIVE_FLAG}=`)) {\n const raw =\n arg === DESTRUCTIVE_FLAG ? args[++index.i] : arg.slice(DESTRUCTIVE_FLAG.length + 1)\n if (raw === undefined) {\n fatal(`${DESTRUCTIVE_FLAG} requires a value. Use ${DESTRUCTIVE_FLAG}=allow or =block.`)\n }\n // Parsed here, per occurrence, rather than deferred with the environment:\n // a repeated flag is one operator typing twice in a single command line,\n // so a bad value has no legitimate override story and refusing to start is\n // the safe direction. Among *valid* values the last one still wins.\n destructiveToolsFromFlag = parseDestructiveToolsModeOrExit(raw, DESTRUCTIVE_FLAG)\n continue\n }\n\n // The three profile-related flags: same \"silently ignored fails open\"\n // reasoning, so a missing value is fatal rather than defaulted.\n const profileValue = takeFlagValue('--auth-profile', arg, args, index)\n if (profileValue !== undefined) {\n authProfileFlag = profileValue\n continue\n }\n const hostValue = takeFlagValue('--host', arg, args, index)\n if (hostValue !== undefined) {\n hostFlag = hostValue\n continue\n }\n const issuerValue = takeFlagValue('--issuer', arg, args, index)\n if (issuerValue !== undefined) {\n issuerFlag = issuerValue\n continue\n }\n\n switch (arg) {\n case '--token':\n config.token = args[++index.i] ?? ''\n break\n case '--base-url':\n config.baseUrl = args[++index.i] ?? ''\n break\n case 'serve':\n config.mode = 'http'\n break\n case '--port': {\n const parsed = Number(args[++index.i])\n config.port = Number.isNaN(parsed) ? 3001 : parsed\n break\n }\n case '--allowed-origin':\n config.allowedOrigin = args[++index.i] ?? 'http://localhost:3000'\n break\n case '--role': {\n const raw = args[++index.i]\n const parsed = parseRole(raw)\n if (parsed.invalid) {\n console.warn(`Unknown --role value '${raw}'; ignoring and registering all tools.`)\n }\n // A valid --role overrides env; an invalid/`all` value falls back to all.\n config.role = parsed.role\n break\n }\n case '--enable-assignment-submission':\n config.enableAssignmentSubmission = true\n break\n }\n }\n\n // Precedence mirrors `resolveDestructiveToolsMode` (src/tools/destructive-policy.ts):\n // whichever source wins is the *only* source parsed. Reading the environment\n // eagerly instead — as this function used to, while building `config` above —\n // let an invalid ambient `CANVAS_DESTRUCTIVE_TOOLS` abort startup even when the\n // command line carried a valid override, making the flag unusable on precisely\n // the hosts where an operator needs it (BRU-2463).\n const destructiveTools =\n destructiveToolsFromFlag ??\n parseDestructiveToolsModeOrExit(\n process.env.CANVAS_DESTRUCTIVE_TOOLS,\n 'CANVAS_DESTRUCTIVE_TOOLS',\n )\n\n // Same rule for the profile: the flag wins and is the only source parsed;\n // an unknown value on the winning source is fatal, never a fallback.\n let authProfile: AuthProfile\n try {\n authProfile = resolveAuthProfile({\n flag: authProfileFlag,\n env: process.env.CANVAS_AUTH_PROFILE,\n mode: config.mode,\n })\n } catch (error) {\n return fatal(error instanceof Error ? error.message : String(error))\n }\n\n const hostRaw = hostFlag ?? process.env.CANVAS_HTTP_HOST\n\n if (!profileRequiresStaticToken(authProfile)) {\n // oauth_brokered: the Canvas credential comes from the OAuth flow, never\n // from the environment. A leftover PAT is worth a warning, not a refusal.\n if (config.token !== '') {\n console.warn(\n 'CANVAS_API_TOKEN / --token is ignored in the oauth_brokered profile; Canvas credentials come from the OAuth flow.',\n )\n config.token = ''\n }\n let oauth: OAuthProfileConfig\n let host: string\n try {\n oauth = loadOAuthProfileConfig(process.env, {\n issuer: issuerFlag,\n baseUrl: config.baseUrl === '' ? undefined : config.baseUrl,\n })\n host = resolveBindHost(oauth, hostRaw)\n } catch (error) {\n if (error instanceof OAuthConfigError) return fatal(`${error.message}. ${DOCTOR_HINT}`)\n throw error\n }\n return {\n ...config,\n baseUrl: oauth.canvas.baseUrl,\n host,\n authProfile,\n oauth,\n destructiveTools,\n // The Canvas credential arrives per request from the OAuth flow, and\n // `loadOAuthProfileConfig` above has already refused to start without\n // the client credentials it needs.\n credentials: 'present',\n }\n }\n\n // Credentialless stdio discovery (GH #442, BRU-3043). An MCP directory\n // launches the package with no environment at all and reads `tools/list`;\n // exiting here is what made those listings report \"No tools detected\", and\n // the only server that scored was one injecting placeholder credentials.\n //\n // Deliberately narrow. Only the local stdio profile qualifies, because\n // nothing introspects a remote listener over stdio and the HTTP profiles\n // keep their startup requirements exactly. And only when *neither*\n // credential is set: one of the two present is a typo or a half-finished\n // setup, so it keeps the fatal error rather than starting in a mode whose\n // own message would tell the deployer to set what they already set.\n if (authProfile === 'local_static_token' && !config.token && !config.baseUrl) {\n return { ...config, authProfile, destructiveTools, credentials: 'absent' }\n }\n\n if (!config.token) {\n fatal(`Canvas API token required. Use --token or set CANVAS_API_TOKEN. ${DOCTOR_HINT}`)\n }\n if (!config.baseUrl) {\n fatal(`Canvas base URL required. Use --base-url or set CANVAS_BASE_URL. ${DOCTOR_HINT}`)\n }\n\n return {\n ...config,\n ...(hostRaw !== undefined && hostRaw.trim() !== '' ? { host: hostRaw.trim() } : {}),\n authProfile,\n destructiveTools,\n credentials: 'present',\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0DA,IAAM,mBAAmB;AAGzB,SAAS,MAAM,SAAwB;AACrC,UAAQ,MAAM,UAAU,OAAO,EAAE;AACjC,UAAQ,KAAK,CAAC;AAChB;AAQA,SAAS,gCACP,KACA,QACsB;AACtB,MAAI;AACF,WAAO,0BAA0B,KAAK,MAAM;AAAA,EAC9C,SAAS,OAAO;AACd,WAAO,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,EACrE;AACF;AAMA,SAAS,cACP,MACA,KACA,MACA,OACoB;AACpB,MAAI,QAAQ,MAAM;AAChB,UAAM,QAAQ,KAAK,EAAE,MAAM,CAAC;AAC5B,QAAI,UAAU,OAAW,OAAM,GAAG,IAAI,oBAAoB;AAC1D,WAAO;AAAA,EACT;AACA,MAAI,KAAK,WAAW,GAAG,IAAI,GAAG,EAAG,QAAO,IAAI,MAAM,KAAK,SAAS,CAAC;AACjE,SAAO;AACT;AAEO,SAAS,UAAU,MAA2B;AACnD,QAAM,UAAU,UAAU,QAAQ,IAAI,WAAW;AACjD,MAAI,QAAQ,SAAS;AACnB,YAAQ;AAAA,MACN,wBAAwB,QAAQ,IAAI,WAAW;AAAA,IACjD;AAAA,EACF;AAQA,QAAM,SAA8E;AAAA,IAClF,OAAO,QAAQ,IAAI,oBAAoB;AAAA,IACvC,SAAS,QAAQ,IAAI,mBAAmB;AAAA,IACxC,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe,QAAQ,IAAI,yBAAyB;AAAA,IACpD,MAAM,QAAQ;AAAA,IACd,4BAA4B,YAAY,QAAQ,IAAI,mCAAmC;AAAA,EACzF;AAGA,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,QAAM,QAAQ,EAAE,GAAG,EAAE;AACrB,OAAK,MAAM,IAAI,GAAG,MAAM,IAAI,KAAK,QAAQ,MAAM,KAAK;AAClD,UAAM,MAAM,KAAK,MAAM,CAAC;AAOxB,QAAI,QAAQ,oBAAoB,KAAK,WAAW,GAAG,gBAAgB,GAAG,GAAG;AACvE,YAAM,MACJ,QAAQ,mBAAmB,KAAK,EAAE,MAAM,CAAC,IAAI,IAAI,MAAM,iBAAiB,SAAS,CAAC;AACpF,UAAI,QAAQ,QAAW;AACrB,cAAM,GAAG,gBAAgB,0BAA0B,gBAAgB,mBAAmB;AAAA,MACxF;AAKA,iCAA2B,gCAAgC,KAAK,gBAAgB;AAChF;AAAA,IACF;AAIA,UAAM,eAAe,cAAc,kBAAkB,KAAK,MAAM,KAAK;AACrE,QAAI,iBAAiB,QAAW;AAC9B,wBAAkB;AAClB;AAAA,IACF;AACA,UAAM,YAAY,cAAc,UAAU,KAAK,MAAM,KAAK;AAC1D,QAAI,cAAc,QAAW;AAC3B,iBAAW;AACX;AAAA,IACF;AACA,UAAM,cAAc,cAAc,YAAY,KAAK,MAAM,KAAK;AAC9D,QAAI,gBAAgB,QAAW;AAC7B,mBAAa;AACb;AAAA,IACF;AAEA,YAAQ,KAAK;AAAA,MACX,KAAK;AACH,eAAO,QAAQ,KAAK,EAAE,MAAM,CAAC,KAAK;AAClC;AAAA,MACF,KAAK;AACH,eAAO,UAAU,KAAK,EAAE,MAAM,CAAC,KAAK;AACpC;AAAA,MACF,KAAK;AACH,eAAO,OAAO;AACd;AAAA,MACF,KAAK,UAAU;AACb,cAAM,SAAS,OAAO,KAAK,EAAE,MAAM,CAAC,CAAC;AACrC,eAAO,OAAO,OAAO,MAAM,MAAM,IAAI,OAAO;AAC5C;AAAA,MACF;AAAA,MACA,KAAK;AACH,eAAO,gBAAgB,KAAK,EAAE,MAAM,CAAC,KAAK;AAC1C;AAAA,MACF,KAAK,UAAU;AACb,cAAM,MAAM,KAAK,EAAE,MAAM,CAAC;AAC1B,cAAM,SAAS,UAAU,GAAG;AAC5B,YAAI,OAAO,SAAS;AAClB,kBAAQ,KAAK,yBAAyB,GAAG,wCAAwC;AAAA,QACnF;AAEA,eAAO,OAAO,OAAO;AACrB;AAAA,MACF;AAAA,MACA,KAAK;AACH,eAAO,6BAA6B;AACpC;AAAA,IACJ;AAAA,EACF;AAQA,QAAM,mBACJ,4BACA;AAAA,IACE,QAAQ,IAAI;AAAA,IACZ;AAAA,EACF;AAIF,MAAI;AACJ,MAAI;AACF,kBAAc,mBAAmB;AAAA,MAC/B,MAAM;AAAA,MACN,KAAK,QAAQ,IAAI;AAAA,MACjB,MAAM,OAAO;AAAA,IACf,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,EACrE;AAEA,QAAM,UAAU,YAAY,QAAQ,IAAI;AAExC,MAAI,CAAC,2BAA2B,WAAW,GAAG;AAG5C,QAAI,OAAO,UAAU,IAAI;AACvB,cAAQ;AAAA,QACN;AAAA,MACF;AACA,aAAO,QAAQ;AAAA,IACjB;AACA,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,cAAQ,uBAAuB,QAAQ,KAAK;AAAA,QAC1C,QAAQ;AAAA,QACR,SAAS,OAAO,YAAY,KAAK,SAAY,OAAO;AAAA,MACtD,CAAC;AACD,aAAO,gBAAgB,OAAO,OAAO;AAAA,IACvC,SAAS,OAAO;AACd,UAAI,iBAAiB,iBAAkB,QAAO,MAAM,GAAG,MAAM,OAAO,KAAK,WAAW,EAAE;AACtF,YAAM;AAAA,IACR;AACA,WAAO;AAAA,MACL,GAAG;AAAA,MACH,SAAS,MAAM,OAAO;AAAA,MACtB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAAA;AAAA;AAAA,MAIA,aAAa;AAAA,IACf;AAAA,EACF;AAaA,MAAI,gBAAgB,wBAAwB,CAAC,OAAO,SAAS,CAAC,OAAO,SAAS;AAC5E,WAAO,EAAE,GAAG,QAAQ,aAAa,kBAAkB,aAAa,SAAS;AAAA,EAC3E;AAEA,MAAI,CAAC,OAAO,OAAO;AACjB,UAAM,mEAAmE,WAAW,EAAE;AAAA,EACxF;AACA,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,oEAAoE,WAAW,EAAE;AAAA,EACzF;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAI,YAAY,UAAa,QAAQ,KAAK,MAAM,KAAK,EAAE,MAAM,QAAQ,KAAK,EAAE,IAAI,CAAC;AAAA,IACjF;AAAA,IACA;AAAA,IACA,aAAa;AAAA,EACf;AACF;","names":[]}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MISSING_CREDENTIALS_TOOL_CALL_ERROR
|
|
3
|
+
} from "./chunk-XLORAICV.js";
|
|
4
|
+
|
|
1
5
|
// package.json
|
|
2
|
-
var version = "2.
|
|
6
|
+
var version = "2.8.0";
|
|
3
7
|
|
|
4
8
|
// src/canvas/query.ts
|
|
5
9
|
function appendCanvasQuery(target, params) {
|
|
@@ -305,6 +309,9 @@ var CanvasHttpClient = class {
|
|
|
305
309
|
* `/api/quiz/v1` is excluded by pathname prefix.
|
|
306
310
|
*/
|
|
307
311
|
canvasHeaders(url) {
|
|
312
|
+
if (this.token === "") {
|
|
313
|
+
throw new Error(MISSING_CREDENTIALS_TOOL_CALL_ERROR);
|
|
314
|
+
}
|
|
308
315
|
return {
|
|
309
316
|
Authorization: `Bearer ${this.token}`,
|
|
310
317
|
"User-Agent": USER_AGENT,
|
|
@@ -336,4 +343,4 @@ export {
|
|
|
336
343
|
CanvasApiError,
|
|
337
344
|
CanvasHttpClient
|
|
338
345
|
};
|
|
339
|
-
//# sourceMappingURL=chunk-
|
|
346
|
+
//# sourceMappingURL=chunk-5EZSBL5J.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../package.json","../src/canvas/query.ts","../src/canvas/normalize-ids.ts","../src/canvas/string-ids.ts","../src/canvas/client.ts"],"sourcesContent":["{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"2.7.0\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 183 tools across Canvas courses, assignments, grades, gradebook history, quizzes, New Quizzes (LTI), outcomes, discussions, admin workflows, and more.\",\n \"type\": \"module\",\n \"types\": \"./dist/server.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\",\n \"require\": \"./dist/server.cjs\"\n },\n \"./stdio\": {\n \"types\": \"./dist/stdio.d.ts\",\n \"import\": \"./dist/stdio.js\",\n \"require\": \"./dist/stdio.cjs\"\n },\n \"./http\": {\n \"types\": \"./dist/http.d.ts\",\n \"import\": \"./dist/http.js\",\n \"require\": \"./dist/http.cjs\"\n },\n \"./canvas\": {\n \"types\": \"./dist/canvas/index.d.ts\",\n \"import\": \"./dist/canvas/index.js\",\n \"require\": \"./dist/canvas/index.cjs\"\n }\n },\n \"bin\": {\n \"canvas-lms-mcp\": \"bin/canvas-lms-mcp.js\"\n },\n \"files\": [\n \"bin/\",\n \"dist/\",\n \"skills/\"\n ],\n \"scripts\": {\n \"dev\": \"tsup --watch\",\n \"build\": \"tsup && tsc -p tsconfig.build.json\",\n \"generate:manifests\": \"tsx scripts/generate-manifests.ts\",\n \"generate:prompts\": \"tsx scripts/generate-prompts.ts\",\n \"mcpb:pack\": \"node scripts/pack-mcpb.mjs\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"lint\": \"eslint src/ tests/ && prettier --check src/ tests/\",\n \"lint:fix\": \"eslint src/ tests/ --fix && prettier --write src/ tests/\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"keywords\": [\n \"canvas\",\n \"canvas-lms\",\n \"lms\",\n \"mcp\",\n \"model-context-protocol\",\n \"education\",\n \"grading\",\n \"ai-agent\",\n \"ai-tools\",\n \"claude-desktop\",\n \"cursor\",\n \"vscode\",\n \"instructure\",\n \"education-technology\",\n \"edtech\",\n \"teacher\",\n \"student\",\n \"grading-automation\",\n \"typescript\"\n ],\n \"author\": \"Christian Bru\",\n \"license\": \"MIT\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/bruchris/canvas-lms-mcp.git\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@10.11.0\",\n \"dependencies\": {\n \"@iarna/toml\": \"^2.2.5\",\n \"@modelcontextprotocol/sdk\": \"^1.32.1\",\n \"prompts\": \"^2.4.2\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^10.0.1\",\n \"@types/jsdom\": \"^30.0.0\",\n \"@types/node\": \"^26.4.0\",\n \"@types/prompts\": \"^2.4.9\",\n \"@typescript-eslint/eslint-plugin\": \"^8.65.0\",\n \"@typescript-eslint/parser\": \"^8.65.0\",\n \"@vitest/coverage-v8\": \"^4.1.10\",\n \"ajv\": \"^8.20.0\",\n \"eslint\": \"^10.7.0\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-prettier\": \"^5.5.6\",\n \"jsdom\": \"^30.0.1\",\n \"prettier\": \"^3.9.6\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.23.1\",\n \"typescript\": \"^6.0.3\",\n \"typescript-eslint\": \"^8.65.0\",\n \"vite\": \"^8.1.5\",\n \"vitest\": \"^4.1.10\",\n \"yaml\": \"^2.8.3\"\n }\n}\n","export type CanvasQueryPrimitive = string | number | boolean\n\nexport type CanvasQueryValue = CanvasQueryPrimitive | ReadonlyArray<CanvasQueryPrimitive>\n\nexport type CanvasQueryParams = Record<string, CanvasQueryValue | undefined | null>\n\nexport function appendCanvasQuery(target: URLSearchParams, params?: CanvasQueryParams): void {\n if (!params) return\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n const arrayKey = key.endsWith('[]') ? key : `${key}[]`\n for (const item of value) {\n if (item === undefined || item === null) continue\n target.append(arrayKey, String(item))\n }\n } else {\n target.set(key, String(value))\n }\n }\n}\n\nexport function toCanvasQuery(params?: CanvasQueryParams): URLSearchParams {\n const search = new URLSearchParams()\n appendCanvasQuery(search, params)\n return search\n}\n","/**\n * Response-side Canvas identifier normalization.\n *\n * Phase 2, PR 2a of the 64-bit identifier design\n * (`docs/superpowers/specs/2026-10-05-bru-2730-canvas-64bit-identifiers.md`\n * §4.3, BRU-2730 / BRU-2828). Every parsed Canvas response body passes through\n * `normalizeCanvasIds()` on all three `CanvasHttpClient` paths, so that after\n * the HTTP boundary **every identifier in the process is a `CanvasId` string**\n * — the same representation `canvasIdInput()` produces on the way in.\n *\n * ## Why this runs unconditionally, and lands before the Accept header\n *\n * Canvas offers `Accept: application/json+canvas-string-ids`, which stringifies\n * response IDs. On its own it is **not** a drop-in, and the measurement that\n * settles it is a grade:\n *\n * `StringifyIds.recursively_stringify_ids` converts a value only when the key\n * matches `/(^|_)id$/i` (scalar) or `/(^|_)ids$/i` (array). Canvas's\n * `assignment_group.rules.never_drop` holds a list of assignment IDs and\n * matches neither, so the header leaves it numeric while stringifying the\n * sibling `assignment.id` it is compared against. `src/tools/grade-engine.ts`\n * builds `new Set(rules.never_drop)` and tests `has(assignment.id)`, so every\n * pinned assignment silently becomes droppable: on the §2.3.1 fixture the\n * reported course grade moves from **47.5 to 92.5**, with no error and no log\n * line. `explain_grade` and `project_grade` share that engine.\n *\n * So the normalization must be uniform, and it must hold whether or not the\n * header was sent. Then PR 2b's header becomes a pure precision improvement\n * rather than a semantic change, and it can be turned on and off without\n * changing any join. The order is not negotiable: normalization first.\n *\n * ## Why the key policy is Canvas's regexes *plus* an explicit exception list\n *\n * The two regexes are the right base — on today's `types.ts` no field whose\n * name matches `(^|_)ids?$` is a quantity, so the over-match risk on them is\n * zero (§3.1). But they miss 12 identifier fields (§3.2), and those are exactly\n * the ones that fail a join silently. The over-match risk on the *exceptions*\n * is **not** zero, which is why they carry a parent scope (§4.3).\n *\n * `tests/canvas/normalize-ids.test.ts` asserts the inverse of this list\n * directly against `src/canvas/types.ts`: no numeric field may remain that this\n * module would convert. That is the soundness gate — a field this module\n * stringifies while `types.ts` still declares it `number` is a declared type\n * that lies.\n */\n\n/** Canvas's scalar key rule, from `gems/stringify_ids`. */\nconst SCALAR_ID_KEY = /(^|_)id$/i\n\n/** Canvas's array key rule, from `gems/stringify_ids`. */\nconst ARRAY_ID_KEY = /(^|_)ids$/i\n\n/**\n * The parent of a property at the top of a response body, where there is no\n * enclosing key. A distinct value rather than `null` so that an exception\n * declaring it reads as a deliberate choice about a wire shape.\n */\nexport const RESPONSE_ROOT = Symbol('canvas response root')\nexport type ResponseParent = string | typeof RESPONSE_ROOT\n\nexport interface IdFieldException {\n /** The property name, exactly as Canvas spells it on the wire. */\n readonly name: string\n /** `scalar` for a single identifier, `array` for a list of them. */\n readonly kind: 'scalar' | 'array'\n /**\n * The nearest enclosing property names this exception applies under, or\n * `'any'` where the name is unique enough across Canvas's payloads that a\n * scope would add no safety. Array indices are transparent: the parent of a\n * property inside `graders: [{…}]` is `graders`.\n */\n readonly parents: 'any' | readonly ResponseParent[]\n /** Why Canvas's own regexes miss it, and what breaks if it is left numeric. */\n readonly why: string\n}\n\n/**\n * The 12 identifier fields of §3.2, as 11 entries — `links.user` covers both\n * `CanvasOutcomeResult.links.user` and `CanvasOutcomeRollup.links.user`.\n *\n * Exported so the tests can assert a count floor on it: §8 requires that an\n * entry cannot be silently dropped, because dropping one reintroduces a silent\n * join failure rather than a visible error.\n */\nexport const ID_FIELD_EXCEPTIONS: readonly IdFieldException[] = [\n {\n name: 'never_drop',\n kind: 'array',\n parents: 'any',\n why: 'Assignment IDs pinned against `drop_lowest`. The §2.3 grade divergence: left numeric, every pinned assignment silently becomes droppable.',\n },\n {\n name: 'assignment_visibility',\n kind: 'array',\n parents: 'any',\n why: 'User IDs a differentiated assignment is visible to. Left numeric, visibility checks silently come back empty.',\n },\n {\n name: 'assignments',\n // `GradebookHistoryModule.listDays()` returns `[{date, graders: [...]}]`,\n // but `getDay()` returns the grader array *at the response root* with no\n // `graders` key at all. Scoping this to `graders` alone would silently miss\n // `get_gradebook_history_day` — the §4.3 instruction to path-scope this\n // field is right, but one parent is not enough to cover its two call sites.\n kind: 'array',\n parents: [RESPONSE_ROOT, 'graders'],\n why: 'Assignment IDs a grader touched, on `CanvasGradebookHistoryGrader` only. Left numeric, gradebook-history joins silently miss.',\n },\n {\n name: 'courseId',\n kind: 'scalar',\n parents: 'any',\n why: 'Dashboard cards are the one camelCase payload Canvas returns; `(^|_)id$` needs `^id` or `_id`, and `courseId` matches neither.',\n },\n {\n name: 'answer',\n // Scoped to the envelope key `getSubmissionAnswers()` reads, because a bare\n // name match would reach free-text answers in unrelated payloads (§4.3).\n kind: 'scalar',\n parents: ['quiz_submission_questions'],\n why: 'An answer ID for choice question types. See the polymorphism note below — this field is not an identifier for every question type.',\n },\n {\n name: 'course',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup's course ID. The three `links` objects in types.ts are all outcome-related; these member names are far too generic to match unscoped.\",\n },\n {\n name: 'user',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup's and an outcome result's user ID — two field sites, one entry.\",\n },\n {\n name: 'section',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup's course-section ID.\",\n },\n {\n name: 'learning_outcome',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome result's outcome ID. Already `string | number` on the wire, which is itself evidence that Canvas's outcomes endpoints return these as strings on some paths.\",\n },\n {\n name: 'alignment',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome result's alignment ID. Already `string | number`.\",\n },\n {\n name: 'outcome',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup score's outcome ID. Its own siblings `score` and `count` are quantities in the same object — the clearest illustration of why this has to be field-level, not type-level.\",\n },\n]\n\n/**\n * `answer` is the one exception whose field is **polymorphic by question type**,\n * and §3.2's description of it (\"an answer ID for choice questions\") is not the\n * whole story: a `numerical_question` answer is a quantity, and\n * `CanvasQuizSubmissionQuestion` carries no `question_type` to discriminate on.\n *\n * `Number.isInteger` is therefore the only guard available, which bounds the\n * blast radius to *integral* numerical answers: `42.5` survives as a number,\n * `42` becomes `\"42\"`. The field's declared type already admits `string`, its\n * sole consumer (`src/tools/quiz-question-responses.ts`) passes it straight\n * into a text payload without joining on it, and the alternative — dropping it\n * from the list — leaves choice-question answer IDs unjoinable, which is the\n * failure §4.3 exists to remove. Recorded here rather than left implicit.\n */\n\ntype ExceptionIndex = ReadonlyMap<string, 'any' | ReadonlySet<ResponseParent>>\n\nfunction indexExceptions(kind: IdFieldException['kind']): ExceptionIndex {\n const index = new Map<string, 'any' | ReadonlySet<ResponseParent>>()\n for (const exception of ID_FIELD_EXCEPTIONS) {\n if (exception.kind !== kind) continue\n index.set(exception.name, exception.parents === 'any' ? 'any' : new Set(exception.parents))\n }\n return index\n}\n\nconst SCALAR_EXCEPTIONS = indexExceptions('scalar')\nconst ARRAY_EXCEPTIONS = indexExceptions('array')\n\nfunction isExcepted(index: ExceptionIndex, key: string, parent: ResponseParent): boolean {\n const parents = index.get(key)\n if (parents === undefined) return false\n return parents === 'any' || parents.has(parent)\n}\n\n/**\n * Canvas's own conversion guard: only an `Integer` becomes a string. A float\n * under an identifier key is not an identifier, and stringifying it would be a\n * quantity change rather than a representation change.\n */\nfunction normalizeScalar(value: unknown): unknown {\n return typeof value === 'number' && Number.isInteger(value) ? String(value) : value\n}\n\nfunction visit(node: unknown, parent: ResponseParent): void {\n if (Array.isArray(node)) {\n // Array indices are transparent: the members of an object inside\n // `graders: [{…}]` have `graders` as their parent, not an index.\n for (const item of node) {\n if (item !== null && typeof item === 'object') visit(item, parent)\n }\n return\n }\n if (node === null || typeof node !== 'object') return\n\n const record = node as Record<string, unknown>\n for (const key of Object.keys(record)) {\n const value = record[key]\n\n if (typeof value === 'number') {\n if (SCALAR_ID_KEY.test(key) || isExcepted(SCALAR_EXCEPTIONS, key, parent)) {\n record[key] = normalizeScalar(value)\n }\n continue\n }\n\n if (Array.isArray(value)) {\n if (ARRAY_ID_KEY.test(key) || isExcepted(ARRAY_EXCEPTIONS, key, parent)) {\n for (let index = 0; index < value.length; index += 1) {\n value[index] = normalizeScalar(value[index])\n }\n }\n visit(value, key)\n continue\n }\n\n if (value !== null && typeof value === 'object') visit(value, key)\n }\n}\n\n/**\n * Rewrites every Canvas identifier in a parsed response body to its canonical\n * decimal string, **in place**, and returns the same reference.\n *\n * In place because this runs on every response on every path: a structural copy\n * would double the allocation of the largest thing the client handles, and\n * nothing holds a reference to the pre-normalization body (it is the direct\n * result of `response.json()`).\n */\nexport function normalizeCanvasIds<T>(body: T): T {\n visit(body, RESPONSE_ROOT)\n return body\n}\n","/**\n * Opt-in Canvas string-ID response negotiation.\n *\n * Phase 2, PR 2b of the 64-bit identifier design\n * (`docs/superpowers/specs/2026-10-05-bru-2730-canvas-64bit-identifiers.md`\n * §5 / §8, BRU-2730 / BRU-2851), landing strictly after PR 2a's unconditional\n * `normalizeCanvasIds()`.\n *\n * ## What the header buys, and what it cannot\n *\n * `response.json()` is `JSON.parse`, so a Canvas ID above `Number.MAX_SAFE_INTEGER`\n * is already rounded before any code of ours runs: the shard-901 id\n * `9010000000000001` arrives as `9010000000000000`. No amount of normalization\n * recovers a digit that never arrived. Asking Canvas to render identifiers as\n * JSON **strings** is the only fix, and this header is how you ask.\n *\n * It is best-effort on purpose. `ApplicationController#render` applies\n * `json_cast` only `unless json.is_a?(String)`, so an endpoint that renders\n * pre-serialized JSON answers with numbers even when the header was sent.\n * That is exactly why §4.3's normalization runs unconditionally and this is a\n * pure precision improvement layered on top: every join in the process keeps\n * working in both modes, and the flag can be flipped without changing\n * semantics.\n *\n * ## Why the flag comparison is byte-exact\n *\n * This flag changes the wire type of every identifier in every response. Every\n * trim, lowercase or truthiness step widens the set of strings that\n * *accidentally* enable it — `TRUE` out of a YAML file, `1` out of a template,\n * a trailing space out of a paste — and each of those is a plausible accident.\n * So the comparison is `=== 'true'` and nothing else. This deliberately does\n * **not** use `isEnvTruthy()` from `src/env.ts`, which accepts\n * `true`/`1`/`yes`/`on` case-insensitively after trimming.\n */\n\n/** The only environment variable that enables negotiation. */\nexport const CANVAS_STRING_IDS_ENV_VAR = 'CANVAS_STRING_IDS'\n\n/**\n * Canvas gates on `request.headers[\"Accept\"]&.include?(\"application/json+canvas-string-ids\")`\n * — a substring test (`application_controller.rb:2978`, `stringify_json_ids?`).\n * The plain `application/json` fallback therefore costs nothing and keeps the\n * request acceptable to a proxy or endpoint that does not know the vendor type.\n */\nexport const CANVAS_STRING_IDS_ACCEPT = 'application/json+canvas-string-ids, application/json'\n\n/**\n * New Quizzes. `json_cast` is an `ApplicationController` concern and\n * `/api/quiz/v1` is served by the separate New Quizzes service, which does not\n * implement the vendor media type — a code search for `quiz/v1` under Canvas's\n * `config/` returns no routes. Our own types already record that it answers\n * differently: `CanvasNewQuizItem.id` is a `string` while `CanvasNewQuiz.id` is\n * a `number`. Sending the header there negotiates nothing, so excluding it\n * keeps those 11 call sites byte-identical to their pre-flag requests instead\n * of betting on an unverified service (§5, open question 2).\n */\nconst NEW_QUIZZES_PATH_PREFIX = '/api/quiz/v1'\n\n/** Byte-exact, deliberately un-normalized. See the module comment. */\nfunction negotiationEnabled(): boolean {\n return process.env[CANVAS_STRING_IDS_ENV_VAR] === 'true'\n}\n\n/**\n * The `Accept` header to merge into a Canvas request, or `undefined` when this\n * request must not negotiate. Spread it into a header record: `{...undefined}`\n * is `{}`, so a disabled flag adds no key at all and the request stays\n * byte-identical to the pre-flag one.\n *\n * Decided per URL rather than per client so that a `Link` rel=\"next\" URL is\n * judged on its own pathname, and read from the environment per call so the\n * flag has no construction-order dependency.\n *\n * Fails **closed** on a URL that does not parse: no header means numbers,\n * which is the pre-2b status quo, and §4.3 normalization still applies.\n */\nexport function stringIdsAcceptHeader(url: string): { Accept: string } | undefined {\n if (!negotiationEnabled()) return undefined\n\n let pathname: string\n try {\n pathname = new URL(url).pathname\n } catch {\n return undefined\n }\n if (pathname.startsWith(NEW_QUIZZES_PATH_PREFIX)) return undefined\n\n return { Accept: CANVAS_STRING_IDS_ACCEPT }\n}\n","import { version } from '../../package.json'\nimport { normalizeCanvasIds } from './normalize-ids'\nimport { appendCanvasQuery, type CanvasQueryParams } from './query'\nimport { stringIdsAcceptHeader } from './string-ids'\nimport type { CanvasClientConfig, CanvasErrorResponse } from './types'\n\nexport class CanvasApiError extends Error {\n status: number\n endpoint: string\n\n constructor(message: string, status: number, endpoint: string) {\n super(message)\n this.name = 'CanvasApiError'\n this.status = status\n this.endpoint = endpoint\n }\n}\n\nconst DEFAULT_MAX_PAGINATION_PAGES = 1000\nconst USER_AGENT = `canvas-lms-mcp/${version}`\n\nexport interface CanvasRequestOptions extends RequestInit {\n /**\n * Query params to append to the endpoint URL. Arrays produce repeated\n * `key[]=v1&key[]=v2` entries (the convention Canvas uses for `include[]`).\n * Preserves any query string already present on the endpoint.\n */\n query?: CanvasQueryParams\n}\n\nexport class CanvasHttpClient {\n private token: string\n private _baseUrl: string\n private maxPaginationPages: number\n\n constructor(config: CanvasClientConfig) {\n this.token = config.token\n this._baseUrl = config.baseUrl.replace(/\\/+$/, '')\n this.maxPaginationPages = config.maxPaginationPages ?? DEFAULT_MAX_PAGINATION_PAGES\n }\n\n get baseUrl(): string {\n return this._baseUrl\n }\n\n /**\n * Makes a single authenticated request to the Canvas API.\n * Returns `undefined` (typed as `T`) when Canvas responds with 204 No Content,\n * which is the expected response for DELETE operations.\n */\n async request<T>(endpoint: string, options: CanvasRequestOptions = {}): Promise<T> {\n const { query, ...init } = options\n let url = endpoint.startsWith('http') ? endpoint : `${this._baseUrl}${endpoint}`\n if (query) {\n const parsed = new URL(url)\n appendCanvasQuery(parsed.searchParams, query)\n url = parsed.toString()\n }\n\n const method = (init.method ?? 'GET').toUpperCase()\n if (init.body != null && (method === 'GET' || method === 'HEAD')) {\n throw new Error(\n `GET requests must not include a body (Canvas CloudFront CDN rejects them with 403): ${endpoint}`,\n )\n }\n\n const headers: Record<string, string> = this.canvasHeaders(url)\n if (init.body) {\n headers['Content-Type'] = 'application/json'\n }\n\n const response = await fetch(url, {\n ...init,\n headers: {\n ...headers,\n ...init.headers,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n if (response.status === 204) {\n return undefined as T\n }\n\n return normalizeCanvasIds((await response.json()) as T)\n }\n\n async paginate<T>(\n endpoint: string,\n params?: CanvasQueryParams,\n options?: { maxItems?: number },\n ): Promise<T[]> {\n const maxItems = options?.maxItems\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n let stoppedAtMaxItems = false\n\n while (nextUrl && pages < this.maxPaginationPages) {\n // Inside the loop, keyed on `nextUrl`: a followed `Link` rel=\"next\" URL\n // must get the same header block, and is judged on its own pathname.\n const response = await fetch(nextUrl, { headers: this.canvasHeaders(nextUrl) })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n const data = normalizeCanvasIds((await response.json()) as T[])\n results.push(...data)\n pages++\n\n if (maxItems !== undefined && results.length >= maxItems) {\n stoppedAtMaxItems = true\n break\n }\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n // A deliberate maxItems stop leaves `nextUrl` non-null on purpose — that\n // is not the page-cap truncation assertNotTruncated exists to catch.\n if (!stoppedAtMaxItems) {\n this.assertNotTruncated(nextUrl, endpoint)\n }\n return stoppedAtMaxItems ? results.slice(0, maxItems) : results\n }\n\n async paginateEnvelope<T>(\n endpoint: string,\n envelopeKey: string,\n params?: CanvasQueryParams,\n ): Promise<T[]> {\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n\n while (nextUrl && pages < this.maxPaginationPages) {\n // Inside the loop, keyed on `nextUrl`: a followed `Link` rel=\"next\" URL\n // must get the same header block, and is judged on its own pathname.\n const response = await fetch(nextUrl, { headers: this.canvasHeaders(nextUrl) })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n // Normalized before the envelope is opened, not after: the `answer`\n // exception in `normalize-ids.ts` is scoped to the envelope key it\n // arrives under, so the walk has to see the whole body (§4.3).\n const body = normalizeCanvasIds((await response.json()) as Record<string, T[]>)\n const data = body[envelopeKey] ?? []\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n this.assertNotTruncated(nextUrl, endpoint)\n return results\n }\n\n /**\n * The header block every Canvas request shares. Defining it in one place is\n * what makes \"the string-ID Accept header is on all three response paths\"\n * (BRU-2730 §5) structural rather than three coincidences.\n *\n * Takes the fully-resolved URL because the negotiation decision is per URL:\n * `/api/quiz/v1` is excluded by pathname prefix.\n */\n private canvasHeaders(url: string): Record<string, string> {\n return {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n ...stringIdsAcceptHeader(url),\n }\n }\n\n /**\n * The pagination loops exit with a non-null `nextUrl` only when the page cap\n * was reached while Canvas still advertised another page. Surface that as an\n * error rather than returning a partial list that looks complete.\n */\n private assertNotTruncated(nextUrl: string | null, endpoint: string): void {\n if (!nextUrl) return\n throw new Error(\n `Results are incomplete: ${endpoint} has more pages beyond the configured limit of ${this.maxPaginationPages} pages, so the list was truncated. Narrow the query with filters or raise maxPaginationPages.`,\n )\n }\n\n private parseNextLink(linkHeader: string | null): string | null {\n if (!linkHeader) return null\n const match = linkHeader.match(/<([^>]+)>;\\s*rel=\"next\"/)\n return match?.[1] ?? null\n }\n}\n"],"mappings":"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","names":["body"]}
|
|
1
|
+
{"version":3,"sources":["../package.json","../src/canvas/query.ts","../src/canvas/normalize-ids.ts","../src/canvas/string-ids.ts","../src/canvas/client.ts"],"sourcesContent":["{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"2.8.0\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 183 tools across Canvas courses, assignments, grades, gradebook history, quizzes, New Quizzes (LTI), outcomes, discussions, admin workflows, and more.\",\n \"type\": \"module\",\n \"types\": \"./dist/server.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/server.d.ts\",\n \"import\": \"./dist/server.js\",\n \"require\": \"./dist/server.cjs\"\n },\n \"./stdio\": {\n \"types\": \"./dist/stdio.d.ts\",\n \"import\": \"./dist/stdio.js\",\n \"require\": \"./dist/stdio.cjs\"\n },\n \"./http\": {\n \"types\": \"./dist/http.d.ts\",\n \"import\": \"./dist/http.js\",\n \"require\": \"./dist/http.cjs\"\n },\n \"./canvas\": {\n \"types\": \"./dist/canvas/index.d.ts\",\n \"import\": \"./dist/canvas/index.js\",\n \"require\": \"./dist/canvas/index.cjs\"\n }\n },\n \"bin\": {\n \"canvas-lms-mcp\": \"bin/canvas-lms-mcp.js\"\n },\n \"files\": [\n \"bin/\",\n \"dist/\",\n \"skills/\"\n ],\n \"scripts\": {\n \"dev\": \"tsup --watch\",\n \"build\": \"tsup && tsc -p tsconfig.build.json\",\n \"generate:manifests\": \"tsx scripts/generate-manifests.ts\",\n \"generate:prompts\": \"tsx scripts/generate-prompts.ts\",\n \"mcpb:pack\": \"node scripts/pack-mcpb.mjs\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"lint\": \"eslint src/ tests/ && prettier --check src/ tests/\",\n \"lint:fix\": \"eslint src/ tests/ --fix && prettier --write src/ tests/\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"keywords\": [\n \"canvas\",\n \"canvas-lms\",\n \"lms\",\n \"mcp\",\n \"model-context-protocol\",\n \"education\",\n \"grading\",\n \"ai-agent\",\n \"ai-tools\",\n \"claude-desktop\",\n \"cursor\",\n \"vscode\",\n \"instructure\",\n \"education-technology\",\n \"edtech\",\n \"teacher\",\n \"student\",\n \"grading-automation\",\n \"typescript\"\n ],\n \"author\": \"Christian Bru\",\n \"license\": \"MIT\",\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/bruchris/canvas-lms-mcp.git\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"packageManager\": \"pnpm@10.11.0\",\n \"dependencies\": {\n \"@iarna/toml\": \"^2.2.5\",\n \"@modelcontextprotocol/sdk\": \"^1.32.1\",\n \"prompts\": \"^2.4.2\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^10.0.1\",\n \"@types/jsdom\": \"^30.0.0\",\n \"@types/node\": \"^26.4.0\",\n \"@types/prompts\": \"^2.4.9\",\n \"@typescript-eslint/eslint-plugin\": \"^8.65.0\",\n \"@typescript-eslint/parser\": \"^8.65.0\",\n \"@vitest/coverage-v8\": \"^4.1.10\",\n \"ajv\": \"^8.20.0\",\n \"eslint\": \"^10.7.0\",\n \"eslint-config-prettier\": \"^10.1.8\",\n \"eslint-plugin-prettier\": \"^5.5.6\",\n \"jsdom\": \"^30.0.1\",\n \"prettier\": \"^3.9.6\",\n \"tsup\": \"^8.5.1\",\n \"tsx\": \"^4.23.1\",\n \"typescript\": \"^6.0.3\",\n \"typescript-eslint\": \"^8.65.0\",\n \"vite\": \"^8.1.5\",\n \"vitest\": \"^4.1.10\",\n \"yaml\": \"^2.8.3\"\n }\n}\n","export type CanvasQueryPrimitive = string | number | boolean\n\nexport type CanvasQueryValue = CanvasQueryPrimitive | ReadonlyArray<CanvasQueryPrimitive>\n\nexport type CanvasQueryParams = Record<string, CanvasQueryValue | undefined | null>\n\nexport function appendCanvasQuery(target: URLSearchParams, params?: CanvasQueryParams): void {\n if (!params) return\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n const arrayKey = key.endsWith('[]') ? key : `${key}[]`\n for (const item of value) {\n if (item === undefined || item === null) continue\n target.append(arrayKey, String(item))\n }\n } else {\n target.set(key, String(value))\n }\n }\n}\n\nexport function toCanvasQuery(params?: CanvasQueryParams): URLSearchParams {\n const search = new URLSearchParams()\n appendCanvasQuery(search, params)\n return search\n}\n","/**\n * Response-side Canvas identifier normalization.\n *\n * Phase 2, PR 2a of the 64-bit identifier design\n * (`docs/superpowers/specs/2026-10-05-bru-2730-canvas-64bit-identifiers.md`\n * §4.3, BRU-2730 / BRU-2828). Every parsed Canvas response body passes through\n * `normalizeCanvasIds()` on all three `CanvasHttpClient` paths, so that after\n * the HTTP boundary **every identifier in the process is a `CanvasId` string**\n * — the same representation `canvasIdInput()` produces on the way in.\n *\n * ## Why this runs unconditionally, and lands before the Accept header\n *\n * Canvas offers `Accept: application/json+canvas-string-ids`, which stringifies\n * response IDs. On its own it is **not** a drop-in, and the measurement that\n * settles it is a grade:\n *\n * `StringifyIds.recursively_stringify_ids` converts a value only when the key\n * matches `/(^|_)id$/i` (scalar) or `/(^|_)ids$/i` (array). Canvas's\n * `assignment_group.rules.never_drop` holds a list of assignment IDs and\n * matches neither, so the header leaves it numeric while stringifying the\n * sibling `assignment.id` it is compared against. `src/tools/grade-engine.ts`\n * builds `new Set(rules.never_drop)` and tests `has(assignment.id)`, so every\n * pinned assignment silently becomes droppable: on the §2.3.1 fixture the\n * reported course grade moves from **47.5 to 92.5**, with no error and no log\n * line. `explain_grade` and `project_grade` share that engine.\n *\n * So the normalization must be uniform, and it must hold whether or not the\n * header was sent. Then PR 2b's header becomes a pure precision improvement\n * rather than a semantic change, and it can be turned on and off without\n * changing any join. The order is not negotiable: normalization first.\n *\n * ## Why the key policy is Canvas's regexes *plus* an explicit exception list\n *\n * The two regexes are the right base — on today's `types.ts` no field whose\n * name matches `(^|_)ids?$` is a quantity, so the over-match risk on them is\n * zero (§3.1). But they miss 12 identifier fields (§3.2), and those are exactly\n * the ones that fail a join silently. The over-match risk on the *exceptions*\n * is **not** zero, which is why they carry a parent scope (§4.3).\n *\n * `tests/canvas/normalize-ids.test.ts` asserts the inverse of this list\n * directly against `src/canvas/types.ts`: no numeric field may remain that this\n * module would convert. That is the soundness gate — a field this module\n * stringifies while `types.ts` still declares it `number` is a declared type\n * that lies.\n */\n\n/** Canvas's scalar key rule, from `gems/stringify_ids`. */\nconst SCALAR_ID_KEY = /(^|_)id$/i\n\n/** Canvas's array key rule, from `gems/stringify_ids`. */\nconst ARRAY_ID_KEY = /(^|_)ids$/i\n\n/**\n * The parent of a property at the top of a response body, where there is no\n * enclosing key. A distinct value rather than `null` so that an exception\n * declaring it reads as a deliberate choice about a wire shape.\n */\nexport const RESPONSE_ROOT = Symbol('canvas response root')\nexport type ResponseParent = string | typeof RESPONSE_ROOT\n\nexport interface IdFieldException {\n /** The property name, exactly as Canvas spells it on the wire. */\n readonly name: string\n /** `scalar` for a single identifier, `array` for a list of them. */\n readonly kind: 'scalar' | 'array'\n /**\n * The nearest enclosing property names this exception applies under, or\n * `'any'` where the name is unique enough across Canvas's payloads that a\n * scope would add no safety. Array indices are transparent: the parent of a\n * property inside `graders: [{…}]` is `graders`.\n */\n readonly parents: 'any' | readonly ResponseParent[]\n /** Why Canvas's own regexes miss it, and what breaks if it is left numeric. */\n readonly why: string\n}\n\n/**\n * The 12 identifier fields of §3.2, as 11 entries — `links.user` covers both\n * `CanvasOutcomeResult.links.user` and `CanvasOutcomeRollup.links.user`.\n *\n * Exported so the tests can assert a count floor on it: §8 requires that an\n * entry cannot be silently dropped, because dropping one reintroduces a silent\n * join failure rather than a visible error.\n */\nexport const ID_FIELD_EXCEPTIONS: readonly IdFieldException[] = [\n {\n name: 'never_drop',\n kind: 'array',\n parents: 'any',\n why: 'Assignment IDs pinned against `drop_lowest`. The §2.3 grade divergence: left numeric, every pinned assignment silently becomes droppable.',\n },\n {\n name: 'assignment_visibility',\n kind: 'array',\n parents: 'any',\n why: 'User IDs a differentiated assignment is visible to. Left numeric, visibility checks silently come back empty.',\n },\n {\n name: 'assignments',\n // `GradebookHistoryModule.listDays()` returns `[{date, graders: [...]}]`,\n // but `getDay()` returns the grader array *at the response root* with no\n // `graders` key at all. Scoping this to `graders` alone would silently miss\n // `get_gradebook_history_day` — the §4.3 instruction to path-scope this\n // field is right, but one parent is not enough to cover its two call sites.\n kind: 'array',\n parents: [RESPONSE_ROOT, 'graders'],\n why: 'Assignment IDs a grader touched, on `CanvasGradebookHistoryGrader` only. Left numeric, gradebook-history joins silently miss.',\n },\n {\n name: 'courseId',\n kind: 'scalar',\n parents: 'any',\n why: 'Dashboard cards are the one camelCase payload Canvas returns; `(^|_)id$` needs `^id` or `_id`, and `courseId` matches neither.',\n },\n {\n name: 'answer',\n // Scoped to the envelope key `getSubmissionAnswers()` reads, because a bare\n // name match would reach free-text answers in unrelated payloads (§4.3).\n kind: 'scalar',\n parents: ['quiz_submission_questions'],\n why: 'An answer ID for choice question types. See the polymorphism note below — this field is not an identifier for every question type.',\n },\n {\n name: 'course',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup's course ID. The three `links` objects in types.ts are all outcome-related; these member names are far too generic to match unscoped.\",\n },\n {\n name: 'user',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup's and an outcome result's user ID — two field sites, one entry.\",\n },\n {\n name: 'section',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup's course-section ID.\",\n },\n {\n name: 'learning_outcome',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome result's outcome ID. Already `string | number` on the wire, which is itself evidence that Canvas's outcomes endpoints return these as strings on some paths.\",\n },\n {\n name: 'alignment',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome result's alignment ID. Already `string | number`.\",\n },\n {\n name: 'outcome',\n kind: 'scalar',\n parents: ['links'],\n why: \"An outcome rollup score's outcome ID. Its own siblings `score` and `count` are quantities in the same object — the clearest illustration of why this has to be field-level, not type-level.\",\n },\n]\n\n/**\n * `answer` is the one exception whose field is **polymorphic by question type**,\n * and §3.2's description of it (\"an answer ID for choice questions\") is not the\n * whole story: a `numerical_question` answer is a quantity, and\n * `CanvasQuizSubmissionQuestion` carries no `question_type` to discriminate on.\n *\n * `Number.isInteger` is therefore the only guard available, which bounds the\n * blast radius to *integral* numerical answers: `42.5` survives as a number,\n * `42` becomes `\"42\"`. The field's declared type already admits `string`, its\n * sole consumer (`src/tools/quiz-question-responses.ts`) passes it straight\n * into a text payload without joining on it, and the alternative — dropping it\n * from the list — leaves choice-question answer IDs unjoinable, which is the\n * failure §4.3 exists to remove. Recorded here rather than left implicit.\n */\n\ntype ExceptionIndex = ReadonlyMap<string, 'any' | ReadonlySet<ResponseParent>>\n\nfunction indexExceptions(kind: IdFieldException['kind']): ExceptionIndex {\n const index = new Map<string, 'any' | ReadonlySet<ResponseParent>>()\n for (const exception of ID_FIELD_EXCEPTIONS) {\n if (exception.kind !== kind) continue\n index.set(exception.name, exception.parents === 'any' ? 'any' : new Set(exception.parents))\n }\n return index\n}\n\nconst SCALAR_EXCEPTIONS = indexExceptions('scalar')\nconst ARRAY_EXCEPTIONS = indexExceptions('array')\n\nfunction isExcepted(index: ExceptionIndex, key: string, parent: ResponseParent): boolean {\n const parents = index.get(key)\n if (parents === undefined) return false\n return parents === 'any' || parents.has(parent)\n}\n\n/**\n * Canvas's own conversion guard: only an `Integer` becomes a string. A float\n * under an identifier key is not an identifier, and stringifying it would be a\n * quantity change rather than a representation change.\n */\nfunction normalizeScalar(value: unknown): unknown {\n return typeof value === 'number' && Number.isInteger(value) ? String(value) : value\n}\n\nfunction visit(node: unknown, parent: ResponseParent): void {\n if (Array.isArray(node)) {\n // Array indices are transparent: the members of an object inside\n // `graders: [{…}]` have `graders` as their parent, not an index.\n for (const item of node) {\n if (item !== null && typeof item === 'object') visit(item, parent)\n }\n return\n }\n if (node === null || typeof node !== 'object') return\n\n const record = node as Record<string, unknown>\n for (const key of Object.keys(record)) {\n const value = record[key]\n\n if (typeof value === 'number') {\n if (SCALAR_ID_KEY.test(key) || isExcepted(SCALAR_EXCEPTIONS, key, parent)) {\n record[key] = normalizeScalar(value)\n }\n continue\n }\n\n if (Array.isArray(value)) {\n if (ARRAY_ID_KEY.test(key) || isExcepted(ARRAY_EXCEPTIONS, key, parent)) {\n for (let index = 0; index < value.length; index += 1) {\n value[index] = normalizeScalar(value[index])\n }\n }\n visit(value, key)\n continue\n }\n\n if (value !== null && typeof value === 'object') visit(value, key)\n }\n}\n\n/**\n * Rewrites every Canvas identifier in a parsed response body to its canonical\n * decimal string, **in place**, and returns the same reference.\n *\n * In place because this runs on every response on every path: a structural copy\n * would double the allocation of the largest thing the client handles, and\n * nothing holds a reference to the pre-normalization body (it is the direct\n * result of `response.json()`).\n */\nexport function normalizeCanvasIds<T>(body: T): T {\n visit(body, RESPONSE_ROOT)\n return body\n}\n","/**\n * Opt-in Canvas string-ID response negotiation.\n *\n * Phase 2, PR 2b of the 64-bit identifier design\n * (`docs/superpowers/specs/2026-10-05-bru-2730-canvas-64bit-identifiers.md`\n * §5 / §8, BRU-2730 / BRU-2851), landing strictly after PR 2a's unconditional\n * `normalizeCanvasIds()`.\n *\n * ## What the header buys, and what it cannot\n *\n * `response.json()` is `JSON.parse`, so a Canvas ID above `Number.MAX_SAFE_INTEGER`\n * is already rounded before any code of ours runs: the shard-901 id\n * `9010000000000001` arrives as `9010000000000000`. No amount of normalization\n * recovers a digit that never arrived. Asking Canvas to render identifiers as\n * JSON **strings** is the only fix, and this header is how you ask.\n *\n * It is best-effort on purpose. `ApplicationController#render` applies\n * `json_cast` only `unless json.is_a?(String)`, so an endpoint that renders\n * pre-serialized JSON answers with numbers even when the header was sent.\n * That is exactly why §4.3's normalization runs unconditionally and this is a\n * pure precision improvement layered on top: every join in the process keeps\n * working in both modes, and the flag can be flipped without changing\n * semantics.\n *\n * ## Why the flag comparison is byte-exact\n *\n * This flag changes the wire type of every identifier in every response. Every\n * trim, lowercase or truthiness step widens the set of strings that\n * *accidentally* enable it — `TRUE` out of a YAML file, `1` out of a template,\n * a trailing space out of a paste — and each of those is a plausible accident.\n * So the comparison is `=== 'true'` and nothing else. This deliberately does\n * **not** use `isEnvTruthy()` from `src/env.ts`, which accepts\n * `true`/`1`/`yes`/`on` case-insensitively after trimming.\n */\n\n/** The only environment variable that enables negotiation. */\nexport const CANVAS_STRING_IDS_ENV_VAR = 'CANVAS_STRING_IDS'\n\n/**\n * Canvas gates on `request.headers[\"Accept\"]&.include?(\"application/json+canvas-string-ids\")`\n * — a substring test (`application_controller.rb:2978`, `stringify_json_ids?`).\n * The plain `application/json` fallback therefore costs nothing and keeps the\n * request acceptable to a proxy or endpoint that does not know the vendor type.\n */\nexport const CANVAS_STRING_IDS_ACCEPT = 'application/json+canvas-string-ids, application/json'\n\n/**\n * New Quizzes. `json_cast` is an `ApplicationController` concern and\n * `/api/quiz/v1` is served by the separate New Quizzes service, which does not\n * implement the vendor media type — a code search for `quiz/v1` under Canvas's\n * `config/` returns no routes. Our own types already record that it answers\n * differently: `CanvasNewQuizItem.id` is a `string` while `CanvasNewQuiz.id` is\n * a `number`. Sending the header there negotiates nothing, so excluding it\n * keeps those 11 call sites byte-identical to their pre-flag requests instead\n * of betting on an unverified service (§5, open question 2).\n */\nconst NEW_QUIZZES_PATH_PREFIX = '/api/quiz/v1'\n\n/** Byte-exact, deliberately un-normalized. See the module comment. */\nfunction negotiationEnabled(): boolean {\n return process.env[CANVAS_STRING_IDS_ENV_VAR] === 'true'\n}\n\n/**\n * The `Accept` header to merge into a Canvas request, or `undefined` when this\n * request must not negotiate. Spread it into a header record: `{...undefined}`\n * is `{}`, so a disabled flag adds no key at all and the request stays\n * byte-identical to the pre-flag one.\n *\n * Decided per URL rather than per client so that a `Link` rel=\"next\" URL is\n * judged on its own pathname, and read from the environment per call so the\n * flag has no construction-order dependency.\n *\n * Fails **closed** on a URL that does not parse: no header means numbers,\n * which is the pre-2b status quo, and §4.3 normalization still applies.\n */\nexport function stringIdsAcceptHeader(url: string): { Accept: string } | undefined {\n if (!negotiationEnabled()) return undefined\n\n let pathname: string\n try {\n pathname = new URL(url).pathname\n } catch {\n return undefined\n }\n if (pathname.startsWith(NEW_QUIZZES_PATH_PREFIX)) return undefined\n\n return { Accept: CANVAS_STRING_IDS_ACCEPT }\n}\n","import { version } from '../../package.json'\nimport { MISSING_CREDENTIALS_TOOL_CALL_ERROR } from '../auth/credential-gate'\nimport { normalizeCanvasIds } from './normalize-ids'\nimport { appendCanvasQuery, type CanvasQueryParams } from './query'\nimport { stringIdsAcceptHeader } from './string-ids'\nimport type { CanvasClientConfig, CanvasErrorResponse } from './types'\n\nexport class CanvasApiError extends Error {\n status: number\n endpoint: string\n\n constructor(message: string, status: number, endpoint: string) {\n super(message)\n this.name = 'CanvasApiError'\n this.status = status\n this.endpoint = endpoint\n }\n}\n\nconst DEFAULT_MAX_PAGINATION_PAGES = 1000\nconst USER_AGENT = `canvas-lms-mcp/${version}`\n\nexport interface CanvasRequestOptions extends RequestInit {\n /**\n * Query params to append to the endpoint URL. Arrays produce repeated\n * `key[]=v1&key[]=v2` entries (the convention Canvas uses for `include[]`).\n * Preserves any query string already present on the endpoint.\n */\n query?: CanvasQueryParams\n}\n\nexport class CanvasHttpClient {\n private token: string\n private _baseUrl: string\n private maxPaginationPages: number\n\n constructor(config: CanvasClientConfig) {\n this.token = config.token\n this._baseUrl = config.baseUrl.replace(/\\/+$/, '')\n this.maxPaginationPages = config.maxPaginationPages ?? DEFAULT_MAX_PAGINATION_PAGES\n }\n\n get baseUrl(): string {\n return this._baseUrl\n }\n\n /**\n * Makes a single authenticated request to the Canvas API.\n * Returns `undefined` (typed as `T`) when Canvas responds with 204 No Content,\n * which is the expected response for DELETE operations.\n */\n async request<T>(endpoint: string, options: CanvasRequestOptions = {}): Promise<T> {\n const { query, ...init } = options\n let url = endpoint.startsWith('http') ? endpoint : `${this._baseUrl}${endpoint}`\n if (query) {\n const parsed = new URL(url)\n appendCanvasQuery(parsed.searchParams, query)\n url = parsed.toString()\n }\n\n const method = (init.method ?? 'GET').toUpperCase()\n if (init.body != null && (method === 'GET' || method === 'HEAD')) {\n throw new Error(\n `GET requests must not include a body (Canvas CloudFront CDN rejects them with 403): ${endpoint}`,\n )\n }\n\n const headers: Record<string, string> = this.canvasHeaders(url)\n if (init.body) {\n headers['Content-Type'] = 'application/json'\n }\n\n const response = await fetch(url, {\n ...init,\n headers: {\n ...headers,\n ...init.headers,\n },\n })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n if (response.status === 204) {\n return undefined as T\n }\n\n return normalizeCanvasIds((await response.json()) as T)\n }\n\n async paginate<T>(\n endpoint: string,\n params?: CanvasQueryParams,\n options?: { maxItems?: number },\n ): Promise<T[]> {\n const maxItems = options?.maxItems\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n let stoppedAtMaxItems = false\n\n while (nextUrl && pages < this.maxPaginationPages) {\n // Inside the loop, keyed on `nextUrl`: a followed `Link` rel=\"next\" URL\n // must get the same header block, and is judged on its own pathname.\n const response = await fetch(nextUrl, { headers: this.canvasHeaders(nextUrl) })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n const data = normalizeCanvasIds((await response.json()) as T[])\n results.push(...data)\n pages++\n\n if (maxItems !== undefined && results.length >= maxItems) {\n stoppedAtMaxItems = true\n break\n }\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n // A deliberate maxItems stop leaves `nextUrl` non-null on purpose — that\n // is not the page-cap truncation assertNotTruncated exists to catch.\n if (!stoppedAtMaxItems) {\n this.assertNotTruncated(nextUrl, endpoint)\n }\n return stoppedAtMaxItems ? results.slice(0, maxItems) : results\n }\n\n async paginateEnvelope<T>(\n endpoint: string,\n envelopeKey: string,\n params?: CanvasQueryParams,\n ): Promise<T[]> {\n const url = new URL(`${this._baseUrl}${endpoint}`)\n if (!url.searchParams.has('per_page')) {\n url.searchParams.set('per_page', '100')\n }\n appendCanvasQuery(url.searchParams, params)\n\n const results: T[] = []\n let nextUrl: string | null = url.toString()\n let pages = 0\n\n while (nextUrl && pages < this.maxPaginationPages) {\n // Inside the loop, keyed on `nextUrl`: a followed `Link` rel=\"next\" URL\n // must get the same header block, and is judged on its own pathname.\n const response = await fetch(nextUrl, { headers: this.canvasHeaders(nextUrl) })\n\n if (!response.ok) {\n const body = (await response.json().catch(() => ({}))) as CanvasErrorResponse\n const message =\n body.errors?.[0]?.message ?? body.message ?? `Canvas API error: ${response.status}`\n throw new CanvasApiError(message, response.status, endpoint)\n }\n\n // Normalized before the envelope is opened, not after: the `answer`\n // exception in `normalize-ids.ts` is scoped to the envelope key it\n // arrives under, so the walk has to see the whole body (§4.3).\n const body = normalizeCanvasIds((await response.json()) as Record<string, T[]>)\n const data = body[envelopeKey] ?? []\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n this.assertNotTruncated(nextUrl, endpoint)\n return results\n }\n\n /**\n * The header block every Canvas request shares. Defining it in one place is\n * what makes \"the string-ID Accept header is on all three response paths\"\n * (BRU-2730 §5) structural rather than three coincidences.\n *\n * Takes the fully-resolved URL because the negotiation decision is per URL:\n * `/api/quiz/v1` is excluded by pathname prefix.\n */\n private canvasHeaders(url: string): Record<string, string> {\n // Credentialless servers never reach the network (GH #442, BRU-3043).\n // The tool layer gates earlier and more legibly; this is the property\n // underneath that convention. Every authenticated Canvas request builds\n // its header block here, and all three call sites evaluate this call\n // *before* invoking `fetch`, so there is no way for a caller the tool\n // wrapper does not cover — a resource read, or an embedder using the\n // standalone client as a library — to issue a tokenless request.\n if (this.token === '') {\n throw new Error(MISSING_CREDENTIALS_TOOL_CALL_ERROR)\n }\n return {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n ...stringIdsAcceptHeader(url),\n }\n }\n\n /**\n * The pagination loops exit with a non-null `nextUrl` only when the page cap\n * was reached while Canvas still advertised another page. Surface that as an\n * error rather than returning a partial list that looks complete.\n */\n private assertNotTruncated(nextUrl: string | null, endpoint: string): void {\n if (!nextUrl) return\n throw new Error(\n `Results are incomplete: ${endpoint} has more pages beyond the configured limit of ${this.maxPaginationPages} pages, so the list was truncated. Narrow the query with filters or raise maxPaginationPages.`,\n )\n }\n\n private parseNextLink(linkHeader: string | null): string | null {\n if (!linkHeader) return null\n const match = linkHeader.match(/<([^>]+)>;\\s*rel=\"next\"/)\n return match?.[1] ?? null\n }\n}\n"],"mappings":"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","names":["body"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/auth/credential-gate.ts
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _typesjs = require('@modelcontextprotocol/sdk/types.js');
|
|
6
|
+
var DOCTOR_HINT = "Run `canvas-lms-mcp doctor` to check your setup.";
|
|
7
|
+
var MISSING_CREDENTIALS_TOOL_CALL_ERROR = `Canvas API token required. This server started without Canvas credentials, so Canvas tool calls are unavailable. Set --token or CANVAS_API_TOKEN and --base-url or CANVAS_BASE_URL, then restart the server. ${DOCTOR_HINT}`;
|
|
8
|
+
var CREDENTIALLESS_STDIO_WARNING = `Warning: Canvas credentials are absent (CANVAS_API_TOKEN / CANVAS_BASE_URL are unset). canvas-lms-mcp started in discovery-only mode: tools, prompts and resources are listed, but every tool call returns a credential error. ${DOCTOR_HINT}`;
|
|
9
|
+
function canvasCredentialGate() {
|
|
10
|
+
return { message: MISSING_CREDENTIALS_TOOL_CALL_ERROR };
|
|
11
|
+
}
|
|
12
|
+
function gatedToolCallResult(gate) {
|
|
13
|
+
return { content: [{ type: "text", text: gate.message }], isError: true };
|
|
14
|
+
}
|
|
15
|
+
var CALL_TOOL_METHOD = _typesjs.CallToolRequestSchema.shape.method.value;
|
|
16
|
+
function installToolCallGateHandler(server, gate) {
|
|
17
|
+
const protocol = server.server;
|
|
18
|
+
if (!protocol) return;
|
|
19
|
+
protocol.removeRequestHandler(CALL_TOOL_METHOD);
|
|
20
|
+
protocol.fallbackRequestHandler = async (request) => {
|
|
21
|
+
if (request.method === CALL_TOOL_METHOD) return gatedToolCallResult(gate);
|
|
22
|
+
throw Object.assign(new Error("Method not found"), { code: _typesjs.ErrorCode.MethodNotFound });
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
exports.DOCTOR_HINT = DOCTOR_HINT; exports.MISSING_CREDENTIALS_TOOL_CALL_ERROR = MISSING_CREDENTIALS_TOOL_CALL_ERROR; exports.CREDENTIALLESS_STDIO_WARNING = CREDENTIALLESS_STDIO_WARNING; exports.canvasCredentialGate = canvasCredentialGate; exports.gatedToolCallResult = gatedToolCallResult; exports.installToolCallGateHandler = installToolCallGateHandler;
|
|
34
|
+
//# sourceMappingURL=chunk-AMZAZNZZ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-AMZAZNZZ.cjs","../src/auth/credential-gate.ts"],"names":[],"mappings":"AAAA;ACaA;AACE;AACA;AAAA,6DAEK;AAGA,IAAM,YAAA,EAAc,kDAAA;AAUpB,IAAM,oCAAA,EACX,CAAA,6MAAA,EAE4B,WAAW,CAAA,CAAA;AAUQ;AAeI;AACG,EAAA;AACxD;AAqB6E;AACH,EAAA;AAC1E;AAM2E;AA8Da;AAI7B,EAAA;AAC1C,EAAA;AAE+B,EAAA;AAC8B,EAAA;AACF,IAAA;AAOa,IAAA;AACvF,EAAA;AACF;ADjJsQ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-AMZAZNZZ.cjs","sourcesContent":[null,"// Credentialless discovery (GH #442, BRU-3043).\n//\n// MCP directories such as mcp.so launch a package with no environment at all\n// and read its `tools/list` to build a listing. Until this module existed the\n// stdio entry point exited inside `parseArgs()` before MCP initialization, so\n// those directories reported \"No tools detected\" and the only server that\n// scored was one whose build config injected placeholder Canvas credentials.\n//\n// The answer is to separate *discovery* from *authority*: start, advertise the\n// full default surface, and refuse every tool call with one actionable message.\n// Nothing here loosens authentication — a gated server holds no usable Canvas\n// credential, so there is nothing for it to authorize.\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport {\n CallToolRequestSchema,\n ErrorCode,\n type ServerResult,\n} from '@modelcontextprotocol/sdk/types.js'\n\n/** Shared by the fatal startup errors and the gated tool-call error. */\nexport const DOCTOR_HINT = 'Run `canvas-lms-mcp doctor` to check your setup.'\n\n/**\n * The error text every tool call returns while the gate is installed.\n *\n * Stable on purpose: a directory, a client and an acceptance test all read it.\n * It names the token first (so it matches the fatal startup error a configured\n * deployer sees), both configuration surfaces for each credential, and the\n * doctor hint.\n */\nexport const MISSING_CREDENTIALS_TOOL_CALL_ERROR =\n 'Canvas API token required. This server started without Canvas credentials, so Canvas tool ' +\n 'calls are unavailable. Set --token or CANVAS_API_TOKEN and --base-url or CANVAS_BASE_URL, ' +\n `then restart the server. ${DOCTOR_HINT}`\n\n/**\n * The single startup notice. One line, stderr only: stdout is the MCP protocol\n * channel on the stdio transport, and one stray byte there breaks the session\n * for every client.\n */\nexport const CREDENTIALLESS_STDIO_WARNING =\n 'Warning: Canvas credentials are absent (CANVAS_API_TOKEN / CANVAS_BASE_URL are unset). ' +\n 'canvas-lms-mcp started in discovery-only mode: tools, prompts and resources are listed, but ' +\n `every tool call returns a credential error. ${DOCTOR_HINT}`\n\n/**\n * Installed on a server whose tool calls must all be refused before they run.\n *\n * A value rather than a boolean flag so the refusal message travels with the\n * decision: the tool layer never has to know *why* it is gated, and there is no\n * second place where the message could drift. Absent = no gate, which is every\n * configured deployment byte-for-byte.\n */\nexport interface ToolCallGate {\n readonly message: string\n}\n\n/** The gate for a server started with no Canvas credentials. */\nexport function canvasCredentialGate(): ToolCallGate {\n return { message: MISSING_CREDENTIALS_TOOL_CALL_ERROR }\n}\n\n/** The refusal, in `CallToolResult` shape. */\n// A `type`, not an `interface`: only a type alias gets the implicit index\n// signature that makes it assignable to the SDK's `CallToolResult`, whose\n// schema is loose and so carries `[x: string]: unknown`.\nexport type GatedToolCallResult = {\n content: { type: 'text'; text: string }[]\n isError: true\n}\n\n/**\n * The one refusal value. Both gate layers return *this*, so the message, the\n * `isError` flag and the absence of `structuredContent` cannot drift apart\n * between a well-formed call and a malformed one.\n *\n * `isError: true` rather than a JSON-RPC error: a tool-level error is what the\n * MCP spec reserves for \"the tool ran and failed\", it reaches the model as\n * readable text in every client, and it carries no structured payload that\n * could satisfy a tool's success schema.\n */\nexport function gatedToolCallResult(gate: ToolCallGate): GatedToolCallResult {\n return { content: [{ type: 'text', text: gate.message }], isError: true }\n}\n\n/**\n * The `tools/call` method name, taken from the SDK's own schema rather than\n * retyped, so a gate and a dispatcher can never name different methods.\n */\nexport const CALL_TOOL_METHOD: string = CallToolRequestSchema.shape.method.value\n\n/**\n * Make **every** `tools/call` request answer with the gate refusal.\n *\n * This is the wire-level half of the gate. The other half is the early return\n * in `buildHandler` (`src/tools/index.ts`), which covers a tool invoked through\n * a path that never goes over the protocol — an embedder calling\n * `tool.handler` directly, or a server whose protocol object is a test double.\n * Neither layer shadows the other: this one answers requests that never reach a\n * registered handler, that one answers invocations that never reach dispatch.\n *\n * ## Why the registration path cannot be used\n *\n * A `tools/call` request is validated **twice** before any registered handler\n * runs, and both rejections happen inside the SDK:\n *\n * 1. `Protocol.setRequestHandler` wraps the handler in\n * `requestSchema.safeParse(request)` and rethrows the `ZodError`, so with\n * the SDK's `CallToolRequestSchema` a request with no `params`, a\n * non-string `name` or a non-object `arguments` is answered with a raw Zod\n * dump under JSON-RPC `-32603`.\n * 2. `Server.setRequestHandler` **overrides** that method and, for\n * `tools/call` specifically, wraps *any* handler — whatever schema it was\n * registered under — in an unconditional\n * `safeParse(CallToolRequestSchema, request)` that throws\n * `McpError(InvalidParams, 'Invalid tools/call request: …')`.\n *\n * Layer 2 means re-registering under a permissive schema does not work: it was\n * measured, and it only changed `-32603` to `-32602`. There is no option to\n * disable it (`ServerOptions` has no such flag), and the SDK exposes no way to\n * read a registered handler back.\n *\n * ## The seam used instead\n *\n * `Protocol._onrequest` resolves a handler as\n * `this._requestHandlers.get(method) ?? this.fallbackRequestHandler` and calls\n * the fallback with the **raw** request — no schema parse, and outside\n * `Server`'s `tools/call` wrapper, which only decorates handlers passed to\n * `setRequestHandler`. Both `removeRequestHandler` and\n * `fallbackRequestHandler` are public, documented API, so this patches\n * nothing and reaches into no private internals.\n *\n * Removing the entry rather than shadowing it is the stronger arrangement: the\n * dispatch table then holds *no* `tools/call` handler at all, so there is no\n * path from a request to a tool handler to a Canvas request. The cost is that\n * installing a fallback replaces `Protocol`'s built-in \"Method not found\"\n * branch for every other unhandled method, so this reproduces it exactly.\n *\n * This loosens no validation that protects anything: a gated server registers\n * no usable Canvas credential and can run no tool, so there is no execution\n * for argument validation to guard. A configured server is untouched — the\n * gate object does not exist in its config.\n *\n * ## Ordering\n *\n * Called from `registerAllTools` after the registration loop, because\n * `McpServer.setToolRequestHandlers` runs on the first `registerTool` and is\n * what installs the handler this removes. Its `_toolHandlersInitialized` latch\n * means later `registerTool` calls do not reinstall it, so an embedder may\n * still add tools afterwards without reopening the hole.\n */\nexport function installToolCallGateHandler(server: McpServer, gate: ToolCallGate): void {\n // Handler-capture test doubles pass a bare `{ registerTool }` cast to\n // `McpServer` and carry no protocol object; they never answer a request, so\n // `buildHandler`'s early return is the whole gate for them.\n const protocol: McpServer['server'] | undefined = server.server\n if (!protocol) return\n\n protocol.removeRequestHandler(CALL_TOOL_METHOD)\n protocol.fallbackRequestHandler = async (request): Promise<ServerResult> => {\n if (request.method === CALL_TOOL_METHOD) return gatedToolCallResult(gate)\n // Byte-for-byte the response from the branch in `Protocol._onrequest` that\n // installing a fallback at all has just displaced. Deliberately not an\n // `McpError`: its constructor rewrites `message` to\n // `MCP error -32601: Method not found`, which a client would see as a\n // changed string on a path this feature must not touch. `_onrequest`\n // reads only `error.code` and `error.message`.\n throw Object.assign(new Error('Method not found'), { code: ErrorCode.MethodNotFound })\n }\n}\n"]}
|
|
@@ -25,11 +25,15 @@ import {
|
|
|
25
25
|
compareCanvasIds,
|
|
26
26
|
mapWithConcurrency,
|
|
27
27
|
normalizeCanvasIdInput
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-FRCOQ7JD.js";
|
|
29
29
|
import {
|
|
30
30
|
CanvasApiError,
|
|
31
31
|
version
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-5EZSBL5J.js";
|
|
33
|
+
import {
|
|
34
|
+
gatedToolCallResult,
|
|
35
|
+
installToolCallGateHandler
|
|
36
|
+
} from "./chunk-XLORAICV.js";
|
|
33
37
|
|
|
34
38
|
// src/server.ts
|
|
35
39
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -8687,8 +8691,11 @@ function getAllTools(canvas, pseudonymizer, role, features) {
|
|
|
8687
8691
|
if (!role) return all;
|
|
8688
8692
|
return all.filter((tool) => isVisibleForRole(tool, role));
|
|
8689
8693
|
}
|
|
8690
|
-
function buildHandler(tool, pseudonymizer) {
|
|
8694
|
+
function buildHandler(tool, pseudonymizer, toolCallGate) {
|
|
8691
8695
|
return async (params) => {
|
|
8696
|
+
if (toolCallGate) {
|
|
8697
|
+
return gatedToolCallResult(toolCallGate);
|
|
8698
|
+
}
|
|
8692
8699
|
try {
|
|
8693
8700
|
const fencingEnabled = isProvenanceFencingEnabled();
|
|
8694
8701
|
if (fencingEnabled && tool.annotations.destructiveHint === true) {
|
|
@@ -8739,11 +8746,11 @@ function buildHandler(tool, pseudonymizer) {
|
|
|
8739
8746
|
}
|
|
8740
8747
|
};
|
|
8741
8748
|
}
|
|
8742
|
-
function registerAllTools(server, canvas, pseudonymizer, role, features) {
|
|
8749
|
+
function registerAllTools(server, canvas, pseudonymizer, role, features, toolCallGate) {
|
|
8743
8750
|
installSchemaDialectCompat(server);
|
|
8744
8751
|
const tools = getAllTools(canvas, pseudonymizer, role, features);
|
|
8745
8752
|
for (const tool of tools) {
|
|
8746
|
-
const handler = buildHandler(tool, pseudonymizer);
|
|
8753
|
+
const handler = buildHandler(tool, pseudonymizer, toolCallGate);
|
|
8747
8754
|
if (tool.ui) {
|
|
8748
8755
|
registerAppTool(
|
|
8749
8756
|
server,
|
|
@@ -8772,6 +8779,9 @@ function registerAllTools(server, canvas, pseudonymizer, role, features) {
|
|
|
8772
8779
|
);
|
|
8773
8780
|
}
|
|
8774
8781
|
}
|
|
8782
|
+
if (toolCallGate && tools.length > 0) {
|
|
8783
|
+
installToolCallGateHandler(server, toolCallGate);
|
|
8784
|
+
}
|
|
8775
8785
|
}
|
|
8776
8786
|
|
|
8777
8787
|
// src/resources/syllabus.ts
|
|
@@ -10296,7 +10306,7 @@ function createCanvasMCPServer(config) {
|
|
|
10296
10306
|
),
|
|
10297
10307
|
...config.writeTools ? { writeTools: config.writeTools } : {}
|
|
10298
10308
|
};
|
|
10299
|
-
registerAllTools(server, canvas, pseudonymizer, config.role, features);
|
|
10309
|
+
registerAllTools(server, canvas, pseudonymizer, config.role, features, config.toolCallGate);
|
|
10300
10310
|
registerAllResources(server, canvas);
|
|
10301
10311
|
registerAllPrompts(server, config.role);
|
|
10302
10312
|
return { server, canvas, pseudonymizer };
|
|
@@ -10307,4 +10317,4 @@ export {
|
|
|
10307
10317
|
createSharedPseudonymizer,
|
|
10308
10318
|
createCanvasMCPServer
|
|
10309
10319
|
};
|
|
10310
|
-
//# sourceMappingURL=chunk-
|
|
10320
|
+
//# sourceMappingURL=chunk-EXBMBM75.js.map
|