canvas-lms-mcp 1.28.0 → 1.29.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/README.md +47 -3
- package/dist/canvas/index.cjs +3 -3
- package/dist/canvas/index.js +2 -2
- package/dist/{chunk-FF7TOBVJ.js → chunk-36BW4FIO.js} +2 -2
- package/dist/{chunk-EY7MUK6A.js → chunk-55WIXZWE.js} +2 -2
- package/dist/{chunk-EY7MUK6A.js.map → chunk-55WIXZWE.js.map} +1 -1
- package/dist/chunk-67YTMYAF.cjs +57 -0
- package/dist/chunk-67YTMYAF.cjs.map +1 -0
- package/dist/{chunk-BJFRBBXI.cjs → chunk-6VAQBQ2V.cjs} +57 -47
- package/dist/chunk-6VAQBQ2V.cjs.map +1 -0
- package/dist/{chunk-MZGRCYAP.js → chunk-EYELCMRH.js} +17 -7
- package/dist/chunk-EYELCMRH.js.map +1 -0
- package/dist/{chunk-4BXEVAHA.cjs → chunk-FFK34HYS.cjs} +33 -6
- package/dist/chunk-FFK34HYS.cjs.map +1 -0
- package/dist/{chunk-WVQPWTIE.js → chunk-HIX2GVT6.js} +32 -5
- package/dist/chunk-HIX2GVT6.js.map +1 -0
- package/dist/chunk-NIFJL3F7.js +57 -0
- package/dist/chunk-NIFJL3F7.js.map +1 -0
- package/dist/{chunk-V7NUQZO2.cjs → chunk-Y7342FIO.cjs} +2 -2
- package/dist/{chunk-V7NUQZO2.cjs.map → chunk-Y7342FIO.cjs.map} +1 -1
- package/dist/{chunk-FXFEJU2S.cjs → chunk-ZTZ5OH2B.cjs} +4 -4
- package/dist/{chunk-FXFEJU2S.cjs.map → chunk-ZTZ5OH2B.cjs.map} +1 -1
- package/dist/cli.cjs +3 -3
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/discovery/catalog.d.ts +0 -1
- package/dist/discovery/catalog.d.ts.map +1 -1
- package/dist/discovery/manifests.d.ts +0 -1
- package/dist/discovery/manifests.d.ts.map +1 -1
- package/dist/http.cjs +15 -10
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +12 -7
- package/dist/http.js.map +1 -1
- package/dist/init.cjs +3 -3
- package/dist/init.js +1 -1
- package/dist/server.cjs +5 -5
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +4 -4
- package/dist/stdio.cjs +9 -8
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.js +7 -6
- package/dist/stdio.js.map +1 -1
- package/dist/tools/catalog.d.ts +12 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/destructive-policy.d.ts +77 -0
- package/dist/tools/destructive-policy.d.ts.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/titles.d.ts.map +1 -1
- package/dist/tools/types.d.ts +10 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4BXEVAHA.cjs.map +0 -1
- package/dist/chunk-BJFRBBXI.cjs.map +0 -1
- package/dist/chunk-MZGRCYAP.js.map +0 -1
- package/dist/chunk-QCEGMTXW.cjs +0 -11
- package/dist/chunk-QCEGMTXW.cjs.map +0 -1
- package/dist/chunk-WVQPWTIE.js.map +0 -1
- package/dist/chunk-XVZ3T4Z4.js +0 -11
- package/dist/chunk-XVZ3T4Z4.js.map +0 -1
- /package/dist/{chunk-FF7TOBVJ.js.map → chunk-36BW4FIO.js.map} +0 -0
package/README.md
CHANGED
|
@@ -283,6 +283,7 @@ const courses = await canvas.courses.list()
|
|
|
283
283
|
| `--port` | -- | `3001` | HTTP server port |
|
|
284
284
|
| `--allowed-origin` | `CANVAS_ALLOWED_ORIGIN` | `http://localhost:3000` | CORS allowed origin |
|
|
285
285
|
| `--role` | `CANVAS_ROLE` | (all tools) | Filter tools by Canvas role: `student`, `teacher`, or `admin` (see [Role-based tool filtering](#role-based-tool-filtering)) |
|
|
286
|
+
| `--destructive-tools=<mode>` | `CANVAS_DESTRUCTIVE_TOOLS` | `allow` | `allow` or `block`. `block` unregisters the seven irreversible delete tools (see [Destructive tool policy](#destructive-tool-policy)) |
|
|
286
287
|
|
|
287
288
|
## Environment Variables
|
|
288
289
|
|
|
@@ -298,6 +299,49 @@ const courses = await canvas.courses.list()
|
|
|
298
299
|
| `CANVAS_PSEUDONYM_DIR` | No | Absolute path that overrides the default pseudonym map directory |
|
|
299
300
|
| `CANVAS_PSEUDONYM_AUDIT_LOG` | No | Path to an append-only file that mirrors `resolve_pseudonym` audit lines (stderr is always written) |
|
|
300
301
|
| `CANVAS_PROVENANCE_FENCING` | No | **On by default.** Set to exactly `false` to disable [provenance fencing](#provenance-fencing-untrusted-canvas-content) |
|
|
302
|
+
| `CANVAS_DESTRUCTIVE_TOOLS` | No | `allow` (default) or `block`. Set to exactly `block` to unregister the seven irreversible delete tools (see [Destructive tool policy](#destructive-tool-policy)) |
|
|
303
|
+
|
|
304
|
+
## Destructive tool policy
|
|
305
|
+
|
|
306
|
+
Canvas has no undo. This server cannot restore anything it deletes -- every recovery
|
|
307
|
+
story for a mistaken delete is something you do outside this tooling, in Canvas or
|
|
308
|
+
with your institution's admin. `CANVAS_DESTRUCTIVE_TOOLS=block` removes the seven
|
|
309
|
+
irreversible delete tools from the server entirely, so no amount of model confusion
|
|
310
|
+
or [prompt injection](#provenance-fencing-untrusted-canvas-content) can reach them.
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
CANVAS_DESTRUCTIVE_TOOLS=block canvas-lms-mcp --base-url https://school.instructure.com
|
|
314
|
+
# or
|
|
315
|
+
canvas-lms-mcp --destructive-tools=block --base-url https://school.instructure.com
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
| Mode | Behaviour |
|
|
319
|
+
|------|-----------|
|
|
320
|
+
| `allow` | **Default.** Every tool is registered -- unchanged from previous releases. |
|
|
321
|
+
| `block` | The seven tools below are not registered at all. They are absent from `tools/list`, and a call naming one is refused by the MCP protocol layer before any Canvas request is made. |
|
|
322
|
+
|
|
323
|
+
**Blocked by `block`:**
|
|
324
|
+
|
|
325
|
+
| Tool | What is lost |
|
|
326
|
+
|------|--------------|
|
|
327
|
+
| `delete_assignment` | The assignment **plus its submissions and gradebook column** |
|
|
328
|
+
| `delete_new_quiz` | The quiz, all its items, and all student results |
|
|
329
|
+
| `delete_new_quiz_item` | One question and its responses; re-authoring is manual |
|
|
330
|
+
| `delete_discussion` | The whole reply thread, including student-authored posts |
|
|
331
|
+
| `delete_page` | Page body and revision history (keyed by URL slug, not a numeric ID) |
|
|
332
|
+
| `delete_file` | A file, addressed by a **global** ID with no course scoping in the call |
|
|
333
|
+
| `delete_appointment_group` | Every reservation -- and it **emails every signed-up student** |
|
|
334
|
+
|
|
335
|
+
**Not blocked:** `delete_peer_review`. It is the only delete this server can itself
|
|
336
|
+
undo (`create_peer_review` recreates the row) and it destroys no authored content.
|
|
337
|
+
|
|
338
|
+
Notes:
|
|
339
|
+
|
|
340
|
+
- **Invalid values stop startup.** The value is matched byte-exactly: `Block`, `BLOCK`, `block ` and an empty value are all errors, not a silent fall-back to `allow`. A kill switch that fails open on a typo is worse than none.
|
|
341
|
+
- **The flag beats the environment outright.** When `--destructive-tools` is present, `CANVAS_DESTRUCTIVE_TOOLS` is not read or validated at all -- so a host that exports a typo'd value cannot stop you overriding it on the command line. Precedence is last-writer-wins, not strictest-wins: `--destructive-tools=allow` really does override `CANVAS_DESTRUCTIVE_TOOLS=block`.
|
|
342
|
+
- **`confirm` is reserved but not implemented.** Setting it is a startup error naming it as such, so it can never be mistaken for protection you do not have.
|
|
343
|
+
- **Server-side only.** Unlike `CANVAS_ROLE`, there is no request header for this in HTTP mode -- a client that could pick the mode could switch the gate off.
|
|
344
|
+
- **This is a real boundary, not a UX filter.** `CANVAS_ROLE` hides tools from a listing; `block` means the handler is never registered.
|
|
301
345
|
|
|
302
346
|
## Provenance fencing (untrusted Canvas content)
|
|
303
347
|
|
|
@@ -402,9 +446,9 @@ Three roles, plus the default of "unset = every tool":
|
|
|
402
446
|
| `CANVAS_ROLE` | Tools exposed | Typical use |
|
|
403
447
|
|---------------|---------------|-------------|
|
|
404
448
|
| _(unset)_ | all (~165) | default; backwards-compatible |
|
|
405
|
-
| `student` | ~
|
|
406
|
-
| `teacher` | ~
|
|
407
|
-
| `admin` | ~
|
|
449
|
+
| `student` | ~58 | a student's own courses, grades, submissions, and read-only course content |
|
|
450
|
+
| `teacher` | ~145 | grading, roster, content authoring, analytics |
|
|
451
|
+
| `admin` | ~157 | everything `teacher` sees plus account-level tools (`enroll_user`, `list_account_users`, …) |
|
|
408
452
|
|
|
409
453
|
Notes:
|
|
410
454
|
|
package/dist/canvas/index.cjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkZTZ5OH2Bcjs = require('../chunk-ZTZ5OH2B.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var _chunkY7342FIOcjs = require('../chunk-Y7342FIO.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
exports.CanvasApiError =
|
|
16
|
+
exports.CanvasApiError = _chunkY7342FIOcjs.CanvasApiError; exports.CanvasClient = _chunkZTZ5OH2Bcjs.CanvasClient; exports.CanvasHttpClient = _chunkY7342FIOcjs.CanvasHttpClient; exports.appendCanvasQuery = _chunkY7342FIOcjs.appendCanvasQuery; exports.toCanvasQuery = _chunkY7342FIOcjs.toCanvasQuery;
|
|
17
17
|
//# sourceMappingURL=index.cjs.map
|
package/dist/canvas/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasClient
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-36BW4FIO.js";
|
|
4
4
|
import {
|
|
5
5
|
CanvasApiError,
|
|
6
6
|
CanvasHttpClient,
|
|
7
7
|
appendCanvasQuery,
|
|
8
8
|
toCanvasQuery
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-55WIXZWE.js";
|
|
10
10
|
export {
|
|
11
11
|
CanvasApiError,
|
|
12
12
|
CanvasClient,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasApiError,
|
|
3
3
|
CanvasHttpClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-55WIXZWE.js";
|
|
5
5
|
|
|
6
6
|
// src/canvas/courses.ts
|
|
7
7
|
var DEFAULT_LIST_INCLUDE = ["term"];
|
|
@@ -1771,4 +1771,4 @@ export {
|
|
|
1771
1771
|
OUTCOME_EXCLUDE_OPTIONS,
|
|
1772
1772
|
CanvasClient
|
|
1773
1773
|
};
|
|
1774
|
-
//# sourceMappingURL=chunk-
|
|
1774
|
+
//# sourceMappingURL=chunk-36BW4FIO.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "1.
|
|
2
|
+
var version = "1.29.0";
|
|
3
3
|
|
|
4
4
|
// src/canvas/query.ts
|
|
5
5
|
function appendCanvasQuery(target, params) {
|
|
@@ -163,4 +163,4 @@ export {
|
|
|
163
163
|
CanvasApiError,
|
|
164
164
|
CanvasHttpClient
|
|
165
165
|
};
|
|
166
|
-
//# sourceMappingURL=chunk-
|
|
166
|
+
//# sourceMappingURL=chunk-55WIXZWE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../package.json","../src/canvas/query.ts","../src/canvas/client.ts"],"sourcesContent":["{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"1.28.0\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 165 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 ],\n \"scripts\": {\n \"dev\": \"tsup --watch\",\n \"build\": \"tsup && tsc -p tsconfig.build.json\",\n \"generate:manifests\": \"tsx scripts/generate-manifests.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.30.0\",\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 \"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 }\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","import { version } from '../../package.json'\nimport { appendCanvasQuery, type CanvasQueryParams } from './query'\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> = {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n }\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 response.json() as Promise<T>\n }\n\n async paginate<T>(endpoint: string, params?: CanvasQueryParams): 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 const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\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 const data = (await response.json()) as T[]\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n return 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 const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\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 const body = (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 return results\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/client.ts"],"sourcesContent":["{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"1.29.0\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 165 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 ],\n \"scripts\": {\n \"dev\": \"tsup --watch\",\n \"build\": \"tsup && tsc -p tsconfig.build.json\",\n \"generate:manifests\": \"tsx scripts/generate-manifests.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.30.0\",\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 \"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 }\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","import { version } from '../../package.json'\nimport { appendCanvasQuery, type CanvasQueryParams } from './query'\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> = {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\n }\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 response.json() as Promise<T>\n }\n\n async paginate<T>(endpoint: string, params?: CanvasQueryParams): 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 const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\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 const data = (await response.json()) as T[]\n results.push(...data)\n pages++\n\n nextUrl = this.parseNextLink(response.headers.get('Link'))\n }\n\n return 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 const response = await fetch(nextUrl, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': USER_AGENT,\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 const body = (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 return results\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,57 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/env.ts
|
|
2
|
+
var TRUTHY_ENV_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
|
|
3
|
+
function isEnvTruthy(value) {
|
|
4
|
+
if (!value) return false;
|
|
5
|
+
return TRUTHY_ENV_VALUES.has(value.trim().toLowerCase());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/tools/destructive-policy.ts
|
|
9
|
+
var DEFAULT_DESTRUCTIVE_TOOLS_MODE = "allow";
|
|
10
|
+
var GATED_DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set([
|
|
11
|
+
// Removes the assignment *and* its submissions and gradebook column.
|
|
12
|
+
"delete_assignment",
|
|
13
|
+
// Quizzes.Next: destroys all items and student results.
|
|
14
|
+
"delete_new_quiz",
|
|
15
|
+
// One question plus its responses; re-authoring is manual.
|
|
16
|
+
"delete_new_quiz_item",
|
|
17
|
+
// Destroys the whole reply thread — student-authored content.
|
|
18
|
+
"delete_discussion",
|
|
19
|
+
// Page body and revision history; keyed by URL slug, not a numeric ID.
|
|
20
|
+
"delete_page",
|
|
21
|
+
// Global file ID with no course scoping in the call.
|
|
22
|
+
"delete_file",
|
|
23
|
+
// Cancels every reservation and emails every signed-up student.
|
|
24
|
+
"delete_appointment_group"
|
|
25
|
+
]);
|
|
26
|
+
var RESERVED_MODES = {
|
|
27
|
+
confirm: "the preview/confirmation-token flow is not implemented yet (BRU-2390 Phase 2). Use `block` for a hard guarantee, or `allow` for the current behaviour."
|
|
28
|
+
};
|
|
29
|
+
var SHIPPED_MODES = ["allow", "block"];
|
|
30
|
+
function parseDestructiveToolsMode(raw, source) {
|
|
31
|
+
if (raw === void 0) return DEFAULT_DESTRUCTIVE_TOOLS_MODE;
|
|
32
|
+
if (raw === "allow" || raw === "block") return raw;
|
|
33
|
+
const reserved = Object.prototype.hasOwnProperty.call(RESERVED_MODES, raw) ? RESERVED_MODES[raw] : void 0;
|
|
34
|
+
if (reserved !== void 0) {
|
|
35
|
+
throw new Error(`${source}=${raw} is reserved but not implemented: ${reserved}`);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Invalid ${source} value ${JSON.stringify(raw)}. Expected exactly one of: ${SHIPPED_MODES.join(", ")} (lowercase, no surrounding whitespace). Refusing to start rather than fall back to "${DEFAULT_DESTRUCTIVE_TOOLS_MODE}".`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function resolveDestructiveToolsMode(configured, envValue) {
|
|
42
|
+
if (configured !== void 0) return parseDestructiveToolsMode(configured, "destructiveTools");
|
|
43
|
+
return parseDestructiveToolsMode(envValue, "CANVAS_DESTRUCTIVE_TOOLS");
|
|
44
|
+
}
|
|
45
|
+
function applyDestructiveToolsPolicy(tools, mode) {
|
|
46
|
+
if (mode === "allow") return [...tools];
|
|
47
|
+
return tools.filter((tool) => !GATED_DESTRUCTIVE_TOOLS.has(tool.name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
exports.isEnvTruthy = isEnvTruthy; exports.DEFAULT_DESTRUCTIVE_TOOLS_MODE = DEFAULT_DESTRUCTIVE_TOOLS_MODE; exports.parseDestructiveToolsMode = parseDestructiveToolsMode; exports.resolveDestructiveToolsMode = resolveDestructiveToolsMode; exports.applyDestructiveToolsPolicy = applyDestructiveToolsPolicy;
|
|
57
|
+
//# sourceMappingURL=chunk-67YTMYAF.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-67YTMYAF.cjs","../src/env.ts","../src/tools/destructive-policy.ts"],"names":[],"mappings":"AAAA;ACAO,IAAM,kBAAA,kBAAoB,IAAI,GAAA,CAAI,CAAC,MAAA,EAAQ,GAAA,EAAK,KAAA,EAAO,IAAI,CAAC,CAAA;AAE5D,SAAS,WAAA,CAAY,KAAA,EAAoC;AAC9D,EAAA,GAAA,CAAI,CAAC,KAAA,EAAO,OAAO,KAAA;AACnB,EAAA,OAAO,iBAAA,CAAkB,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,CAAA,CAAE,WAAA,CAAY,CAAC,CAAA;AACzD;ADCA;AACA;AEkBO,IAAM,+BAAA,EAAuD,OAAA;AAU7D,IAAM,wBAAA,kBAA+C,IAAI,GAAA,CAAI;AAAA;AAAA,EAElE,mBAAA;AAAA;AAAA,EAEA,iBAAA;AAAA;AAAA,EAEA,sBAAA;AAAA;AAAA,EAEA,mBAAA;AAAA;AAAA,EAEA,aAAA;AAAA;AAAA,EAEA,aAAA;AAAA;AAAA,EAEA;AACF,CAAC,CAAA;AAsBD,IAAM,eAAA,EAAmD;AAAA,EACvD,OAAA,EACE;AACJ,CAAA;AAEA,IAAM,cAAA,EAAiD,CAAC,OAAA,EAAS,OAAO,CAAA;AAmBjE,SAAS,yBAAA,CACd,GAAA,EACA,MAAA,EACsB;AACtB,EAAA,GAAA,CAAI,IAAA,IAAQ,KAAA,CAAA,EAAW,OAAO,8BAAA;AAC9B,EAAA,GAAA,CAAI,IAAA,IAAQ,QAAA,GAAW,IAAA,IAAQ,OAAA,EAAS,OAAO,GAAA;AAE/C,EAAA,MAAM,SAAA,EAAW,MAAA,CAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,cAAA,EAAgB,GAAG,EAAA,EACrE,cAAA,CAAe,GAAG,EAAA,EAClB,KAAA,CAAA;AACJ,EAAA,GAAA,CAAI,SAAA,IAAa,KAAA,CAAA,EAAW;AAC1B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAA;AAClB,EAAA;AAEU,EAAA;AACS,IAAA;AAGnB,EAAA;AACF;AAWgB;AAIK,EAAA;AACZ,EAAA;AACT;AAWgB;AAID,EAAA;AACA,EAAA;AACf;AFrGqB;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-67YTMYAF.cjs","sourcesContent":[null,"export const TRUTHY_ENV_VALUES = new Set(['true', '1', 'yes', 'on'])\n\nexport function isEnvTruthy(value: string | undefined): boolean {\n if (!value) return false\n return TRUTHY_ENV_VALUES.has(value.trim().toLowerCase())\n}\n","// Deployer policy gate for irreversible Canvas deletes.\n//\n// BRU-2444 — Phase 1 of the BRU-2390 design\n// (`docs/superpowers/specs/2026-08-31-bru-2390-destructive-confirmation.md`).\n//\n// This is the only control in that design that offers a *hard* guarantee. Under\n// `block` the seven irreversible delete tools are never registered, so the MCP\n// dispatch table refuses the call before any handler exists to run. Everything\n// else the design proposes — preview/confirm tokens, elicitation — is a nudge a\n// confused or prompt-injected model can satisfy on its own within a single turn\n// (design §1.1). The gate does not depend on model behaviour at all.\n\n/**\n * Shipped modes. `confirm` is named by the design (§7) but its token machinery\n * is Phase 2+; it is deliberately absent from this union and rejected by the\n * parser rather than aliased onto a neighbour.\n */\nexport type DestructiveToolsMode = 'allow' | 'block'\n\n/**\n * `allow` — today's behaviour, byte-for-byte — is the v1 default. Defaulting to\n * anything stricter would make a previously-working `delete_assignment` call\n * start failing, which is a breaking change and therefore 2.0.0 material\n * (design §7).\n */\nexport const DEFAULT_DESTRUCTIVE_TOOLS_MODE: DestructiveToolsMode = 'allow'\n\n/**\n * The seven irreversible deletes gated by `block` (design §3).\n *\n * The boundary is *irreversible destruction of existing state*, not the\n * `destructiveHint` annotation (48 tools carry it) and not the `delete_` name\n * prefix on its own — `delete_peer_review` carries the prefix and is\n * deliberately excluded below.\n */\nexport const GATED_DESTRUCTIVE_TOOLS: ReadonlySet<string> = new Set([\n // Removes the assignment *and* its submissions and gradebook column.\n 'delete_assignment',\n // Quizzes.Next: destroys all items and student results.\n 'delete_new_quiz',\n // One question plus its responses; re-authoring is manual.\n 'delete_new_quiz_item',\n // Destroys the whole reply thread — student-authored content.\n 'delete_discussion',\n // Page body and revision history; keyed by URL slug, not a numeric ID.\n 'delete_page',\n // Global file ID with no course scoping in the call.\n 'delete_file',\n // Cancels every reservation and emails every signed-up student.\n 'delete_appointment_group',\n])\n\n/**\n * Delete tools deliberately left ungated, with the reason recorded here rather\n * than in a commit message.\n *\n * `delete_peer_review` is the only delete in the set whose effect this server\n * can itself undo — `create_peer_review` recreates the row and no authored\n * content is destroyed — and gating it would cost a round-trip on peer-review\n * reshuffling, which is inherently many-call work (design §3).\n *\n * The coverage guard in `tests/tools/destructive-gate.test.ts` fails if a\n * future `delete_*` tool lands in neither this set nor\n * `GATED_DESTRUCTIVE_TOOLS`, so tool #9 cannot arrive silently ungated.\n */\nexport const UNGATED_DELETE_TOOLS: ReadonlySet<string> = new Set(['delete_peer_review'])\n\n/**\n * Modes the design names but this phase does not implement. Accepting one of\n * these as an alias for `allow` would silently permit exactly the deletes the\n * deployer asked to be confirmed, so they get their own loud error.\n */\nconst RESERVED_MODES: Readonly<Record<string, string>> = {\n confirm:\n 'the preview/confirmation-token flow is not implemented yet (BRU-2390 Phase 2). Use `block` for a hard guarantee, or `allow` for the current behaviour.',\n}\n\nconst SHIPPED_MODES: readonly DestructiveToolsMode[] = ['allow', 'block']\n\n/**\n * Parse a raw mode string byte-exactly.\n *\n * No trimming, no case-folding, no truthiness — this is a kill switch, and\n * every normalisation step widens the set of strings that *accidentally* land\n * on a mode the deployer did not choose. The accident that matters is a\n * near-miss (`Block`, `block `, `blocked`) silently registering the deletes, so\n * anything unrecognised throws at startup instead of falling back to `allow`.\n * The offending value is echoed so a typo is self-diagnosing.\n *\n * @param raw The configured value; `undefined` means unset, which is the\n * documented default. An empty string is *not* unset — it is an\n * explicit, unrecognised value and is rejected.\n * @param source Human-readable origin (`CANVAS_DESTRUCTIVE_TOOLS`,\n * `--destructive-tools`, `destructiveTools`) so an env failure\n * and a CLI failure are distinguishable in the error text.\n */\nexport function parseDestructiveToolsMode(\n raw: string | undefined,\n source: string,\n): DestructiveToolsMode {\n if (raw === undefined) return DEFAULT_DESTRUCTIVE_TOOLS_MODE\n if (raw === 'allow' || raw === 'block') return raw\n\n const reserved = Object.prototype.hasOwnProperty.call(RESERVED_MODES, raw)\n ? RESERVED_MODES[raw]\n : undefined\n if (reserved !== undefined) {\n throw new Error(`${source}=${raw} is reserved but not implemented: ${reserved}`)\n }\n\n throw new Error(\n `Invalid ${source} value ${JSON.stringify(raw)}. ` +\n `Expected exactly one of: ${SHIPPED_MODES.join(', ')} (lowercase, no surrounding whitespace). ` +\n `Refusing to start rather than fall back to \"${DEFAULT_DESTRUCTIVE_TOOLS_MODE}\".`,\n )\n}\n\n/**\n * Resolve the effective mode from an explicitly configured value and the\n * environment, validating whichever one wins.\n *\n * The configured value is validated too. TypeScript stops `'Block'` at a\n * typed call site, but a plain-JS embedder is the real risk: an unvalidated\n * passthrough would be treated as \"not `allow`\"… or, worse, as \"not `block`\" by\n * a future comparison, and fail open.\n */\nexport function resolveDestructiveToolsMode(\n configured: string | undefined,\n envValue: string | undefined,\n): DestructiveToolsMode {\n if (configured !== undefined) return parseDestructiveToolsMode(configured, 'destructiveTools')\n return parseDestructiveToolsMode(envValue, 'CANVAS_DESTRUCTIVE_TOOLS')\n}\n\n/**\n * Remove the gated tools unless the mode is exactly `allow`.\n *\n * Written as an allow-list check rather than `mode === 'block'` so that any\n * value reaching here without passing the parser — a cast, a corrupted config\n * object, a future mode added to the union but not to this filter — blocks\n * rather than permits. Failing closed is the only safe direction for a control\n * whose failure mode is an unrecoverable delete.\n */\nexport function applyDestructiveToolsPolicy<T extends { name: string }>(\n tools: readonly T[],\n mode: DestructiveToolsMode,\n): T[] {\n if (mode === 'allow') return [...tools]\n return tools.filter((tool) => !GATED_DESTRUCTIVE_TOOLS.has(tool.name))\n}\n"]}
|