canvas-lms-mcp 1.31.2 → 1.32.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.
Files changed (74) hide show
  1. package/README.md +2 -0
  2. package/dist/canvas/index.cjs +3 -3
  3. package/dist/canvas/index.js +2 -2
  4. package/dist/{chunk-ZJURO66M.cjs → chunk-26UC2T3R.cjs} +2 -2
  5. package/dist/chunk-26UC2T3R.cjs.map +1 -0
  6. package/dist/{chunk-OOP4MJA7.cjs → chunk-4U7BDEQZ.cjs} +7 -3
  7. package/dist/chunk-4U7BDEQZ.cjs.map +1 -0
  8. package/dist/{chunk-T7NTLKGA.js → chunk-5WTLGWRA.js} +328 -4
  9. package/dist/chunk-5WTLGWRA.js.map +1 -0
  10. package/dist/{chunk-UP6NGBEG.cjs → chunk-DELZA7F6.cjs} +4 -4
  11. package/dist/{chunk-UP6NGBEG.cjs.map → chunk-DELZA7F6.cjs.map} +1 -1
  12. package/dist/{chunk-E2RSL5I4.js → chunk-H3LDGHYB.js} +2 -2
  13. package/dist/{chunk-AT6C4S4M.cjs → chunk-KS6CBFHY.cjs} +369 -45
  14. package/dist/chunk-KS6CBFHY.cjs.map +1 -0
  15. package/dist/{chunk-A6OUWDRT.js → chunk-LYNXEFQN.js} +2 -2
  16. package/dist/chunk-LYNXEFQN.js.map +1 -0
  17. package/dist/{chunk-57JW3L6O.js → chunk-PC6KZ5DK.js} +2 -2
  18. package/dist/{chunk-GC6O2AT6.cjs → chunk-TOX2OUSE.cjs} +4 -4
  19. package/dist/{chunk-GC6O2AT6.cjs.map → chunk-TOX2OUSE.cjs.map} +1 -1
  20. package/dist/{chunk-46PS7BZ5.js → chunk-WJ5RENYH.js} +6 -2
  21. package/dist/chunk-WJ5RENYH.js.map +1 -0
  22. package/dist/cli.cjs +3 -3
  23. package/dist/cli.js +2 -2
  24. package/dist/discovery/manifests.d.ts.map +1 -1
  25. package/dist/http.cjs +10 -10
  26. package/dist/http.js +5 -5
  27. package/dist/init.cjs +5 -5
  28. package/dist/init.js +2 -2
  29. package/dist/prompts/arguments.d.ts +13 -0
  30. package/dist/prompts/arguments.d.ts.map +1 -0
  31. package/dist/prompts/catalog.d.ts +36 -0
  32. package/dist/prompts/catalog.d.ts.map +1 -0
  33. package/dist/prompts/generate.d.ts +54 -0
  34. package/dist/prompts/generate.d.ts.map +1 -0
  35. package/dist/prompts/index.d.ts +36 -0
  36. package/dist/prompts/index.d.ts.map +1 -0
  37. package/dist/prompts/skills.generated.d.ts +3 -0
  38. package/dist/prompts/skills.generated.d.ts.map +1 -0
  39. package/dist/prompts/types.d.ts +38 -0
  40. package/dist/prompts/types.d.ts.map +1 -0
  41. package/dist/server.cjs +5 -5
  42. package/dist/server.d.ts.map +1 -1
  43. package/dist/server.js +4 -4
  44. package/dist/stdio.cjs +7 -7
  45. package/dist/stdio.js +5 -5
  46. package/dist/tools/registry-probe.d.ts +16 -0
  47. package/dist/tools/registry-probe.d.ts.map +1 -0
  48. package/dist/tools/roles.d.ts +8 -0
  49. package/dist/tools/roles.d.ts.map +1 -1
  50. package/package.json +5 -3
  51. package/skills/canvas-accessibility-sweep/SKILL.md +110 -0
  52. package/skills/canvas-admin-roster/SKILL.md +173 -0
  53. package/skills/canvas-at-risk-students/SKILL.md +90 -0
  54. package/skills/canvas-course-pulse/SKILL.md +149 -0
  55. package/skills/canvas-course-qc/SKILL.md +132 -0
  56. package/skills/canvas-discussion-facilitator/SKILL.md +152 -0
  57. package/skills/canvas-gradebook-audit/SKILL.md +87 -0
  58. package/skills/canvas-grading-pass/SKILL.md +128 -0
  59. package/skills/canvas-morning-check/SKILL.md +89 -0
  60. package/skills/canvas-office-hours/SKILL.md +127 -0
  61. package/skills/canvas-outcome-tracker/SKILL.md +114 -0
  62. package/skills/canvas-peer-review-tracker/SKILL.md +162 -0
  63. package/skills/canvas-quiz-review/SKILL.md +150 -0
  64. package/skills/canvas-student-todo/SKILL.md +88 -0
  65. package/skills/canvas-syllabus-coach/SKILL.md +140 -0
  66. package/skills/canvas-week-plan/SKILL.md +90 -0
  67. package/dist/chunk-46PS7BZ5.js.map +0 -1
  68. package/dist/chunk-A6OUWDRT.js.map +0 -1
  69. package/dist/chunk-AT6C4S4M.cjs.map +0 -1
  70. package/dist/chunk-OOP4MJA7.cjs.map +0 -1
  71. package/dist/chunk-T7NTLKGA.js.map +0 -1
  72. package/dist/chunk-ZJURO66M.cjs.map +0 -1
  73. /package/dist/{chunk-E2RSL5I4.js.map → chunk-H3LDGHYB.js.map} +0 -0
  74. /package/dist/{chunk-57JW3L6O.js.map → chunk-PC6KZ5DK.js.map} +0 -0
package/README.md CHANGED
@@ -89,6 +89,8 @@ npx skills add bruchris/canvas-lms-mcp
89
89
 
90
90
  Skills are markdown workflow files (no extra dependencies). They work with the MCP server you already have installed. See the [`skills/` directory](./skills/) for the full list.
91
91
 
92
+ The same 16 workflows are also served over MCP as [prompts](https://modelcontextprotocol.io/docs/concepts/prompts), so a host without a skill-file loader — Claude Desktop, ChatGPT, or an application embedding this server in-process — can list them with `prompts/list` and fetch one with `prompts/get`. Each prompt advertises optional `course_id`-style arguments a host can prefill, and marks the workflows that reach write tools. Prompts are inert templates the user chooses: selecting one grants the model no tool authority it did not already have.
93
+
92
94
  ## Example Prompts
93
95
 
94
96
  Once configured, try these prompts with your AI client:
@@ -1,17 +1,17 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunkUP6NGBEGcjs = require('../chunk-UP6NGBEG.cjs');
3
+ var _chunkDELZA7F6cjs = require('../chunk-DELZA7F6.cjs');
4
4
 
5
5
 
6
6
 
7
7
 
8
8
 
9
- var _chunkZJURO66Mcjs = require('../chunk-ZJURO66M.cjs');
9
+ var _chunk26UC2T3Rcjs = require('../chunk-26UC2T3R.cjs');
10
10
 
11
11
 
12
12
 
13
13
 
14
14
 
15
15
 
16
- exports.CanvasApiError = _chunkZJURO66Mcjs.CanvasApiError; exports.CanvasClient = _chunkUP6NGBEGcjs.CanvasClient; exports.CanvasHttpClient = _chunkZJURO66Mcjs.CanvasHttpClient; exports.appendCanvasQuery = _chunkZJURO66Mcjs.appendCanvasQuery; exports.toCanvasQuery = _chunkZJURO66Mcjs.toCanvasQuery;
16
+ exports.CanvasApiError = _chunk26UC2T3Rcjs.CanvasApiError; exports.CanvasClient = _chunkDELZA7F6cjs.CanvasClient; exports.CanvasHttpClient = _chunk26UC2T3Rcjs.CanvasHttpClient; exports.appendCanvasQuery = _chunk26UC2T3Rcjs.appendCanvasQuery; exports.toCanvasQuery = _chunk26UC2T3Rcjs.toCanvasQuery;
17
17
  //# sourceMappingURL=index.cjs.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  CanvasClient
3
- } from "../chunk-E2RSL5I4.js";
3
+ } from "../chunk-H3LDGHYB.js";
4
4
  import {
5
5
  CanvasApiError,
6
6
  CanvasHttpClient,
7
7
  appendCanvasQuery,
8
8
  toCanvasQuery
9
- } from "../chunk-A6OUWDRT.js";
9
+ } from "../chunk-LYNXEFQN.js";
10
10
  export {
11
11
  CanvasApiError,
12
12
  CanvasClient,
@@ -1,5 +1,5 @@
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; }// package.json
2
- var version = "1.31.2";
2
+ var version = "1.32.0";
3
3
 
4
4
  // src/canvas/query.ts
5
5
  function appendCanvasQuery(target, params) {
@@ -176,4 +176,4 @@ var CanvasHttpClient = class {
176
176
 
177
177
 
178
178
  exports.version = version; exports.appendCanvasQuery = appendCanvasQuery; exports.toCanvasQuery = toCanvasQuery; exports.CanvasApiError = CanvasApiError; exports.CanvasHttpClient = CanvasHttpClient;
179
- //# sourceMappingURL=chunk-ZJURO66M.cjs.map
179
+ //# sourceMappingURL=chunk-26UC2T3R.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-26UC2T3R.cjs","../package.json","../src/canvas/query.ts","../src/canvas/client.ts"],"names":[],"mappings":"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","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-26UC2T3R.cjs","sourcesContent":[null,"{\n \"name\": \"canvas-lms-mcp\",\n \"version\": \"1.32.0\",\n \"mcpName\": \"io.github.bruchris/canvas-lms-mcp\",\n \"description\": \"TypeScript MCP 1.x server for Canvas LMS — 167 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.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 \"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","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 this.assertNotTruncated(nextUrl, endpoint)\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 this.assertNotTruncated(nextUrl, endpoint)\n return results\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"]}
@@ -12,8 +12,11 @@ function tagAudience(defaultPrimaryAudience) {
12
12
  return tool;
13
13
  };
14
14
  }
15
+ function isAudienceVisibleForRole(audience, role) {
16
+ return ROLE_VISIBILITY[role].has(_nullishCoalesce(audience, () => ( "shared")));
17
+ }
15
18
  function isVisibleForRole(tool, role) {
16
- return ROLE_VISIBILITY[role].has(_nullishCoalesce(tool.audience, () => ( "shared")));
19
+ return isAudienceVisibleForRole(tool.audience, role);
17
20
  }
18
21
  function parseRole(raw) {
19
22
  if (raw == null) return { invalid: false };
@@ -29,5 +32,6 @@ function parseRole(raw) {
29
32
 
30
33
 
31
34
 
32
- exports.tagAudience = tagAudience; exports.isVisibleForRole = isVisibleForRole; exports.parseRole = parseRole;
33
- //# sourceMappingURL=chunk-OOP4MJA7.cjs.map
35
+
36
+ exports.tagAudience = tagAudience; exports.isAudienceVisibleForRole = isAudienceVisibleForRole; exports.isVisibleForRole = isVisibleForRole; exports.parseRole = parseRole;
37
+ //# sourceMappingURL=chunk-4U7BDEQZ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-4U7BDEQZ.cjs","../src/tools/roles.ts"],"names":[],"mappings":"AAAA;ACcO,IAAM,gBAAA,EAAiE;AAAA,EAC5E,OAAA,kBAAS,IAAI,GAAA,CAAkB,CAAC,QAAA,EAAU,SAAS,CAAC,CAAA;AAAA,EACpD,OAAA,kBAAS,IAAI,GAAA,CAAkB,CAAC,QAAA,EAAU,UAAU,CAAC,CAAA;AAAA,EACrD,KAAA,kBAAO,IAAI,GAAA,CAAkB,CAAC,QAAA,EAAU,UAAA,EAAY,OAAO,CAAC;AAC9D,CAAA;AAQO,SAAS,WAAA,CACd,sBAAA,EAC0C;AAC1C,EAAA,OAAO,CAAC,IAAA,EAAA,GAAS;AACf,IAAA,GAAA,CAAI,IAAA,CAAK,SAAA,IAAa,KAAA,CAAA,EAAW;AAC/B,MAAA,IAAA,CAAK,SAAA,EAAW,sBAAA;AAAA,IAClB;AACA,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AACF;AASO,SAAS,wBAAA,CACd,QAAA,EACA,IAAA,EACS;AACT,EAAA,OAAO,eAAA,CAAgB,IAAI,CAAA,CAAE,GAAA,kBAAI,QAAA,UAAY,UAAQ,CAAA;AACvD;AAGO,SAAS,gBAAA,CAAiB,IAAA,EAAsB,IAAA,EAA2B;AAChF,EAAA,OAAO,wBAAA,CAAyB,IAAA,CAAK,QAAA,EAAU,IAAI,CAAA;AACrD;AAkBO,SAAS,SAAA,CAAU,GAAA,EAAiD;AACzE,EAAA,GAAA,CAAI,IAAA,GAAO,IAAA,EAAM,OAAO,EAAE,OAAA,EAAS,MAAM,CAAA;AACzC,EAAA,MAAM,MAAA,EAAQ,GAAA,CAAI,IAAA,CAAK,CAAA,CAAE,WAAA,CAAY,CAAA;AACrC,EAAA,GAAA,CAAI,MAAA,IAAU,GAAA,GAAM,MAAA,IAAU,KAAA,EAAO,OAAO,EAAE,OAAA,EAAS,MAAM,CAAA;AAC7D,EAAA,GAAA,CAAI,MAAA,IAAU,UAAA,GAAa,MAAA,IAAU,UAAA,GAAa,MAAA,IAAU,OAAA,EAAS;AACnE,IAAA,OAAO,EAAE,IAAA,EAAM,KAAA,EAAO,OAAA,EAAS,MAAM,CAAA;AAAA,EACvC;AACA,EAAA,OAAO,EAAE,OAAA,EAAS,KAAK,CAAA;AACzB;ADnDA;AACA;AACE;AACA;AACA;AACA;AACF,2KAAC","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-4U7BDEQZ.cjs","sourcesContent":[null,"// Role-based tool filtering (BRU-1637 / design BRU-1530).\n//\n// A role is a client-side UX/context-reduction filter only — Canvas still\n// enforces real permissions server-side. Setting `CANVAS_ROLE=admin` does not\n// grant admin powers; it only changes which tools appear in `tools/list`.\n\nimport type { CanvasRole, ToolAudience, ToolDefinition } from './types'\n\n/**\n * Which audiences each role can see. Note `teacher`/`admin` do NOT see the\n * `student`-only audience in v1 — the `get_my_*` tools are framed around the\n * caller being a student. Flipping this is a one-line change here if we later\n * decide instructors should see them.\n */\nexport const ROLE_VISIBILITY: Record<CanvasRole, ReadonlySet<ToolAudience>> = {\n student: new Set<ToolAudience>(['shared', 'student']),\n teacher: new Set<ToolAudience>(['shared', 'educator']),\n admin: new Set<ToolAudience>(['shared', 'educator', 'admin']),\n}\n\n/**\n * Returns a mapper that fills in the domain default audience for any tool that\n * did not declare its own. After mapping, every tool carries a concrete\n * `audience`, which the coverage gate (tests/tools/audience-coverage.test.ts)\n * enforces.\n */\nexport function tagAudience(\n defaultPrimaryAudience: ToolAudience,\n): (tool: ToolDefinition) => ToolDefinition {\n return (tool) => {\n if (tool.audience === undefined) {\n tool.audience = defaultPrimaryAudience\n }\n return tool\n }\n}\n\n/**\n * Whether an audience tag is visible to the given role.\n *\n * Split out from {@link isVisibleForRole} so the prompt surface\n * (`src/prompts/catalog.ts`) filters against this same table. A skill and the\n * tools it drives must never disagree about which role can see them.\n */\nexport function isAudienceVisibleForRole(\n audience: ToolAudience | undefined,\n role: CanvasRole,\n): boolean {\n return ROLE_VISIBILITY[role].has(audience ?? 'shared')\n}\n\n/** Whether a tool is visible to the given role. */\nexport function isVisibleForRole(tool: ToolDefinition, role: CanvasRole): boolean {\n return isAudienceVisibleForRole(tool.audience, role)\n}\n\nexport interface RoleParseResult {\n /** The canonical role, or undefined for unset / \"all\" / invalid input. */\n role?: CanvasRole\n /** True only when a non-empty value was supplied that is not a known role. */\n invalid: boolean\n}\n\n/**\n * Parse a raw role string (env var, CLI flag, or HTTP header) case-insensitively.\n *\n * - unset / empty / `all` → `{ invalid: false }` (no filter, no warning)\n * - `student` / `teacher` / `admin` (any case, trimmed) → `{ role, invalid: false }`\n * - anything else → `{ invalid: true }` (caller warns to stderr + registers all)\n *\n * Never throws — a config typo must not stop the MCP server from starting.\n */\nexport function parseRole(raw: string | null | undefined): RoleParseResult {\n if (raw == null) return { invalid: false }\n const value = raw.trim().toLowerCase()\n if (value === '' || value === 'all') return { invalid: false }\n if (value === 'student' || value === 'teacher' || value === 'admin') {\n return { role: value, invalid: false }\n }\n return { invalid: true }\n}\n"]}