canvas-lms-mcp 2.5.2 → 2.7.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 +15 -13
- package/dist/canvas/groups.d.ts +26 -1
- package/dist/canvas/groups.d.ts.map +1 -1
- package/dist/canvas/index.cjs +3 -3
- package/dist/canvas/index.js +2 -2
- package/dist/canvas/modules.d.ts +6 -0
- package/dist/canvas/modules.d.ts.map +1 -1
- package/dist/canvas/types.d.ts +20 -0
- package/dist/canvas/types.d.ts.map +1 -1
- package/dist/{chunk-66TL3XQU.js → chunk-3ED6EUGQ.js} +2 -2
- package/dist/{chunk-66TL3XQU.js.map → chunk-3ED6EUGQ.js.map} +1 -1
- package/dist/{chunk-DG22XTGE.cjs → chunk-55HSTOYO.cjs} +3 -3
- package/dist/{chunk-DG22XTGE.cjs.map → chunk-55HSTOYO.cjs.map} +1 -1
- package/dist/{chunk-2EY3U7IR.js → chunk-CDOKT4BS.js} +50 -2
- package/dist/chunk-CDOKT4BS.js.map +1 -0
- package/dist/{chunk-IQU4YMPO.cjs → chunk-D7UCPSSH.cjs} +488 -339
- package/dist/chunk-D7UCPSSH.cjs.map +1 -0
- package/dist/{chunk-EA5YGN5Z.cjs → chunk-DXVGALW5.cjs} +8 -2
- package/dist/chunk-DXVGALW5.cjs.map +1 -0
- package/dist/{chunk-ZWEZ6ROI.js → chunk-NMAHYAC3.js} +8 -2
- package/dist/chunk-NMAHYAC3.js.map +1 -0
- package/dist/{chunk-QBDIQW5O.js → chunk-RPY5WO74.js} +407 -258
- package/dist/chunk-RPY5WO74.js.map +1 -0
- package/dist/{chunk-ZOOMOVE5.cjs → chunk-X3F3MYF4.cjs} +2 -2
- package/dist/{chunk-ZOOMOVE5.cjs.map → chunk-X3F3MYF4.cjs.map} +1 -1
- package/dist/{chunk-U2FEEK3Y.js → chunk-YYISGQML.js} +2 -2
- package/dist/{chunk-27FJDG3Y.cjs → chunk-ZXJSKVKS.cjs} +53 -5
- package/dist/chunk-ZXJSKVKS.cjs.map +1 -0
- package/dist/cli.cjs +3 -3
- package/dist/cli.js +2 -2
- package/dist/http.cjs +9 -9
- package/dist/http.js +5 -5
- package/dist/init.cjs +3 -3
- package/dist/init.js +1 -1
- package/dist/prompts/generate.d.ts +2 -2
- package/dist/server.cjs +5 -5
- package/dist/server.d.ts +1 -1
- package/dist/server.js +4 -4
- package/dist/stdio.cjs +7 -7
- package/dist/stdio.js +5 -5
- package/dist/tools/destructive-policy.d.ts +2 -2
- package/dist/tools/destructive-policy.d.ts.map +1 -1
- package/dist/tools/groups.d.ts.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/modules.d.ts.map +1 -1
- package/dist/tools/types.d.ts +1 -1
- package/package.json +2 -2
- package/dist/chunk-27FJDG3Y.cjs.map +0 -1
- package/dist/chunk-2EY3U7IR.js.map +0 -1
- package/dist/chunk-EA5YGN5Z.cjs.map +0 -1
- package/dist/chunk-IQU4YMPO.cjs.map +0 -1
- package/dist/chunk-QBDIQW5O.js.map +0 -1
- package/dist/chunk-ZWEZ6ROI.js.map +0 -1
- /package/dist/{chunk-U2FEEK3Y.js.map → chunk-YYISGQML.js.map} +0 -0
|
@@ -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.5.2\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 176 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.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,6 +1,6 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunkDXVGALW5cjs = require('./chunk-DXVGALW5.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
var _chunk4U7BDEQZcjs = require('./chunk-4U7BDEQZ.cjs');
|
|
@@ -24,7 +24,7 @@ function fatal(message) {
|
|
|
24
24
|
}
|
|
25
25
|
function parseDestructiveToolsModeOrExit(raw, source) {
|
|
26
26
|
try {
|
|
27
|
-
return
|
|
27
|
+
return _chunkDXVGALW5cjs.parseDestructiveToolsMode.call(void 0, raw, source);
|
|
28
28
|
} catch (error) {
|
|
29
29
|
return fatal(error instanceof Error ? error.message : String(error));
|
|
30
30
|
}
|
|
@@ -176,4 +176,4 @@ function parseArgs(args) {
|
|
|
176
176
|
|
|
177
177
|
|
|
178
178
|
exports.parseArgs = parseArgs;
|
|
179
|
-
//# sourceMappingURL=chunk-
|
|
179
|
+
//# sourceMappingURL=chunk-55HSTOYO.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-DG22XTGE.cjs","../src/cli.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACA;AC4BA,IAAM,iBAAA,EAAmB,qBAAA;AACzB,IAAM,YAAA,EAAc,kDAAA;AAGpB,SAAS,KAAA,CAAM,OAAA,EAAwB;AACrC,EAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA;AACjB,EAAA;AAChB;AAQS;AAIH,EAAA;AACK,IAAA;AACO,EAAA;AACgB,IAAA;AAChC,EAAA;AACF;AASE;AAGkB,EAAA;AACY,IAAA;AACG,IAAA;AACxB,IAAA;AACT,EAAA;AACiC,EAAA;AAC1B,EAAA;AACT;AAEqD;AACzB,EAAA;AACL,EAAA;AACX,IAAA;AACkB,MAAA;AAC1B,IAAA;AACF,EAAA;AAOoE,EAAA;AAC/C,IAAA;AACE,IAAA;AACf,IAAA;AACA,IAAA;AACqB,IAAA;AACb,IAAA;AACc,IAAA;AAC9B,EAAA;AAGI,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEiB,EAAA;AACO,EAAA;AACF,IAAA;AAOZ,IAAA;AAEA,MAAA;AACa,MAAA;AACI,QAAA;AAC3B,MAAA;AAK2B,MAAA;AAC3B,MAAA;AACF,IAAA;AAIqB,IAAA;AACW,IAAA;AACZ,MAAA;AAClB,MAAA;AACF,IAAA;AACkB,IAAA;AACW,IAAA;AAChB,MAAA;AACX,MAAA;AACF,IAAA;AACoB,IAAA;AACW,IAAA;AAChB,MAAA;AACb,MAAA;AACF,IAAA;AAEa,IAAA;AACN,MAAA;AACmB,QAAA;AACtB,QAAA;AACG,MAAA;AACqB,QAAA;AACxB,QAAA;AACG,MAAA;AACW,QAAA;AACd,QAAA;AACa,MAAA;AACS,QAAA;AACD,QAAA;AACrB,QAAA;AACF,MAAA;AACK,MAAA;AACoB,QAAA;AACvB,QAAA;AACa,MAAA;AACa,QAAA;AACD,QAAA;AACL,QAAA;AACL,UAAA;AACf,QAAA;AAEqB,QAAA;AACrB,QAAA;AACF,MAAA;AACK,MAAA;AACI,QAAA;AACP,QAAA;AACJ,IAAA;AACF,EAAA;AASE,EAAA;AAEc,IAAA;AACZ,IAAA;AACF,EAAA;AAIE,EAAA;AACA,EAAA;AACY,IAAA;AACN,MAAA;AACW,MAAA;AACJ,MAAA;AACd,IAAA;AACa,EAAA;AACgB,IAAA;AAChC,EAAA;AAE4B,EAAA;AAEI,EAAA;AAGL,IAAA;AACf,MAAA;AACN,QAAA;AACF,MAAA;AACe,MAAA;AACjB,IAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AACM,MAAA;AACE,QAAA;AACQ,QAAA;AACjB,MAAA;AACsB,MAAA;AACT,IAAA;AACO,MAAA;AACf,MAAA;AACR,IAAA;AACO,IAAA;AACF,MAAA;AACmB,MAAA;AACtB,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAEmB,EAAA;AACX,IAAA;AACR,EAAA;AACqB,EAAA;AACb,IAAA;AACR,EAAA;AAEO,EAAA;AACF,IAAA;AAC0B,IAAA;AAC7B,IAAA;AACA,IAAA;AACF,EAAA;AACF;AD5FkC;AACA;AACA;AACA","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-DG22XTGE.cjs","sourcesContent":[null,"import { isEnvTruthy } from './env'\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\nconst DESTRUCTIVE_FLAG = '--destructive-tools'\nconst DOCTOR_HINT = 'Run `canvas-lms-mcp doctor` to check your setup.'\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` and `authProfile` are deliberately absent from this\n // object *and* from its type: both are resolved after the loop, once we know\n // whether the command line supplied a flag. Omitting the keys makes the\n // compiler demand them at the `return` below, so the resolution cannot be\n // dropped, and leaves no placeholder here that could survive and fail *open*.\n const config: Omit<CliConfig, 'destructiveTools' | 'authProfile'> = {\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 }\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 }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-55HSTOYO.cjs","../src/cli.ts"],"names":[],"mappings":"AAAA;AACE;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACA;AC4BA,IAAM,iBAAA,EAAmB,qBAAA;AACzB,IAAM,YAAA,EAAc,kDAAA;AAGpB,SAAS,KAAA,CAAM,OAAA,EAAwB;AACrC,EAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,OAAA,EAAU,OAAO,CAAA,CAAA;AACjB,EAAA;AAChB;AAQS;AAIH,EAAA;AACK,IAAA;AACO,EAAA;AACgB,IAAA;AAChC,EAAA;AACF;AASE;AAGkB,EAAA;AACY,IAAA;AACG,IAAA;AACxB,IAAA;AACT,EAAA;AACiC,EAAA;AAC1B,EAAA;AACT;AAEqD;AACzB,EAAA;AACL,EAAA;AACX,IAAA;AACkB,MAAA;AAC1B,IAAA;AACF,EAAA;AAOoE,EAAA;AAC/C,IAAA;AACE,IAAA;AACf,IAAA;AACA,IAAA;AACqB,IAAA;AACb,IAAA;AACc,IAAA;AAC9B,EAAA;AAGI,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEiB,EAAA;AACO,EAAA;AACF,IAAA;AAOZ,IAAA;AAEA,MAAA;AACa,MAAA;AACI,QAAA;AAC3B,MAAA;AAK2B,MAAA;AAC3B,MAAA;AACF,IAAA;AAIqB,IAAA;AACW,IAAA;AACZ,MAAA;AAClB,MAAA;AACF,IAAA;AACkB,IAAA;AACW,IAAA;AAChB,MAAA;AACX,MAAA;AACF,IAAA;AACoB,IAAA;AACW,IAAA;AAChB,MAAA;AACb,MAAA;AACF,IAAA;AAEa,IAAA;AACN,MAAA;AACmB,QAAA;AACtB,QAAA;AACG,MAAA;AACqB,QAAA;AACxB,QAAA;AACG,MAAA;AACW,QAAA;AACd,QAAA;AACa,MAAA;AACS,QAAA;AACD,QAAA;AACrB,QAAA;AACF,MAAA;AACK,MAAA;AACoB,QAAA;AACvB,QAAA;AACa,MAAA;AACa,QAAA;AACD,QAAA;AACL,QAAA;AACL,UAAA;AACf,QAAA;AAEqB,QAAA;AACrB,QAAA;AACF,MAAA;AACK,MAAA;AACI,QAAA;AACP,QAAA;AACJ,IAAA;AACF,EAAA;AASE,EAAA;AAEc,IAAA;AACZ,IAAA;AACF,EAAA;AAIE,EAAA;AACA,EAAA;AACY,IAAA;AACN,MAAA;AACW,MAAA;AACJ,MAAA;AACd,IAAA;AACa,EAAA;AACgB,IAAA;AAChC,EAAA;AAE4B,EAAA;AAEI,EAAA;AAGL,IAAA;AACf,MAAA;AACN,QAAA;AACF,MAAA;AACe,MAAA;AACjB,IAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AACM,MAAA;AACE,QAAA;AACQ,QAAA;AACjB,MAAA;AACsB,MAAA;AACT,IAAA;AACO,MAAA;AACf,MAAA;AACR,IAAA;AACO,IAAA;AACF,MAAA;AACmB,MAAA;AACtB,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAEmB,EAAA;AACX,IAAA;AACR,EAAA;AACqB,EAAA;AACb,IAAA;AACR,EAAA;AAEO,EAAA;AACF,IAAA;AAC0B,IAAA;AAC7B,IAAA;AACA,IAAA;AACF,EAAA;AACF;AD5FkC;AACA;AACA;AACA","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-55HSTOYO.cjs","sourcesContent":[null,"import { isEnvTruthy } from './env'\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\nconst DESTRUCTIVE_FLAG = '--destructive-tools'\nconst DOCTOR_HINT = 'Run `canvas-lms-mcp doctor` to check your setup.'\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` and `authProfile` are deliberately absent from this\n // object *and* from its type: both are resolved after the loop, once we know\n // whether the command line supplied a flag. Omitting the keys makes the\n // compiler demand them at the `return` below, so the resolution cannot be\n // dropped, and leaves no placeholder here that could survive and fail *open*.\n const config: Omit<CliConfig, 'destructiveTools' | 'authProfile'> = {\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 }\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 }\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasApiError,
|
|
3
3
|
CanvasHttpClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-3ED6EUGQ.js";
|
|
5
5
|
|
|
6
6
|
// src/canvas/courses.ts
|
|
7
7
|
var DEFAULT_LIST_INCLUDE = ["term"];
|
|
@@ -679,6 +679,44 @@ var GroupsModule = class {
|
|
|
679
679
|
async listMembers(groupId) {
|
|
680
680
|
return this.client.paginate(`/api/v1/groups/${groupId}/users`);
|
|
681
681
|
}
|
|
682
|
+
async listCategories(courseId) {
|
|
683
|
+
return this.client.paginate(`/api/v1/courses/${courseId}/group_categories`);
|
|
684
|
+
}
|
|
685
|
+
/** Canvas renders the created category with no root key. */
|
|
686
|
+
async createCategory(courseId, params) {
|
|
687
|
+
return this.client.request(
|
|
688
|
+
`/api/v1/courses/${courseId}/group_categories`,
|
|
689
|
+
{ method: "POST", body: JSON.stringify(params) }
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Groups are created inside a group category, so this is addressed by the
|
|
694
|
+
* category ID, not the course ID. Canvas renders the new group with no root
|
|
695
|
+
* key (its `users` list is empty for a new group).
|
|
696
|
+
*/
|
|
697
|
+
async create(groupCategoryId, params) {
|
|
698
|
+
return this.client.request(`/api/v1/group_categories/${groupCategoryId}/groups`, {
|
|
699
|
+
method: "POST",
|
|
700
|
+
body: JSON.stringify(params)
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
/** Canvas deletes the group, removes its members, and answers with the deleted group. */
|
|
704
|
+
async delete(groupId) {
|
|
705
|
+
return this.client.request(`/api/v1/groups/${groupId}`, { method: "DELETE" });
|
|
706
|
+
}
|
|
707
|
+
/** Returns the membership Canvas creates (or the existing one, if the user is already a member). */
|
|
708
|
+
async addMember(groupId, userId) {
|
|
709
|
+
return this.client.request(`/api/v1/groups/${groupId}/memberships`, {
|
|
710
|
+
method: "POST",
|
|
711
|
+
body: JSON.stringify({ user_id: userId })
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/** Canvas answers a membership removal with `{ "ok": true }`. */
|
|
715
|
+
async removeMember(groupId, userId) {
|
|
716
|
+
return this.client.request(`/api/v1/groups/${groupId}/users/${userId}`, {
|
|
717
|
+
method: "DELETE"
|
|
718
|
+
});
|
|
719
|
+
}
|
|
682
720
|
};
|
|
683
721
|
|
|
684
722
|
// src/canvas/enrollments.ts
|
|
@@ -851,6 +889,16 @@ var ModulesModule = class {
|
|
|
851
889
|
body: JSON.stringify({ module: params })
|
|
852
890
|
});
|
|
853
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* Canvas answers a module DELETE with the deleted module's body (the
|
|
894
|
+
* controller renders `module_json` with no root key). The module's items go
|
|
895
|
+
* with it; the content they link to is stored separately in the course.
|
|
896
|
+
*/
|
|
897
|
+
async delete(courseId, moduleId) {
|
|
898
|
+
return this.client.request(`/api/v1/courses/${courseId}/modules/${moduleId}`, {
|
|
899
|
+
method: "DELETE"
|
|
900
|
+
});
|
|
901
|
+
}
|
|
854
902
|
async createItem(courseId, moduleId, params) {
|
|
855
903
|
return this.client.request(
|
|
856
904
|
`/api/v1/courses/${courseId}/modules/${moduleId}/items`,
|
|
@@ -2060,4 +2108,4 @@ export {
|
|
|
2060
2108
|
normalizeCanvasIdInput,
|
|
2061
2109
|
CanvasClient
|
|
2062
2110
|
};
|
|
2063
|
-
//# sourceMappingURL=chunk-
|
|
2111
|
+
//# sourceMappingURL=chunk-CDOKT4BS.js.map
|