@headless-lms/server 0.3.0 → 0.5.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 (228) hide show
  1. package/README.md +14 -0
  2. package/dist/app/community-mail.d.ts +10 -10
  3. package/dist/app/community-mail.js +72 -28
  4. package/dist/app/community-mail.js.map +1 -1
  5. package/dist/app/container.d.ts +48 -10
  6. package/dist/app/container.js +268 -48
  7. package/dist/app/container.js.map +1 -1
  8. package/dist/app/notification-channels.d.ts +47 -0
  9. package/dist/app/notification-channels.js +175 -0
  10. package/dist/app/notification-channels.js.map +1 -0
  11. package/dist/app/notification-reads.d.ts +23 -0
  12. package/dist/app/notification-reads.js +74 -0
  13. package/dist/app/notification-reads.js.map +1 -0
  14. package/dist/app/platform.d.ts +14 -0
  15. package/dist/app/platform.js +14 -0
  16. package/dist/app/platform.js.map +1 -0
  17. package/dist/http/config.d.ts +4 -0
  18. package/dist/http/errors.d.ts +1 -0
  19. package/dist/http/errors.js +24 -4
  20. package/dist/http/errors.js.map +1 -1
  21. package/dist/http/fastify.d.ts +8 -0
  22. package/dist/http/mcp/catalogue.d.ts +11 -9
  23. package/dist/http/mcp/catalogue.js +15 -21
  24. package/dist/http/mcp/catalogue.js.map +1 -1
  25. package/dist/http/mcp/challenge.js +3 -3
  26. package/dist/http/mcp/challenge.js.map +1 -1
  27. package/dist/http/mcp/route.js +7 -7
  28. package/dist/http/mcp/route.js.map +1 -1
  29. package/dist/http/mcp/test-support.d.ts +6 -5
  30. package/dist/http/mcp/test-support.js +32 -11
  31. package/dist/http/mcp/test-support.js.map +1 -1
  32. package/dist/http/mcp/tool.d.ts +3 -9
  33. package/dist/http/mcp/tool.js +14 -6
  34. package/dist/http/mcp/tool.js.map +1 -1
  35. package/dist/http/mcp/tools/automations.js +10 -20
  36. package/dist/http/mcp/tools/automations.js.map +1 -1
  37. package/dist/http/mcp/tools/catalog.js +25 -42
  38. package/dist/http/mcp/tools/catalog.js.map +1 -1
  39. package/dist/http/mcp/tools/comments.js +9 -19
  40. package/dist/http/mcp/tools/comments.js.map +1 -1
  41. package/dist/http/mcp/tools/communities.js +16 -31
  42. package/dist/http/mcp/tools/communities.js.map +1 -1
  43. package/dist/http/mcp/tools/community-members.js +9 -16
  44. package/dist/http/mcp/tools/community-members.js.map +1 -1
  45. package/dist/http/mcp/tools/community-moderation.js +13 -25
  46. package/dist/http/mcp/tools/community-moderation.js.map +1 -1
  47. package/dist/http/mcp/tools/courses.js +11 -21
  48. package/dist/http/mcp/tools/courses.js.map +1 -1
  49. package/dist/http/mcp/tools/events.d.ts +6 -0
  50. package/dist/http/mcp/tools/events.js +170 -0
  51. package/dist/http/mcp/tools/events.js.map +1 -0
  52. package/dist/http/mcp/tools/index.d.ts +5 -1
  53. package/dist/http/mcp/tools/index.js +45 -10
  54. package/dist/http/mcp/tools/index.js.map +1 -1
  55. package/dist/http/mcp/tools/organization.d.ts +6 -0
  56. package/dist/http/mcp/tools/organization.js +214 -0
  57. package/dist/http/mcp/tools/organization.js.map +1 -0
  58. package/dist/http/mcp/tools/people.js +30 -49
  59. package/dist/http/mcp/tools/people.js.map +1 -1
  60. package/dist/http/plugins/auth.js +37 -8
  61. package/dist/http/plugins/auth.js.map +1 -1
  62. package/dist/http/plugins/error-handler.d.ts +5 -1
  63. package/dist/http/plugins/error-handler.js +13 -3
  64. package/dist/http/plugins/error-handler.js.map +1 -1
  65. package/dist/http/plugins/openapi.js +18 -2
  66. package/dist/http/plugins/openapi.js.map +1 -1
  67. package/dist/http/plugins/platform.d.ts +7 -0
  68. package/dist/http/plugins/platform.js +13 -0
  69. package/dist/http/plugins/platform.js.map +1 -0
  70. package/dist/http/routes/access.js +54 -36
  71. package/dist/http/routes/access.js.map +1 -1
  72. package/dist/http/routes/activities.js +45 -45
  73. package/dist/http/routes/activities.js.map +1 -1
  74. package/dist/http/routes/assets.js +30 -65
  75. package/dist/http/routes/assets.js.map +1 -1
  76. package/dist/http/routes/automations.js +37 -42
  77. package/dist/http/routes/automations.js.map +1 -1
  78. package/dist/http/routes/bundles.js +33 -36
  79. package/dist/http/routes/bundles.js.map +1 -1
  80. package/dist/http/routes/communities.js +144 -136
  81. package/dist/http/routes/communities.js.map +1 -1
  82. package/dist/http/routes/community-moderation.js +83 -137
  83. package/dist/http/routes/community-moderation.js.map +1 -1
  84. package/dist/http/routes/course-analytics.js +32 -15
  85. package/dist/http/routes/course-analytics.js.map +1 -1
  86. package/dist/http/routes/courses.js +25 -32
  87. package/dist/http/routes/courses.js.map +1 -1
  88. package/dist/http/routes/dashboard.js +9 -9
  89. package/dist/http/routes/dashboard.js.map +1 -1
  90. package/dist/http/routes/discussion.js +21 -39
  91. package/dist/http/routes/discussion.js.map +1 -1
  92. package/dist/http/routes/downloads.js +41 -47
  93. package/dist/http/routes/downloads.js.map +1 -1
  94. package/dist/http/routes/entitlements.js +19 -23
  95. package/dist/http/routes/entitlements.js.map +1 -1
  96. package/dist/http/routes/events.d.ts +7 -0
  97. package/dist/http/routes/events.js +333 -0
  98. package/dist/http/routes/events.js.map +1 -0
  99. package/dist/http/routes/integrations.js +40 -73
  100. package/dist/http/routes/integrations.js.map +1 -1
  101. package/dist/http/routes/member/assets.js +10 -13
  102. package/dist/http/routes/member/assets.js.map +1 -1
  103. package/dist/http/routes/member/chats.js +177 -75
  104. package/dist/http/routes/member/chats.js.map +1 -1
  105. package/dist/http/routes/member/comments.js +65 -26
  106. package/dist/http/routes/member/comments.js.map +1 -1
  107. package/dist/http/routes/member/community.js +68 -51
  108. package/dist/http/routes/member/community.js.map +1 -1
  109. package/dist/http/routes/member/courses.js +12 -19
  110. package/dist/http/routes/member/courses.js.map +1 -1
  111. package/dist/http/routes/member/devices.d.ts +7 -0
  112. package/dist/http/routes/member/devices.js +97 -0
  113. package/dist/http/routes/member/devices.js.map +1 -0
  114. package/dist/http/routes/member/downloads.js +6 -9
  115. package/dist/http/routes/member/downloads.js.map +1 -1
  116. package/dist/http/routes/member/events.d.ts +7 -0
  117. package/dist/http/routes/member/events.js +386 -0
  118. package/dist/http/routes/member/events.js.map +1 -0
  119. package/dist/http/routes/member/files.js +71 -46
  120. package/dist/http/routes/member/files.js.map +1 -1
  121. package/dist/http/routes/member/index.js +6 -0
  122. package/dist/http/routes/member/index.js.map +1 -1
  123. package/dist/http/routes/member/notification-settings.d.ts +7 -0
  124. package/dist/http/routes/member/notification-settings.js +100 -0
  125. package/dist/http/routes/member/notification-settings.js.map +1 -0
  126. package/dist/http/routes/member/notifications.js +69 -45
  127. package/dist/http/routes/member/notifications.js.map +1 -1
  128. package/dist/http/routes/member/org.js +5 -15
  129. package/dist/http/routes/member/org.js.map +1 -1
  130. package/dist/http/routes/member/posts.js +94 -95
  131. package/dist/http/routes/member/posts.js.map +1 -1
  132. package/dist/http/routes/member/profile.js +15 -6
  133. package/dist/http/routes/member/profile.js.map +1 -1
  134. package/dist/http/routes/member/progress.js +7 -11
  135. package/dist/http/routes/member/progress.js.map +1 -1
  136. package/dist/http/routes/organizations.js +415 -110
  137. package/dist/http/routes/organizations.js.map +1 -1
  138. package/dist/http/routes/platform.d.ts +7 -0
  139. package/dist/http/routes/platform.js +61 -0
  140. package/dist/http/routes/platform.js.map +1 -0
  141. package/dist/http/routes/students.js +12 -75
  142. package/dist/http/routes/students.js.map +1 -1
  143. package/dist/http/routes/webhooks.js +30 -39
  144. package/dist/http/routes/webhooks.js.map +1 -1
  145. package/dist/http/routes.js +9 -0
  146. package/dist/http/routes.js.map +1 -1
  147. package/dist/http/schemas/access.d.ts +346 -64
  148. package/dist/http/schemas/access.js +39 -12
  149. package/dist/http/schemas/access.js.map +1 -1
  150. package/dist/http/schemas/assets.d.ts +13 -8
  151. package/dist/http/schemas/assets.js +6 -5
  152. package/dist/http/schemas/assets.js.map +1 -1
  153. package/dist/http/schemas/automations.d.ts +4 -8
  154. package/dist/http/schemas/chats.d.ts +238 -36
  155. package/dist/http/schemas/chats.js +14 -7
  156. package/dist/http/schemas/chats.js.map +1 -1
  157. package/dist/http/schemas/community.d.ts +140 -33
  158. package/dist/http/schemas/community.js +25 -9
  159. package/dist/http/schemas/community.js.map +1 -1
  160. package/dist/http/schemas/course-analytics.d.ts +51 -1
  161. package/dist/http/schemas/course-analytics.js +19 -1
  162. package/dist/http/schemas/course-analytics.js.map +1 -1
  163. package/dist/http/schemas/dashboard.d.ts +1 -0
  164. package/dist/http/schemas/dashboard.js +2 -1
  165. package/dist/http/schemas/dashboard.js.map +1 -1
  166. package/dist/http/schemas/discussion.d.ts +45 -73
  167. package/dist/http/schemas/discussion.js +16 -4
  168. package/dist/http/schemas/discussion.js.map +1 -1
  169. package/dist/http/schemas/entitlements.d.ts +9 -12
  170. package/dist/http/schemas/entitlements.js +11 -5
  171. package/dist/http/schemas/entitlements.js.map +1 -1
  172. package/dist/http/schemas/events.d.ts +527 -0
  173. package/dist/http/schemas/events.js +64 -0
  174. package/dist/http/schemas/events.js.map +1 -0
  175. package/dist/http/schemas/files.d.ts +15 -5
  176. package/dist/http/schemas/files.js +10 -2
  177. package/dist/http/schemas/files.js.map +1 -1
  178. package/dist/http/schemas/index.d.ts +14 -12
  179. package/dist/http/schemas/index.js +14 -12
  180. package/dist/http/schemas/invites.d.ts +3 -47
  181. package/dist/http/schemas/invites.js +4 -25
  182. package/dist/http/schemas/invites.js.map +1 -1
  183. package/dist/http/schemas/member.d.ts +94 -7
  184. package/dist/http/schemas/member.js +7 -3
  185. package/dist/http/schemas/member.js.map +1 -1
  186. package/dist/http/schemas/members.d.ts +329 -65
  187. package/dist/http/schemas/members.js +67 -25
  188. package/dist/http/schemas/members.js.map +1 -1
  189. package/dist/http/schemas/notifications.d.ts +502 -175
  190. package/dist/http/schemas/notifications.js +148 -28
  191. package/dist/http/schemas/notifications.js.map +1 -1
  192. package/dist/http/schemas/platform.d.ts +37 -0
  193. package/dist/http/schemas/platform.js +19 -0
  194. package/dist/http/schemas/platform.js.map +1 -0
  195. package/dist/http/schemas/posts.d.ts +92 -38
  196. package/dist/http/schemas/posts.js +4 -4
  197. package/dist/http/schemas/posts.js.map +1 -1
  198. package/dist/http/schemas/profile.d.ts +1 -1
  199. package/dist/http/schemas/shared.d.ts +10 -13
  200. package/dist/http/schemas/shared.js +3 -11
  201. package/dist/http/schemas/shared.js.map +1 -1
  202. package/dist/http/schemas/students.d.ts +22 -0
  203. package/dist/http/schemas/students.js +3 -1
  204. package/dist/http/schemas/students.js.map +1 -1
  205. package/dist/http/scope.d.ts +9 -12
  206. package/dist/http/scope.js +30 -35
  207. package/dist/http/scope.js.map +1 -1
  208. package/dist/http/server.js +9 -0
  209. package/dist/http/server.js.map +1 -1
  210. package/dist/http/shutdown.d.ts +11 -0
  211. package/dist/http/shutdown.js +40 -0
  212. package/dist/http/shutdown.js.map +1 -0
  213. package/dist/http/sse.d.ts +19 -0
  214. package/dist/http/sse.js +104 -0
  215. package/dist/http/sse.js.map +1 -0
  216. package/dist/http/test-caller.d.ts +13 -0
  217. package/dist/http/test-caller.js +14 -0
  218. package/dist/http/test-caller.js.map +1 -0
  219. package/dist/http/well-known.js +2 -2
  220. package/dist/http/well-known.js.map +1 -1
  221. package/dist/index.d.ts +5 -2
  222. package/dist/index.js +4 -2
  223. package/dist/index.js.map +1 -1
  224. package/dist/packages/server/package.js +1 -1
  225. package/package.json +7 -6
  226. package/dist/http/routes/member/actor.d.ts +0 -14
  227. package/dist/http/routes/member/actor.js +0 -11
  228. package/dist/http/routes/member/actor.js.map +0 -1
@@ -7,6 +7,7 @@ type Refusal = {
7
7
  path: string;
8
8
  message: string;
9
9
  }[];
10
+ details?: Record<string, string>;
10
11
  };
11
12
  declare function classifyError(error: unknown): Refusal | undefined;
12
13
  //#endregion
@@ -3,8 +3,8 @@ import { NoActiveOrgError } from "./scope.js";
3
3
  import { InviteError, OrganizationRuleError } from "@headless-lms/core/organizations";
4
4
  import { ActionInvocationError, AlreadyConnectedError, InvalidConfigError, UnknownIntegrationError } from "@headless-lms/core/integrations";
5
5
  import { AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "@headless-lms/core/automations";
6
+ import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError, UnavailableError, ValidationError } from "@headless-lms/core/shared/errors";
6
7
  import { hasZodFastifySchemaValidationErrors } from "fastify-type-provider-zod";
7
- import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "@headless-lms/core/shared/errors";
8
8
  //#region src/http/errors.ts
9
9
  const BODY_ERRORS = {
10
10
  FST_ERR_CTP_INVALID_JSON_BODY: {
@@ -39,13 +39,13 @@ function issuePath(context, instancePath) {
39
39
  function describeIssue(keyword, params, fallback) {
40
40
  switch (keyword) {
41
41
  case "invalid_type":
42
- if (params.received === "undefined" || params.received === "null") return "is required";
42
+ if (params.received === "undefined" || params.received === "null" || /received (undefined|null)$/.test(fallback)) return "is required";
43
43
  return params.expected ? `must be ${withArticle(params.expected)}` : fallback;
44
44
  case "too_small": return params.origin === "string" ? Number(params.minimum) === 1 ? "must not be empty" : `must be at least ${params.minimum} characters` : params.origin === "array" ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? "item" : "items"}` : `must be ${params.inclusive === false ? "greater than" : "at least"} ${params.minimum}`;
45
45
  case "too_big": return params.origin === "string" ? `must be at most ${params.maximum} characters` : params.origin === "array" ? `must have at most ${params.maximum} items` : `must be ${params.inclusive === false ? "less than" : "at most"} ${params.maximum}`;
46
46
  case "unrecognized_keys": return `has ${params.keys && params.keys.length > 1 ? "unknown fields" : "an unknown field"} ${(params.keys ?? []).map((key) => `"${key}"`).join(", ")}`;
47
47
  case "invalid_value": return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(", ")}`;
48
- case "invalid_format": return params.format ? `must be a valid ${params.format.replace(/_/g, " ")}` : fallback;
48
+ case "invalid_format": return params.format && params.format !== "regex" ? `must be a valid ${params.format.replace(/_/g, " ")}` : fallback;
49
49
  case "invalid_union": return "is not in an accepted shape";
50
50
  default: return fallback;
51
51
  }
@@ -64,7 +64,21 @@ function classifyError(error) {
64
64
  error: "invalid_input",
65
65
  message: error.message
66
66
  };
67
- if (error instanceof ConflictError || error instanceof OrganizationRuleError) return {
67
+ if (error instanceof ValidationError) return {
68
+ status: 422,
69
+ error: "unprocessable",
70
+ message: error.message,
71
+ issues: error.problems.map((problem) => ({
72
+ path: problem.path ? `body.${problem.path}` : "body",
73
+ message: problem.message
74
+ }))
75
+ };
76
+ if (error instanceof ConflictError) return {
77
+ status: 409,
78
+ error: error.code ?? "conflict",
79
+ message: error.message
80
+ };
81
+ if (error instanceof OrganizationRuleError) return {
68
82
  status: 409,
69
83
  error: "conflict",
70
84
  message: error.message
@@ -74,6 +88,12 @@ function classifyError(error) {
74
88
  error: "forbidden",
75
89
  message: error.message
76
90
  };
91
+ if (error instanceof UnavailableError) return {
92
+ status: 503,
93
+ error: "unavailable",
94
+ message: error.message,
95
+ ...error.details && { details: error.details }
96
+ };
77
97
  if (error instanceof UnauthorizedError) return {
78
98
  status: 401,
79
99
  error: "unauthorized",
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '@headless-lms/core/shared/errors';\nimport { InviteError, OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n} from '@headless-lms/core/automations';\nimport { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod';\nimport { NoActiveOrgError } from './scope.js';\nimport { UnauthorizedError } from './plugins/auth.js';\n\nexport type Refusal = {\n status: number;\n error: string;\n message: string;\n issues?: { path: string; message: string }[];\n};\n\nconst BODY_ERRORS: Record<string, Refusal> = {\n FST_ERR_CTP_INVALID_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_EMPTY_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_INVALID_MEDIA_TYPE: {\n status: 415,\n error: 'unsupported_media_type',\n message: 'Send the request body as application/json',\n },\n FST_ERR_CTP_BODY_TOO_LARGE: {\n status: 413,\n error: 'payload_too_large',\n message: 'The request body is too large',\n },\n FST_ERR_NOT_FOUND: { status: 404, error: 'not_found', message: 'No such route' },\n};\n\nfunction issuePath(context: string, instancePath: string): string {\n const inner = instancePath\n .split('/')\n .filter((part) => part.length > 0)\n .map((part) => (/^\\d+$/.test(part) ? `[${part}]` : `.${part}`))\n .join('');\n return `${context}${inner}`;\n}\n\ntype IssueParams = {\n expected?: string;\n received?: string;\n minimum?: number | bigint;\n maximum?: number | bigint;\n inclusive?: boolean;\n origin?: string;\n keys?: string[];\n values?: unknown[];\n format?: string;\n};\n\nfunction describeIssue(keyword: string | undefined, params: IssueParams, fallback: string): string {\n switch (keyword) {\n case 'invalid_type':\n if (params.received === 'undefined' || params.received === 'null') {\n return 'is required';\n }\n return params.expected ? `must be ${withArticle(params.expected)}` : fallback;\n case 'too_small':\n return params.origin === 'string'\n ? Number(params.minimum) === 1\n ? 'must not be empty'\n : `must be at least ${params.minimum} characters`\n : params.origin === 'array'\n ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? 'item' : 'items'}`\n : `must be ${params.inclusive === false ? 'greater than' : 'at least'} ${params.minimum}`;\n case 'too_big':\n return params.origin === 'string'\n ? `must be at most ${params.maximum} characters`\n : params.origin === 'array'\n ? `must have at most ${params.maximum} items`\n : `must be ${params.inclusive === false ? 'less than' : 'at most'} ${params.maximum}`;\n case 'unrecognized_keys':\n return `has ${params.keys && params.keys.length > 1 ? 'unknown fields' : 'an unknown field'} ${(params.keys ?? []).map((key) => `\"${key}\"`).join(', ')}`;\n case 'invalid_value':\n return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(', ')}`;\n case 'invalid_format':\n return params.format ? `must be a valid ${params.format.replace(/_/g, ' ')}` : fallback;\n case 'invalid_union':\n return 'is not in an accepted shape';\n default:\n return fallback;\n }\n}\n\nfunction withArticle(noun: string): string {\n return /^[aeiou]/i.test(noun) ? `an ${noun}` : `a ${noun}`;\n}\n\nexport function classifyError(error: unknown): Refusal | undefined {\n if (error instanceof NotFoundError) {\n return { status: 404, error: 'not_found', message: error.message };\n }\n if (error instanceof InvalidInputError) {\n return { status: 400, error: 'invalid_input', message: error.message };\n }\n if (error instanceof ConflictError || error instanceof OrganizationRuleError) {\n return { status: 409, error: 'conflict', message: error.message };\n }\n if (error instanceof ForbiddenError || error instanceof NoActiveOrgError) {\n return { status: 403, error: 'forbidden', message: error.message };\n }\n if (error instanceof UnauthorizedError) {\n return { status: 401, error: 'unauthorized', message: error.message };\n }\n if (error instanceof InviteError) {\n return { status: 400, error: 'invalid_invite', message: error.message };\n }\n if (error instanceof AlreadyConnectedError) {\n return { status: 409, error: 'already_connected', message: error.message };\n }\n if (error instanceof UnknownIntegrationError) {\n return { status: 400, error: 'unknown_integration', message: error.message };\n }\n if (error instanceof InvalidConfigError) {\n return { status: 400, error: 'invalid_config', message: error.message };\n }\n if (error instanceof InvalidTriggerError) {\n return { status: 400, error: 'invalid_trigger', message: error.message };\n }\n if (error instanceof ReservedActionError) {\n return { status: 400, error: 'reserved_action', message: error.message };\n }\n if (error instanceof ActionInvocationError) {\n return {\n status: 502,\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n };\n }\n if (error instanceof AutomationKindMismatchError) {\n return { status: 409, error: 'kind_mismatch', message: error.message };\n }\n if (hasZodFastifySchemaValidationErrors(error)) {\n const context = (error as { validationContext?: string }).validationContext ?? 'request';\n const issues = error.validation.map((issue) => ({\n path: issuePath(context, issue.instancePath),\n message: describeIssue(\n issue.keyword,\n (issue.params ?? {}) as IssueParams,\n issue.message ?? 'is invalid',\n ),\n }));\n const first = issues[0];\n return {\n status: 400,\n error: 'validation',\n message: first ? `${first.path}: ${first.message}` : 'The request is invalid',\n issues,\n };\n }\n const err = error as { statusCode?: number; code?: string };\n if (err.code && BODY_ERRORS[err.code]) {\n return BODY_ERRORS[err.code];\n }\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return {\n status: err.statusCode,\n error: 'bad_request',\n message: 'The request could not be processed',\n };\n }\n return undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/http/errors.ts"],"sourcesContent":["import {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n UnavailableError,\n ValidationError,\n} from '@headless-lms/core/shared/errors';\nimport { InviteError, OrganizationRuleError } from '@headless-lms/core/organizations';\nimport {\n ActionInvocationError,\n AlreadyConnectedError,\n InvalidConfigError,\n UnknownIntegrationError,\n} from '@headless-lms/core/integrations';\nimport {\n AutomationKindMismatchError,\n InvalidTriggerError,\n ReservedActionError,\n} from '@headless-lms/core/automations';\nimport { hasZodFastifySchemaValidationErrors } from 'fastify-type-provider-zod';\nimport { NoActiveOrgError } from './scope.js';\nimport { UnauthorizedError } from './plugins/auth.js';\n\nexport type Refusal = {\n status: number;\n error: string;\n message: string;\n issues?: { path: string; message: string }[];\n details?: Record<string, string>;\n};\n\nconst BODY_ERRORS: Record<string, Refusal> = {\n FST_ERR_CTP_INVALID_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_EMPTY_JSON_BODY: {\n status: 400,\n error: 'invalid_body',\n message: 'The request body must be valid JSON',\n },\n FST_ERR_CTP_INVALID_MEDIA_TYPE: {\n status: 415,\n error: 'unsupported_media_type',\n message: 'Send the request body as application/json',\n },\n FST_ERR_CTP_BODY_TOO_LARGE: {\n status: 413,\n error: 'payload_too_large',\n message: 'The request body is too large',\n },\n FST_ERR_NOT_FOUND: { status: 404, error: 'not_found', message: 'No such route' },\n};\n\nfunction issuePath(context: string, instancePath: string): string {\n const inner = instancePath\n .split('/')\n .filter((part) => part.length > 0)\n .map((part) => (/^\\d+$/.test(part) ? `[${part}]` : `.${part}`))\n .join('');\n return `${context}${inner}`;\n}\n\ntype IssueParams = {\n expected?: string;\n received?: string;\n minimum?: number | bigint;\n maximum?: number | bigint;\n inclusive?: boolean;\n origin?: string;\n keys?: string[];\n values?: unknown[];\n format?: string;\n};\n\nfunction describeIssue(keyword: string | undefined, params: IssueParams, fallback: string): string {\n switch (keyword) {\n case 'invalid_type':\n if (\n params.received === 'undefined' ||\n params.received === 'null' ||\n /received (undefined|null)$/.test(fallback)\n ) {\n return 'is required';\n }\n return params.expected ? `must be ${withArticle(params.expected)}` : fallback;\n case 'too_small':\n return params.origin === 'string'\n ? Number(params.minimum) === 1\n ? 'must not be empty'\n : `must be at least ${params.minimum} characters`\n : params.origin === 'array'\n ? `must have at least ${params.minimum} ${Number(params.minimum) === 1 ? 'item' : 'items'}`\n : `must be ${params.inclusive === false ? 'greater than' : 'at least'} ${params.minimum}`;\n case 'too_big':\n return params.origin === 'string'\n ? `must be at most ${params.maximum} characters`\n : params.origin === 'array'\n ? `must have at most ${params.maximum} items`\n : `must be ${params.inclusive === false ? 'less than' : 'at most'} ${params.maximum}`;\n case 'unrecognized_keys':\n return `has ${params.keys && params.keys.length > 1 ? 'unknown fields' : 'an unknown field'} ${(params.keys ?? []).map((key) => `\"${key}\"`).join(', ')}`;\n case 'invalid_value':\n return `must be one of ${(params.values ?? []).map((value) => JSON.stringify(value)).join(', ')}`;\n case 'invalid_format':\n return params.format && params.format !== 'regex'\n ? `must be a valid ${params.format.replace(/_/g, ' ')}`\n : fallback;\n case 'invalid_union':\n return 'is not in an accepted shape';\n default:\n return fallback;\n }\n}\n\nfunction withArticle(noun: string): string {\n return /^[aeiou]/i.test(noun) ? `an ${noun}` : `a ${noun}`;\n}\n\nexport function classifyError(error: unknown): Refusal | undefined {\n if (error instanceof NotFoundError) {\n return { status: 404, error: 'not_found', message: error.message };\n }\n if (error instanceof InvalidInputError) {\n return { status: 400, error: 'invalid_input', message: error.message };\n }\n if (error instanceof ValidationError) {\n return {\n status: 422,\n error: 'unprocessable',\n message: error.message,\n issues: error.problems.map((problem) => ({\n path: problem.path ? `body.${problem.path}` : 'body',\n message: problem.message,\n })),\n };\n }\n if (error instanceof ConflictError) {\n return { status: 409, error: error.code ?? 'conflict', message: error.message };\n }\n if (error instanceof OrganizationRuleError) {\n return { status: 409, error: 'conflict', message: error.message };\n }\n if (error instanceof ForbiddenError || error instanceof NoActiveOrgError) {\n return { status: 403, error: 'forbidden', message: error.message };\n }\n if (error instanceof UnavailableError) {\n return {\n status: 503,\n error: 'unavailable',\n message: error.message,\n ...(error.details && { details: error.details }),\n };\n }\n if (error instanceof UnauthorizedError) {\n return { status: 401, error: 'unauthorized', message: error.message };\n }\n if (error instanceof InviteError) {\n return { status: 400, error: 'invalid_invite', message: error.message };\n }\n if (error instanceof AlreadyConnectedError) {\n return { status: 409, error: 'already_connected', message: error.message };\n }\n if (error instanceof UnknownIntegrationError) {\n return { status: 400, error: 'unknown_integration', message: error.message };\n }\n if (error instanceof InvalidConfigError) {\n return { status: 400, error: 'invalid_config', message: error.message };\n }\n if (error instanceof InvalidTriggerError) {\n return { status: 400, error: 'invalid_trigger', message: error.message };\n }\n if (error instanceof ReservedActionError) {\n return { status: 400, error: 'reserved_action', message: error.message };\n }\n if (error instanceof ActionInvocationError) {\n return {\n status: 502,\n error: 'action_failed',\n message: 'The integration did not respond as expected',\n };\n }\n if (error instanceof AutomationKindMismatchError) {\n return { status: 409, error: 'kind_mismatch', message: error.message };\n }\n if (hasZodFastifySchemaValidationErrors(error)) {\n const context = (error as { validationContext?: string }).validationContext ?? 'request';\n const issues = error.validation.map((issue) => ({\n path: issuePath(context, issue.instancePath),\n message: describeIssue(\n issue.keyword,\n (issue.params ?? {}) as IssueParams,\n issue.message ?? 'is invalid',\n ),\n }));\n const first = issues[0];\n return {\n status: 400,\n error: 'validation',\n message: first ? `${first.path}: ${first.message}` : 'The request is invalid',\n issues,\n };\n }\n const err = error as { statusCode?: number; code?: string };\n if (err.code && BODY_ERRORS[err.code]) {\n return BODY_ERRORS[err.code];\n }\n if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 500) {\n return {\n status: err.statusCode,\n error: 'bad_request',\n message: 'The request could not be processed',\n };\n }\n return undefined;\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { ResolvedCaller } from "@headless-lms/core/organizations";
1
2
  import "fastify";
2
3
  import { User } from "@headless-lms/core/types";
3
4
  //#region src/http/fastify.d.ts
@@ -5,6 +6,10 @@ import { User } from "@headless-lms/core/types";
5
6
  * package entry point so this module — and its augmentation — gets loaded. */
6
7
  type AuthUser = User;
7
8
  declare module 'fastify' {
9
+ interface FastifyContextConfig {
10
+ /** A server-sent event stream; shutdown waits for it to finish. */
11
+ eventStream?: boolean;
12
+ }
8
13
  interface FastifyInstance {
9
14
  /** Session guard. Populates `authUser` (and `orgId` when the session has an
10
15
  * active org), or throws `UnauthorizedError`. Use for the routes reached
@@ -12,9 +17,11 @@ declare module 'fastify' {
12
17
  requireSession(request: FastifyRequest): Promise<void>;
13
18
  /** As `requireSession`, but also requires an active org on the session. */
14
19
  requireOrgSession(request: FastifyRequest): Promise<void>;
20
+ requirePlatform(request: FastifyRequest): Promise<void>;
15
21
  }
16
22
  interface FastifyRequest {
17
23
  authUser: User;
24
+ authUserId: string;
18
25
  /** Domain `organizations.id` for the session's active org — what every
19
26
  * domain read keys off. Guaranteed set by `requireOrgSession`; absent under
20
27
  * `requireSession` when the session has no active org. */
@@ -28,6 +35,7 @@ declare module 'fastify' {
28
35
  * and every other domain row references. Not `authUser.id`: the two match
29
36
  * only for a self-signup, never for a person an admin provisioned first. */
30
37
  userId: string;
38
+ resolvedCaller?: ResolvedCaller;
31
39
  }
32
40
  }
33
41
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { ToolKind } from "./tool.js";
2
- import { AccessScope, StaffRole } from "@headless-lms/core/organizations";
2
+ import { Level, Permission } from "@headless-lms/core/types";
3
3
  //#region src/http/mcp/catalogue.d.ts
4
4
  interface CatalogueField {
5
5
  name: string;
@@ -11,27 +11,29 @@ interface CatalogueTool {
11
11
  name: string;
12
12
  title: string;
13
13
  description: string;
14
- resource: string;
15
- scope: AccessScope;
14
+ group: string;
16
15
  kind: ToolKind;
17
16
  idempotent: boolean;
18
17
  openWorld: boolean;
19
18
  fields: CatalogueField[];
20
19
  }
21
- interface CatalogueScope {
22
- scope: AccessScope;
20
+ interface CataloguePermission {
21
+ permission: Permission;
23
22
  resource: string;
24
23
  action: string;
25
- permission: string | null;
26
- roles: StaffRole[];
24
+ label: string;
25
+ description: string;
26
+ about: Level;
27
+ implies: Permission[];
28
+ roles: string[];
27
29
  removes: boolean;
28
30
  }
29
31
  interface McpCatalogue {
30
32
  instructions: string;
31
- scopes: CatalogueScope[];
33
+ permissions: CataloguePermission[];
32
34
  tools: CatalogueTool[];
33
35
  }
34
36
  declare function mcpCatalogue(): McpCatalogue;
35
37
  //#endregion
36
- export { CatalogueField, CatalogueScope, CatalogueTool, McpCatalogue, mcpCatalogue };
38
+ export { CatalogueField, CataloguePermission, CatalogueTool, McpCatalogue, mcpCatalogue };
37
39
  //# sourceMappingURL=catalogue.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { toolInputSchema } from "./input-schema.js";
2
2
  import { MCP_INSTRUCTIONS } from "./server.js";
3
- import { TOOLS } from "./tools/index.js";
4
- import { ACCESS_SCOPES, SCOPE_PERMISSION, STAFF_ROLES, isDeleteScope, roleHoldsScope } from "@headless-lms/core/organizations";
3
+ import { TOOL_GROUPS } from "./tools/index.js";
4
+ import { ALL_PERMISSIONS, PERMISSIONS, defaultHolders, withImplied } from "@headless-lms/core/organizations";
5
5
  //#region src/http/mcp/catalogue.ts
6
6
  function typeOf(schema) {
7
7
  const variants = schema.anyOf ?? schema.oneOf;
@@ -27,40 +27,34 @@ function fieldsOf(tool) {
27
27
  }));
28
28
  return [...fields.filter((field) => field.required), ...fields.filter((field) => !field.required)];
29
29
  }
30
- const REMOVING_WRITE_SCOPES = [
31
- "downloads:write",
32
- "bundles:write",
33
- "access:write",
34
- "comments:moderate",
35
- "communities:write",
36
- "community_members:write",
37
- "community_moderation:write"
38
- ];
39
30
  function mcpCatalogue() {
40
31
  return {
41
32
  instructions: MCP_INSTRUCTIONS,
42
- scopes: ACCESS_SCOPES.map((scope) => {
43
- const [resource = scope, action = ""] = scope.split(":");
33
+ permissions: ALL_PERMISSIONS.map((permission) => {
34
+ const [resource = permission, action = ""] = permission.split(":");
35
+ const definition = PERMISSIONS[permission];
44
36
  return {
45
- scope,
37
+ permission,
46
38
  resource,
47
39
  action,
48
- permission: SCOPE_PERMISSION[scope],
49
- roles: STAFF_ROLES.filter((role) => roleHoldsScope(role, scope)),
50
- removes: isDeleteScope(scope) || REMOVING_WRITE_SCOPES.includes(scope)
40
+ label: definition.label,
41
+ description: definition.description,
42
+ about: definition.about,
43
+ implies: [...withImplied([permission])].filter((p) => p !== permission),
44
+ roles: defaultHolders(permission),
45
+ removes: action === "delete"
51
46
  };
52
47
  }),
53
- tools: TOOLS.map((tool) => ({
48
+ tools: TOOL_GROUPS.flatMap(({ title: group, tools }) => tools.map((tool) => ({
54
49
  name: tool.name,
55
50
  title: tool.title,
56
51
  description: tool.description,
57
- resource: tool.scope.split(":")[0] ?? tool.scope,
58
- scope: tool.scope,
52
+ group,
59
53
  kind: tool.kind,
60
54
  idempotent: tool.idempotent,
61
55
  openWorld: tool.openWorld,
62
56
  fields: fieldsOf(tool)
63
- }))
57
+ })))
64
58
  };
65
59
  }
66
60
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"catalogue.js","names":[],"sources":["../../../src/http/mcp/catalogue.ts"],"sourcesContent":["import {\n ACCESS_SCOPES,\n SCOPE_PERMISSION,\n STAFF_ROLES,\n isDeleteScope,\n roleHoldsScope,\n type AccessScope,\n type StaffRole,\n} from '@headless-lms/core/organizations';\nimport { toolInputSchema } from './input-schema.js';\nimport { MCP_INSTRUCTIONS } from './server.js';\nimport type { ToolKind } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\nexport interface CatalogueField {\n name: string;\n type: string;\n required: boolean;\n description: string | null;\n}\n\nexport interface CatalogueTool {\n name: string;\n title: string;\n description: string;\n resource: string;\n scope: AccessScope;\n kind: ToolKind;\n idempotent: boolean;\n openWorld: boolean;\n fields: CatalogueField[];\n}\n\nexport interface CatalogueScope {\n scope: AccessScope;\n resource: string;\n action: string;\n permission: string | null;\n roles: StaffRole[];\n removes: boolean;\n}\n\nexport interface McpCatalogue {\n instructions: string;\n scopes: CatalogueScope[];\n tools: CatalogueTool[];\n}\n\ntype JsonSchema = {\n type?: string | string[];\n format?: string;\n enum?: unknown[];\n const?: unknown;\n items?: JsonSchema;\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n description?: string;\n properties?: Record<string, JsonSchema>;\n required?: string[];\n};\n\nfunction typeOf(schema: JsonSchema): string {\n const variants = schema.anyOf ?? schema.oneOf;\n if (variants) {\n return [...new Set(variants.map(typeOf))].join(' | ');\n }\n if (schema.enum) {\n return schema.enum.map((value) => JSON.stringify(value)).join(' | ');\n }\n if (schema.const !== undefined) {\n return JSON.stringify(schema.const);\n }\n if (schema.type === 'array') {\n return `${schema.items ? typeOf(schema.items) : 'unknown'}[]`;\n }\n if (schema.format === 'date-time') {\n return 'date-time';\n }\n if (Array.isArray(schema.type)) {\n return schema.type.join(' | ');\n }\n return schema.type ?? 'unknown';\n}\n\nfunction descriptionOf(schema: JsonSchema): string | null {\n return (\n schema.description ??\n (schema.anyOf ?? schema.oneOf)?.find((variant) => variant.description)?.description ??\n null\n );\n}\n\nfunction fieldsOf(tool: (typeof TOOLS)[number]): CatalogueField[] {\n const json = toolInputSchema(tool.input)['~standard'].jsonSchema.input({\n target: 'draft-2020-12',\n }) as JsonSchema;\n const required = new Set(json.required ?? []);\n const fields = Object.entries(json.properties ?? {}).map(([name, schema]) => ({\n name,\n type: typeOf(schema),\n required: required.has(name),\n description: descriptionOf(schema),\n }));\n return [\n ...fields.filter((field) => field.required),\n ...fields.filter((field) => !field.required),\n ];\n}\n\nconst REMOVING_WRITE_SCOPES: readonly AccessScope[] = [\n 'downloads:write',\n 'bundles:write',\n 'access:write',\n 'comments:moderate',\n 'communities:write',\n 'community_members:write',\n 'community_moderation:write',\n];\n\nexport function mcpCatalogue(): McpCatalogue {\n return {\n instructions: MCP_INSTRUCTIONS,\n scopes: ACCESS_SCOPES.map((scope) => {\n const [resource = scope, action = ''] = scope.split(':');\n return {\n scope,\n resource,\n action,\n permission: SCOPE_PERMISSION[scope],\n roles: STAFF_ROLES.filter((role) => roleHoldsScope(role, scope)),\n removes: isDeleteScope(scope) || REMOVING_WRITE_SCOPES.includes(scope),\n };\n }),\n tools: TOOLS.map((tool) => ({\n name: tool.name,\n title: tool.title,\n description: tool.description,\n resource: tool.scope.split(':')[0] ?? tool.scope,\n scope: tool.scope,\n kind: tool.kind,\n idempotent: tool.idempotent,\n openWorld: tool.openWorld,\n fields: fieldsOf(tool),\n })),\n };\n}\n"],"mappings":";;;;;AA6DA,SAAS,OAAO,QAA4B;CAC1C,MAAM,WAAW,OAAO,SAAS,OAAO;CACxC,IAAI,UACF,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;CAEtD,IAAI,OAAO,MACT,OAAO,OAAO,KAAK,KAAK,UAAU,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK;CAErE,IAAI,OAAO,UAAU,KAAA,GACnB,OAAO,KAAK,UAAU,OAAO,KAAK;CAEpC,IAAI,OAAO,SAAS,SAClB,OAAO,GAAG,OAAO,QAAQ,OAAO,OAAO,KAAK,IAAI,UAAU;CAE5D,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,IAAI,MAAM,QAAQ,OAAO,IAAI,GAC3B,OAAO,OAAO,KAAK,KAAK,KAAK;CAE/B,OAAO,OAAO,QAAQ;AACxB;AAEA,SAAS,cAAc,QAAmC;CACxD,OACE,OAAO,gBACN,OAAO,SAAS,OAAO,MAAA,EAAQ,MAAM,YAAY,QAAQ,WAAW,CAAC,EAAE,eACxE;AAEJ;AAEA,SAAS,SAAS,MAAgD;CAChE,MAAM,OAAO,gBAAgB,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC,WAAW,MAAM,EACrE,QAAQ,gBACV,CAAC;CACD,MAAM,WAAW,IAAI,IAAI,KAAK,YAAY,CAAC,CAAC;CAC5C,MAAM,SAAS,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,aAAa;EAC5E;EACA,MAAM,OAAO,MAAM;EACnB,UAAU,SAAS,IAAI,IAAI;EAC3B,aAAa,cAAc,MAAM;CACnC,EAAE;CACF,OAAO,CACL,GAAG,OAAO,QAAQ,UAAU,MAAM,QAAQ,GAC1C,GAAG,OAAO,QAAQ,UAAU,CAAC,MAAM,QAAQ,CAC7C;AACF;AAEA,MAAM,wBAAgD;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAgB,eAA6B;CAC3C,OAAO;EACL,cAAc;EACd,QAAQ,cAAc,KAAK,UAAU;GACnC,MAAM,CAAC,WAAW,OAAO,SAAS,MAAM,MAAM,MAAM,GAAG;GACvD,OAAO;IACL;IACA;IACA;IACA,YAAY,iBAAiB;IAC7B,OAAO,YAAY,QAAQ,SAAS,eAAe,MAAM,KAAK,CAAC;IAC/D,SAAS,cAAc,KAAK,KAAK,sBAAsB,SAAS,KAAK;GACvE;EACF,CAAC;EACD,OAAO,MAAM,KAAK,UAAU;GAC1B,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,aAAa,KAAK;GAClB,UAAU,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,KAAK;GAC3C,OAAO,KAAK;GACZ,MAAM,KAAK;GACX,YAAY,KAAK;GACjB,WAAW,KAAK;GAChB,QAAQ,SAAS,IAAI;EACvB,EAAE;CACJ;AACF"}
1
+ {"version":3,"file":"catalogue.js","names":[],"sources":["../../../src/http/mcp/catalogue.ts"],"sourcesContent":["import {\n ALL_PERMISSIONS,\n defaultHolders,\n PERMISSIONS,\n withImplied,\n} from '@headless-lms/core/organizations';\nimport type { Level, Permission } from '@headless-lms/core/types';\nimport { toolInputSchema } from './input-schema.js';\nimport { MCP_INSTRUCTIONS } from './server.js';\nimport type { Tool, ToolKind } from './tool.js';\nimport { TOOL_GROUPS } from './tools/index.js';\n\nexport interface CatalogueField {\n name: string;\n type: string;\n required: boolean;\n description: string | null;\n}\n\nexport interface CatalogueTool {\n name: string;\n title: string;\n description: string;\n group: string;\n kind: ToolKind;\n idempotent: boolean;\n openWorld: boolean;\n fields: CatalogueField[];\n}\n\nexport interface CataloguePermission {\n permission: Permission;\n resource: string;\n action: string;\n label: string;\n description: string;\n about: Level;\n implies: Permission[];\n roles: string[];\n removes: boolean;\n}\n\nexport interface McpCatalogue {\n instructions: string;\n permissions: CataloguePermission[];\n tools: CatalogueTool[];\n}\n\ntype JsonSchema = {\n type?: string | string[];\n format?: string;\n enum?: unknown[];\n const?: unknown;\n items?: JsonSchema;\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n description?: string;\n properties?: Record<string, JsonSchema>;\n required?: string[];\n};\n\nfunction typeOf(schema: JsonSchema): string {\n const variants = schema.anyOf ?? schema.oneOf;\n if (variants) {\n return [...new Set(variants.map(typeOf))].join(' | ');\n }\n if (schema.enum) {\n return schema.enum.map((value) => JSON.stringify(value)).join(' | ');\n }\n if (schema.const !== undefined) {\n return JSON.stringify(schema.const);\n }\n if (schema.type === 'array') {\n return `${schema.items ? typeOf(schema.items) : 'unknown'}[]`;\n }\n if (schema.format === 'date-time') {\n return 'date-time';\n }\n if (Array.isArray(schema.type)) {\n return schema.type.join(' | ');\n }\n return schema.type ?? 'unknown';\n}\n\nfunction descriptionOf(schema: JsonSchema): string | null {\n return (\n schema.description ??\n (schema.anyOf ?? schema.oneOf)?.find((variant) => variant.description)?.description ??\n null\n );\n}\n\nfunction fieldsOf(tool: Tool): CatalogueField[] {\n const json = toolInputSchema(tool.input)['~standard'].jsonSchema.input({\n target: 'draft-2020-12',\n }) as JsonSchema;\n const required = new Set(json.required ?? []);\n const fields = Object.entries(json.properties ?? {}).map(([name, schema]) => ({\n name,\n type: typeOf(schema),\n required: required.has(name),\n description: descriptionOf(schema),\n }));\n return [\n ...fields.filter((field) => field.required),\n ...fields.filter((field) => !field.required),\n ];\n}\n\nexport function mcpCatalogue(): McpCatalogue {\n return {\n instructions: MCP_INSTRUCTIONS,\n permissions: ALL_PERMISSIONS.map((permission) => {\n const [resource = permission, action = ''] = permission.split(':');\n const definition = PERMISSIONS[permission];\n return {\n permission,\n resource,\n action,\n label: definition.label,\n description: definition.description,\n about: definition.about,\n implies: [...withImplied([permission])].filter((p) => p !== permission),\n roles: defaultHolders(permission),\n removes: action === 'delete',\n };\n }),\n tools: TOOL_GROUPS.flatMap(({ title: group, tools }) =>\n tools.map((tool) => ({\n name: tool.name,\n title: tool.title,\n description: tool.description,\n group,\n kind: tool.kind,\n idempotent: tool.idempotent,\n openWorld: tool.openWorld,\n fields: fieldsOf(tool),\n })),\n ),\n };\n}\n"],"mappings":";;;;;AA6DA,SAAS,OAAO,QAA4B;CAC1C,MAAM,WAAW,OAAO,SAAS,OAAO;CACxC,IAAI,UACF,OAAO,CAAC,GAAG,IAAI,IAAI,SAAS,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK;CAEtD,IAAI,OAAO,MACT,OAAO,OAAO,KAAK,KAAK,UAAU,KAAK,UAAU,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK;CAErE,IAAI,OAAO,UAAU,KAAA,GACnB,OAAO,KAAK,UAAU,OAAO,KAAK;CAEpC,IAAI,OAAO,SAAS,SAClB,OAAO,GAAG,OAAO,QAAQ,OAAO,OAAO,KAAK,IAAI,UAAU;CAE5D,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,IAAI,MAAM,QAAQ,OAAO,IAAI,GAC3B,OAAO,OAAO,KAAK,KAAK,KAAK;CAE/B,OAAO,OAAO,QAAQ;AACxB;AAEA,SAAS,cAAc,QAAmC;CACxD,OACE,OAAO,gBACN,OAAO,SAAS,OAAO,MAAA,EAAQ,MAAM,YAAY,QAAQ,WAAW,CAAC,EAAE,eACxE;AAEJ;AAEA,SAAS,SAAS,MAA8B;CAC9C,MAAM,OAAO,gBAAgB,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC,WAAW,MAAM,EACrE,QAAQ,gBACV,CAAC;CACD,MAAM,WAAW,IAAI,IAAI,KAAK,YAAY,CAAC,CAAC;CAC5C,MAAM,SAAS,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,aAAa;EAC5E;EACA,MAAM,OAAO,MAAM;EACnB,UAAU,SAAS,IAAI,IAAI;EAC3B,aAAa,cAAc,MAAM;CACnC,EAAE;CACF,OAAO,CACL,GAAG,OAAO,QAAQ,UAAU,MAAM,QAAQ,GAC1C,GAAG,OAAO,QAAQ,UAAU,CAAC,MAAM,QAAQ,CAC7C;AACF;AAEA,SAAgB,eAA6B;CAC3C,OAAO;EACL,cAAc;EACd,aAAa,gBAAgB,KAAK,eAAe;GAC/C,MAAM,CAAC,WAAW,YAAY,SAAS,MAAM,WAAW,MAAM,GAAG;GACjE,MAAM,aAAa,YAAY;GAC/B,OAAO;IACL;IACA;IACA;IACA,OAAO,WAAW;IAClB,aAAa,WAAW;IACxB,OAAO,WAAW;IAClB,SAAS,CAAC,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,MAAM,MAAM,UAAU;IACtE,OAAO,eAAe,UAAU;IAChC,SAAS,WAAW;GACtB;EACF,CAAC;EACD,OAAO,YAAY,SAAS,EAAE,OAAO,OAAO,YAC1C,MAAM,KAAK,UAAU;GACnB,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,aAAa,KAAK;GAClB;GACA,MAAM,KAAK;GACX,YAAY,KAAK;GACjB,WAAW,KAAK;GAChB,QAAQ,SAAS,IAAI;EACvB,EAAE,CACJ;CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { ACCESS_SCOPES } from "@headless-lms/core/organizations";
1
+ import { ALL_PERMISSIONS } from "@headless-lms/core/organizations";
2
2
  //#region src/http/mcp/challenge.ts
3
3
  function unauthorized(mcp) {
4
4
  return new Response(JSON.stringify({
@@ -12,7 +12,7 @@ function unauthorized(mcp) {
12
12
  status: 401,
13
13
  headers: {
14
14
  "content-type": "application/json",
15
- "www-authenticate": `Bearer resource_metadata="${mcp.resourceMetadataUrl}", scope="${ACCESS_SCOPES.join(" ")}"`
15
+ "www-authenticate": `Bearer resource_metadata="${mcp.resourceMetadataUrl}", scope="${ALL_PERMISSIONS.join(" ")}"`
16
16
  }
17
17
  });
18
18
  }
@@ -21,7 +21,7 @@ function forbidden() {
21
21
  jsonrpc: "2.0",
22
22
  error: {
23
23
  code: -32e3,
24
- message: "Only staff of this organization can use this server"
24
+ message: "Only active members of this organization can use this server"
25
25
  },
26
26
  id: null
27
27
  }), {
@@ -1 +1 @@
1
- {"version":3,"file":"challenge.js","names":[],"sources":["../../../src/http/mcp/challenge.ts"],"sourcesContent":["import { ACCESS_SCOPES } from '@headless-lms/core/organizations';\nimport type { McpEndpoints } from '../../app/mcp-endpoints.js';\n\nexport function unauthorized(mcp: McpEndpoints): Response {\n const body = { jsonrpc: '2.0', error: { code: -32000, message: 'Unauthorized' }, id: null };\n return new Response(JSON.stringify(body), {\n status: 401,\n headers: {\n 'content-type': 'application/json',\n 'www-authenticate': `Bearer resource_metadata=\"${mcp.resourceMetadataUrl}\", scope=\"${ACCESS_SCOPES.join(' ')}\"`,\n },\n });\n}\n\nexport function forbidden(): Response {\n const body = {\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Only staff of this organization can use this server' },\n id: null,\n };\n return new Response(JSON.stringify(body), {\n status: 403,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":";;AAGA,SAAgB,aAAa,KAA6B;CAExD,OAAO,IAAI,SAAS,KAAK,UAAU;EADpB,SAAS;EAAO,OAAO;GAAE,MAAM;GAAQ,SAAS;EAAe;EAAG,IAAI;CAC/C,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,oBAAoB,6BAA6B,IAAI,oBAAoB,YAAY,cAAc,KAAK,GAAG,EAAE;EAC/G;CACF,CAAC;AACH;AAEA,SAAgB,YAAsB;CAMpC,OAAO,IAAI,SAAS,KAAK,UAAU;EAJjC,SAAS;EACT,OAAO;GAAE,MAAM;GAAQ,SAAS;EAAsD;EACtF,IAAI;CAEgC,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
1
+ {"version":3,"file":"challenge.js","names":[],"sources":["../../../src/http/mcp/challenge.ts"],"sourcesContent":["import { ALL_PERMISSIONS } from '@headless-lms/core/organizations';\nimport type { McpEndpoints } from '../../app/mcp-endpoints.js';\n\nexport function unauthorized(mcp: McpEndpoints): Response {\n const body = { jsonrpc: '2.0', error: { code: -32000, message: 'Unauthorized' }, id: null };\n return new Response(JSON.stringify(body), {\n status: 401,\n headers: {\n 'content-type': 'application/json',\n 'www-authenticate': `Bearer resource_metadata=\"${mcp.resourceMetadataUrl}\", scope=\"${ALL_PERMISSIONS.join(' ')}\"`,\n },\n });\n}\n\nexport function forbidden(): Response {\n const body = {\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Only active members of this organization can use this server',\n },\n id: null,\n };\n return new Response(JSON.stringify(body), {\n status: 403,\n headers: { 'content-type': 'application/json' },\n });\n}\n"],"mappings":";;AAGA,SAAgB,aAAa,KAA6B;CAExD,OAAO,IAAI,SAAS,KAAK,UAAU;EADpB,SAAS;EAAO,OAAO;GAAE,MAAM;GAAQ,SAAS;EAAe;EAAG,IAAI;CAC/C,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,oBAAoB,6BAA6B,IAAI,oBAAoB,YAAY,gBAAgB,KAAK,GAAG,EAAE;EACjH;CACF,CAAC;AACH;AAEA,SAAgB,YAAsB;CASpC,OAAO,IAAI,SAAS,KAAK,UAAU;EAPjC,SAAS;EACT,OAAO;GACL,MAAM;GACN,SAAS;EACX;EACA,IAAI;CAEgC,CAAC,GAAG;EACxC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC;AACH"}
@@ -1,8 +1,8 @@
1
- import { NoActiveOrgError, resolveScopeFor } from "../scope.js";
1
+ import { NoActiveOrgError, resolveCallerFor } from "../scope.js";
2
2
  import { forbidden, unauthorized } from "./challenge.js";
3
3
  import { createToolServer } from "./server.js";
4
4
  import { TOOLS } from "./tools/index.js";
5
- import { roleHoldsScope } from "@headless-lms/core/organizations";
5
+ import { isPermission } from "@headless-lms/core/organizations";
6
6
  import { createMcpHandler } from "@modelcontextprotocol/server";
7
7
  import { toWebRequest } from "@modelcontextprotocol/node";
8
8
  //#region src/http/mcp/route.ts
@@ -29,9 +29,9 @@ function registerMcp(app, container, tools = TOOLS) {
29
29
  headers: request.headers
30
30
  });
31
31
  if (!grant) return reply.send(unauthorized(container.mcp));
32
- let scope;
32
+ let caller;
33
33
  try {
34
- scope = await resolveScopeFor(container, grant.userId, grant.orgId);
34
+ caller = await resolveCallerFor(container, grant.userId, grant.orgId, "roleKey" in grant.limit ? grant.limit : { scopes: grant.limit.scopes.filter(isPermission) });
35
35
  } catch (error) {
36
36
  if (error instanceof NoActiveOrgError) return reply.send(forbidden());
37
37
  throw error;
@@ -39,16 +39,16 @@ function registerMcp(app, container, tools = TOOLS) {
39
39
  const connection = {
40
40
  context: {
41
41
  container,
42
- scope,
42
+ caller,
43
43
  requestId: request.id
44
44
  },
45
- tools: tools.filter((tool) => grant.scopes.includes(tool.scope) && roleHoldsScope(scope.role, tool.scope))
45
+ tools
46
46
  };
47
47
  const response = await handler.fetch(await webRequest(request, reply), {
48
48
  authInfo: {
49
49
  token: "",
50
50
  clientId: grant.clientId,
51
- scopes: grant.scopes,
51
+ scopes: "scopes" in grant.limit ? grant.limit.scopes : [],
52
52
  extra: { connection }
53
53
  },
54
54
  parsedBody: request.body
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","names":[],"sources":["../../../src/http/mcp/route.ts"],"sourcesContent":["import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport { createMcpHandler } from '@modelcontextprotocol/server';\nimport { toWebRequest } from '@modelcontextprotocol/node';\nimport { roleHoldsScope } from '@headless-lms/core/organizations';\nimport type { Container } from '../../app/container.js';\nimport { NoActiveOrgError, resolveScopeFor, type OrgScope } from '../scope.js';\nimport { forbidden, unauthorized } from './challenge.js';\nimport { createToolServer } from './server.js';\nimport type { Tool, ToolContext } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\ntype Connection = { context: ToolContext; tools: Tool[] };\n\nexport function registerMcp(\n app: FastifyInstance,\n container: Container,\n tools: readonly Tool[] = TOOLS,\n): void {\n const handler = createMcpHandler(\n (ctx) => {\n const connection = ctx.authInfo?.extra?.connection as Connection | undefined;\n if (!connection) {\n throw new Error('mcp connection missing');\n }\n return createToolServer(connection.context, connection.tools);\n },\n {\n legacy: 'stateless',\n onerror: (err) => container.logger.error('mcp handler error', { err }),\n },\n );\n\n const webRequest = async (request: FastifyRequest, reply: FastifyReply) => {\n const abort = new AbortController();\n reply.raw.on('close', () => {\n if (!reply.raw.writableFinished) {\n abort.abort();\n }\n });\n return toWebRequest(request.raw, request.body, { signal: abort.signal });\n };\n\n app.post('/mcp', { schema: { hide: true } }, async (request, reply) => {\n const grant = await container.auth.verifyAccess({\n method: request.method,\n url: container.mcp.resource,\n headers: request.headers,\n });\n if (!grant) {\n return reply.send(unauthorized(container.mcp));\n }\n let scope: OrgScope;\n try {\n scope = await resolveScopeFor(container, grant.userId, grant.orgId);\n } catch (error) {\n if (error instanceof NoActiveOrgError) {\n return reply.send(forbidden());\n }\n throw error;\n }\n const connection: Connection = {\n context: { container, scope, requestId: request.id },\n tools: tools.filter(\n (tool) => grant.scopes.includes(tool.scope) && roleHoldsScope(scope.role, tool.scope),\n ),\n };\n const response = await handler.fetch(await webRequest(request, reply), {\n authInfo: {\n token: '',\n clientId: grant.clientId,\n scopes: grant.scopes,\n extra: { connection },\n },\n parsedBody: request.body,\n });\n return reply.send(response);\n });\n\n app.route({\n method: ['GET', 'DELETE'],\n url: '/mcp',\n schema: { hide: true },\n handler: async (_request, reply) =>\n reply\n .code(405)\n .header('allow', 'POST')\n .header('content-type', 'application/json')\n .send({\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Method not allowed.' },\n id: null,\n }),\n });\n}\n"],"mappings":";;;;;;;;AAaA,SAAgB,YACd,KACA,WACA,QAAyB,OACnB;CACN,MAAM,UAAU,kBACb,QAAQ;EACP,MAAM,aAAa,IAAI,UAAU,OAAO;EACxC,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,wBAAwB;EAE1C,OAAO,iBAAiB,WAAW,SAAS,WAAW,KAAK;CAC9D,GACA;EACE,QAAQ;EACR,UAAU,QAAQ,UAAU,OAAO,MAAM,qBAAqB,EAAE,IAAI,CAAC;CACvE,CACF;CAEA,MAAM,aAAa,OAAO,SAAyB,UAAwB;EACzE,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,IAAI,GAAG,eAAe;GAC1B,IAAI,CAAC,MAAM,IAAI,kBACb,MAAM,MAAM;EAEhB,CAAC;EACD,OAAO,aAAa,QAAQ,KAAK,QAAQ,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;CACzE;CAEA,IAAI,KAAK,QAAQ,EAAE,QAAQ,EAAE,MAAM,KAAK,EAAE,GAAG,OAAO,SAAS,UAAU;EACrE,MAAM,QAAQ,MAAM,UAAU,KAAK,aAAa;GAC9C,QAAQ,QAAQ;GAChB,KAAK,UAAU,IAAI;GACnB,SAAS,QAAQ;EACnB,CAAC;EACD,IAAI,CAAC,OACH,OAAO,MAAM,KAAK,aAAa,UAAU,GAAG,CAAC;EAE/C,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,gBAAgB,WAAW,MAAM,QAAQ,MAAM,KAAK;EACpE,SAAS,OAAO;GACd,IAAI,iBAAiB,kBACnB,OAAO,MAAM,KAAK,UAAU,CAAC;GAE/B,MAAM;EACR;EACA,MAAM,aAAyB;GAC7B,SAAS;IAAE;IAAW;IAAO,WAAW,QAAQ;GAAG;GACnD,OAAO,MAAM,QACV,SAAS,MAAM,OAAO,SAAS,KAAK,KAAK,KAAK,eAAe,MAAM,MAAM,KAAK,KAAK,CACtF;EACF;EACA,MAAM,WAAW,MAAM,QAAQ,MAAM,MAAM,WAAW,SAAS,KAAK,GAAG;GACrE,UAAU;IACR,OAAO;IACP,UAAU,MAAM;IAChB,QAAQ,MAAM;IACd,OAAO,EAAE,WAAW;GACtB;GACA,YAAY,QAAQ;EACtB,CAAC;EACD,OAAO,MAAM,KAAK,QAAQ;CAC5B,CAAC;CAED,IAAI,MAAM;EACR,QAAQ,CAAC,OAAO,QAAQ;EACxB,KAAK;EACL,QAAQ,EAAE,MAAM,KAAK;EACrB,SAAS,OAAO,UAAU,UACxB,MACG,KAAK,GAAG,CAAC,CACT,OAAO,SAAS,MAAM,CAAC,CACvB,OAAO,gBAAgB,kBAAkB,CAAC,CAC1C,KAAK;GACJ,SAAS;GACT,OAAO;IAAE,MAAM;IAAQ,SAAS;GAAsB;GACtD,IAAI;EACN,CAAC;CACP,CAAC;AACH"}
1
+ {"version":3,"file":"route.js","names":[],"sources":["../../../src/http/mcp/route.ts"],"sourcesContent":["import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';\nimport { createMcpHandler } from '@modelcontextprotocol/server';\nimport { toWebRequest } from '@modelcontextprotocol/node';\nimport { isPermission } from '@headless-lms/core/organizations';\nimport type { Container } from '../../app/container.js';\nimport { NoActiveOrgError, resolveCallerFor, type Caller } from '../scope.js';\nimport { forbidden, unauthorized } from './challenge.js';\nimport { createToolServer } from './server.js';\nimport type { Tool, ToolContext } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\ntype Connection = { context: ToolContext; tools: readonly Tool[] };\n\nexport function registerMcp(\n app: FastifyInstance,\n container: Container,\n tools: readonly Tool[] = TOOLS,\n): void {\n const handler = createMcpHandler(\n (ctx) => {\n const connection = ctx.authInfo?.extra?.connection as Connection | undefined;\n if (!connection) {\n throw new Error('mcp connection missing');\n }\n return createToolServer(connection.context, connection.tools);\n },\n {\n legacy: 'stateless',\n onerror: (err) => container.logger.error('mcp handler error', { err }),\n },\n );\n\n const webRequest = async (request: FastifyRequest, reply: FastifyReply) => {\n const abort = new AbortController();\n reply.raw.on('close', () => {\n if (!reply.raw.writableFinished) {\n abort.abort();\n }\n });\n return toWebRequest(request.raw, request.body, { signal: abort.signal });\n };\n\n app.post('/mcp', { schema: { hide: true } }, async (request, reply) => {\n const grant = await container.auth.verifyAccess({\n method: request.method,\n url: container.mcp.resource,\n headers: request.headers,\n });\n if (!grant) {\n return reply.send(unauthorized(container.mcp));\n }\n let caller: Caller;\n try {\n caller = await resolveCallerFor(\n container,\n grant.userId,\n grant.orgId,\n 'roleKey' in grant.limit\n ? grant.limit\n : { scopes: grant.limit.scopes.filter(isPermission) },\n );\n } catch (error) {\n if (error instanceof NoActiveOrgError) {\n return reply.send(forbidden());\n }\n throw error;\n }\n const connection: Connection = {\n context: { container, caller, requestId: request.id },\n tools,\n };\n const response = await handler.fetch(await webRequest(request, reply), {\n authInfo: {\n token: '',\n clientId: grant.clientId,\n scopes: 'scopes' in grant.limit ? grant.limit.scopes : [],\n extra: { connection },\n },\n parsedBody: request.body,\n });\n return reply.send(response);\n });\n\n app.route({\n method: ['GET', 'DELETE'],\n url: '/mcp',\n schema: { hide: true },\n handler: async (_request, reply) =>\n reply\n .code(405)\n .header('allow', 'POST')\n .header('content-type', 'application/json')\n .send({\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Method not allowed.' },\n id: null,\n }),\n });\n}\n"],"mappings":";;;;;;;;AAaA,SAAgB,YACd,KACA,WACA,QAAyB,OACnB;CACN,MAAM,UAAU,kBACb,QAAQ;EACP,MAAM,aAAa,IAAI,UAAU,OAAO;EACxC,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,wBAAwB;EAE1C,OAAO,iBAAiB,WAAW,SAAS,WAAW,KAAK;CAC9D,GACA;EACE,QAAQ;EACR,UAAU,QAAQ,UAAU,OAAO,MAAM,qBAAqB,EAAE,IAAI,CAAC;CACvE,CACF;CAEA,MAAM,aAAa,OAAO,SAAyB,UAAwB;EACzE,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,IAAI,GAAG,eAAe;GAC1B,IAAI,CAAC,MAAM,IAAI,kBACb,MAAM,MAAM;EAEhB,CAAC;EACD,OAAO,aAAa,QAAQ,KAAK,QAAQ,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;CACzE;CAEA,IAAI,KAAK,QAAQ,EAAE,QAAQ,EAAE,MAAM,KAAK,EAAE,GAAG,OAAO,SAAS,UAAU;EACrE,MAAM,QAAQ,MAAM,UAAU,KAAK,aAAa;GAC9C,QAAQ,QAAQ;GAChB,KAAK,UAAU,IAAI;GACnB,SAAS,QAAQ;EACnB,CAAC;EACD,IAAI,CAAC,OACH,OAAO,MAAM,KAAK,aAAa,UAAU,GAAG,CAAC;EAE/C,IAAI;EACJ,IAAI;GACF,SAAS,MAAM,iBACb,WACA,MAAM,QACN,MAAM,OACN,aAAa,MAAM,QACf,MAAM,QACN,EAAE,QAAQ,MAAM,MAAM,OAAO,OAAO,YAAY,EAAE,CACxD;EACF,SAAS,OAAO;GACd,IAAI,iBAAiB,kBACnB,OAAO,MAAM,KAAK,UAAU,CAAC;GAE/B,MAAM;EACR;EACA,MAAM,aAAyB;GAC7B,SAAS;IAAE;IAAW;IAAQ,WAAW,QAAQ;GAAG;GACpD;EACF;EACA,MAAM,WAAW,MAAM,QAAQ,MAAM,MAAM,WAAW,SAAS,KAAK,GAAG;GACrE,UAAU;IACR,OAAO;IACP,UAAU,MAAM;IAChB,QAAQ,YAAY,MAAM,QAAQ,MAAM,MAAM,SAAS,CAAC;IACxD,OAAO,EAAE,WAAW;GACtB;GACA,YAAY,QAAQ;EACtB,CAAC;EACD,OAAO,MAAM,KAAK,QAAQ;CAC5B,CAAC;CAED,IAAI,MAAM;EACR,QAAQ,CAAC,OAAO,QAAQ;EACxB,KAAK;EACL,QAAQ,EAAE,MAAM,KAAK;EACrB,SAAS,OAAO,UAAU,UACxB,MACG,KAAK,GAAG,CAAC,CACT,OAAO,SAAS,MAAM,CAAC,CACvB,OAAO,gBAAgB,kBAAkB,CAAC,CAC1C,KAAK;GACJ,SAAS;GACT,OAAO;IAAE,MAAM;IAAQ,SAAS;GAAsB;GACtD,IAAI;EACN,CAAC;CACP,CAAC;AACH"}
@@ -1,6 +1,6 @@
1
1
  import { Container } from "../../app/container.js";
2
2
  import { Tool, ToolContext, ToolKind } from "./tool.js";
3
- import { AccessScope, StaffRole } from "@headless-lms/core/organizations";
3
+ import { Actor } from "@headless-lms/core/shared/actor";
4
4
  //#region src/http/mcp/test-support.d.ts
5
5
  declare const ORG: {
6
6
  id: string;
@@ -19,10 +19,11 @@ declare const ORG_USER: {
19
19
  id: string;
20
20
  orgId: string;
21
21
  userId: string;
22
- role: string;
22
+ status: string;
23
23
  };
24
+ declare const OWNER: Actor;
24
25
  declare function fakeContainer(overrides?: Record<string, unknown>): Container;
25
- declare function toolContext(container: Container, role?: StaffRole): ToolContext;
26
+ declare function toolContext(container: Container, actor?: Actor): ToolContext;
26
27
  declare function listTools(ctx: ToolContext, tools?: readonly Tool[]): Promise<{
27
28
  name: string;
28
29
  annotations?: Record<string, unknown>;
@@ -39,7 +40,7 @@ declare function callTool(name: string, args: unknown, ctx: ToolContext, tools?:
39
40
  text?: string;
40
41
  }>;
41
42
  }>;
42
- type ToolSpec = [name: string, scope: AccessScope, kind: ToolKind, idempotent: boolean, openWorld: boolean];
43
+ type ToolSpec = [name: string, kind: ToolKind, idempotent: boolean, openWorld: boolean];
43
44
  declare function toolSpecs(tools: readonly Tool[]): ToolSpec[];
44
45
  type ToolCase = {
45
46
  tool: string;
@@ -51,5 +52,5 @@ type ToolCase = {
51
52
  };
52
53
  declare function runCase(tools: readonly Tool[], c: ToolCase): Promise<void>;
53
54
  //#endregion
54
- export { ORG, ORG_USER, ToolCase, ToolSpec, USER, callTool, fakeContainer, listTools, runCase, toolContext, toolSpecs };
55
+ export { ORG, ORG_USER, OWNER, ToolCase, ToolSpec, USER, callTool, fakeContainer, listTools, runCase, toolContext, toolSpecs };
55
56
  //# sourceMappingURL=test-support.d.ts.map
@@ -3,7 +3,7 @@ import { toToolResult } from "./tool.js";
3
3
  import { createToolServer } from "./server.js";
4
4
  import { TOOLS } from "./tools/index.js";
5
5
  import { a as globalExpect } from "../../node_modules/.pnpm/vitest@2.1.9_@types_node@22.20.0_jsdom@25.0.1_lightningcss@1.32.0/node_modules/vitest/dist/chunks/vi.DgezovHB.js";
6
- import { ACCESS_SCOPES } from "@headless-lms/core/organizations";
6
+ import { ALL_PERMISSIONS, actorWith } from "@headless-lms/core/organizations";
7
7
  import { createMcpHandler } from "@modelcontextprotocol/server";
8
8
  //#region src/http/mcp/test-support.ts
9
9
  const ORG = {
@@ -23,8 +23,13 @@ const ORG_USER = {
23
23
  id: "ou_1",
24
24
  orgId: "org_1",
25
25
  userId: "usr_1",
26
- role: "owner"
26
+ status: "active"
27
27
  };
28
+ const OWNER = actorWith({
29
+ id: ORG_USER.id,
30
+ orgId: ORG.id,
31
+ owner: true
32
+ });
28
33
  function fakeContainer(overrides = {}) {
29
34
  const logger = {
30
35
  debug: () => {},
@@ -46,13 +51,29 @@ function fakeContainer(overrides = {}) {
46
51
  auth: { verifyAccess: async () => ({
47
52
  userId: USER.id,
48
53
  orgId: ORG.id,
49
- scopes: [...ACCESS_SCOPES],
50
- clientId: "client_1"
54
+ clientId: "client_1",
55
+ limit: { scopes: [...ALL_PERMISSIONS] }
51
56
  }) },
52
57
  organizations: {
53
58
  getById: async (id) => id === ORG.id ? ORG : null,
54
59
  getByExternalId: async (id) => id === ORG.externalId ? ORG : null,
55
- getOrgUser: async (orgId, userId) => orgId === ORG.id && userId === USER.id ? ORG_USER : null
60
+ getOrgUser: async (orgId, userId) => orgId === ORG.id && userId === USER.id ? ORG_USER : null,
61
+ actorFor: async (orgId, orgUserId) => orgId === ORG.id && orgUserId === ORG_USER.id ? OWNER : null,
62
+ resolveCaller: async (authOrgId, authUserId, limit) => {
63
+ const orgs = base.organizations;
64
+ const people = base.identity;
65
+ const org = await orgs.getByExternalId(authOrgId);
66
+ if (!org) return null;
67
+ const user = await people.getUserByExternalId(authUserId);
68
+ const orgUser = user ? await orgs.getOrgUser(org.id, user.id) : null;
69
+ const actor = orgUser ? await orgs.actorFor(org.id, orgUser.id, limit) : null;
70
+ return {
71
+ orgId: org.id,
72
+ userId: user?.id ?? null,
73
+ orgUserId: orgUser?.id ?? null,
74
+ actor
75
+ };
76
+ }
56
77
  },
57
78
  identity: {
58
79
  getUserByExternalId: async (id) => id === USER.id ? USER : null,
@@ -68,15 +89,16 @@ function fakeContainer(overrides = {}) {
68
89
  }
69
90
  return base;
70
91
  }
71
- function toolContext(container, role = "owner") {
92
+ function toolContext(container, actor = OWNER) {
72
93
  return {
73
94
  container,
74
- scope: {
95
+ caller: {
96
+ actor,
75
97
  orgId: ORG.id,
76
98
  userId: USER.id,
77
99
  orgUserId: ORG_USER.id,
78
- role,
79
- authOrgId: ORG.id
100
+ authOrgId: ORG.id,
101
+ authUserId: USER.id
80
102
  },
81
103
  requestId: "req_test"
82
104
  };
@@ -121,7 +143,6 @@ async function callTool(name, args, ctx, tools = TOOLS) {
121
143
  function toolSpecs(tools) {
122
144
  return tools.map((tool) => [
123
145
  tool.name,
124
- tool.scope,
125
146
  tool.kind,
126
147
  tool.idempotent,
127
148
  tool.openWorld
@@ -146,6 +167,6 @@ async function runCase(tools, c) {
146
167
  globalExpect(result.structuredContent).toEqual(toToolResult("shown" in c ? c.shown : returns).structuredContent);
147
168
  }
148
169
  //#endregion
149
- export { ORG, ORG_USER, USER, callTool, fakeContainer, listTools, runCase, toolContext, toolSpecs };
170
+ export { ORG, ORG_USER, OWNER, USER, callTool, fakeContainer, listTools, runCase, toolContext, toolSpecs };
150
171
 
151
172
  //# sourceMappingURL=test-support.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-support.js","names":[],"sources":["../../../src/http/mcp/test-support.ts"],"sourcesContent":["import { expect } from 'vitest';\nimport { createMcpHandler } from '@modelcontextprotocol/server';\nimport { ACCESS_SCOPES, type AccessScope, type StaffRole } from '@headless-lms/core/organizations';\nimport type { Container } from '../../app/container.js';\nimport { mcpEndpoints } from '../../app/mcp-endpoints.js';\nimport { createToolServer } from './server.js';\nimport { toToolResult, type Tool, type ToolContext, type ToolKind } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\nexport const ORG = { id: 'org_1', name: 'Acme', slug: 'acme', externalId: 'org_1' };\nexport const USER = {\n id: 'usr_1',\n email: 'owner@acme.test',\n firstName: 'Ada',\n lastName: 'Lovelace',\n externalId: 'usr_1',\n};\nexport const ORG_USER = { id: 'ou_1', orgId: 'org_1', userId: 'usr_1', role: 'owner' };\n\nexport function fakeContainer(overrides: Record<string, unknown> = {}): Container {\n const logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => logger,\n };\n const base: Record<string, unknown> = {\n mcp: mcpEndpoints({\n authBaseURL: 'http://localhost:8100',\n adminAppUrl: 'http://localhost:8101',\n }),\n logger,\n requestContext: { set: () => {}, run: (_ctx: unknown, fn: () => void) => fn() },\n auth: {\n verifyAccess: async () => ({\n userId: USER.id,\n orgId: ORG.id,\n scopes: [...ACCESS_SCOPES],\n clientId: 'client_1',\n }),\n },\n organizations: {\n getById: async (id: string) => (id === ORG.id ? ORG : null),\n getByExternalId: async (id: string) => (id === ORG.externalId ? ORG : null),\n getOrgUser: async (orgId: string, userId: string) =>\n orgId === ORG.id && userId === USER.id ? ORG_USER : null,\n },\n identity: {\n getUserByExternalId: async (id: string) => (id === USER.id ? USER : null),\n getUserById: async (id: string) => (id === USER.id ? USER : null),\n },\n };\n for (const [key, value] of Object.entries(overrides)) {\n const current = base[key];\n base[key] =\n current &&\n typeof current === 'object' &&\n value &&\n typeof value === 'object' &&\n !Array.isArray(value)\n ? { ...(current as object), ...(value as object) }\n : value;\n }\n return base as unknown as Container;\n}\n\nexport function toolContext(container: Container, role: StaffRole = 'owner'): ToolContext {\n return {\n container,\n scope: { orgId: ORG.id, userId: USER.id, orgUserId: ORG_USER.id, role, authOrgId: ORG.id },\n requestId: 'req_test',\n };\n}\n\nconst MODERN_META = {\n 'io.modelcontextprotocol/protocolVersion': '2026-07-28',\n 'io.modelcontextprotocol/clientCapabilities': {},\n};\n\nasync function rpc(\n ctx: ToolContext,\n tools: readonly Tool[],\n method: string,\n params: Record<string, unknown>,\n name?: string,\n) {\n const handler = createMcpHandler(() => createToolServer(ctx, tools));\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n 'mcp-protocol-version': '2026-07-28',\n 'mcp-method': method,\n };\n if (name) {\n headers['mcp-name'] = name;\n }\n const response = await handler.fetch(\n new Request('http://localhost:8100/mcp', {\n method: 'POST',\n headers,\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method,\n params: { ...params, _meta: MODERN_META },\n }),\n }),\n );\n const body = (await response.json()) as {\n result?: Record<string, unknown>;\n error?: { message: string };\n };\n if (body.error) {\n throw new Error(body.error.message);\n }\n return body.result as Record<string, unknown>;\n}\n\nexport async function listTools(ctx: ToolContext, tools: readonly Tool[] = TOOLS) {\n const result = await rpc(ctx, tools, 'tools/list', {});\n return result.tools as Array<{\n name: string;\n annotations?: Record<string, unknown>;\n inputSchema: { type?: string; properties?: Record<string, unknown> };\n }>;\n}\n\nexport async function callTool(\n name: string,\n args: unknown,\n ctx: ToolContext,\n tools: readonly Tool[] = TOOLS,\n) {\n return (await rpc(ctx, tools, 'tools/call', { name, arguments: args }, name)) as {\n isError?: boolean;\n structuredContent?: Record<string, unknown>;\n content: Array<{ type: string; text?: string }>;\n };\n}\n\nexport type ToolSpec = [\n name: string,\n scope: AccessScope,\n kind: ToolKind,\n idempotent: boolean,\n openWorld: boolean,\n];\n\nexport function toolSpecs(tools: readonly Tool[]): ToolSpec[] {\n return tools.map((tool) => [tool.name, tool.scope, tool.kind, tool.idempotent, tool.openWorld]);\n}\n\nexport type ToolCase = {\n tool: string;\n args: Record<string, unknown>;\n service: string;\n expected: unknown[];\n returns?: unknown;\n shown?: unknown;\n};\n\nfunction serviceFake(path: string, returns: unknown, calls: unknown[][]): Record<string, unknown> {\n const keys = path.split('.');\n const method = keys.pop() ?? '';\n let node: Record<string, unknown> = {\n [method]: async (...args: unknown[]) => {\n calls.push(args);\n return returns;\n },\n };\n for (const key of keys.reverse()) {\n node = { [key]: node };\n }\n return node;\n}\n\nexport async function runCase(tools: readonly Tool[], c: ToolCase): Promise<void> {\n const calls: unknown[][] = [];\n const returns = 'returns' in c ? c.returns : { id: 'result' };\n const ctx = toolContext(fakeContainer(serviceFake(c.service, returns, calls)));\n const result = await callTool(c.tool, c.args, ctx, tools);\n expect(result.isError ?? false).toBe(false);\n expect(calls).toEqual([c.expected]);\n expect(result.structuredContent).toEqual(\n toToolResult('shown' in c ? c.shown : returns).structuredContent,\n );\n}\n"],"mappings":";;;;;;;;AASA,MAAa,MAAM;CAAE,IAAI;CAAS,MAAM;CAAQ,MAAM;CAAQ,YAAY;AAAQ;AAClF,MAAa,OAAO;CAClB,IAAI;CACJ,OAAO;CACP,WAAW;CACX,UAAU;CACV,YAAY;AACd;AACA,MAAa,WAAW;CAAE,IAAI;CAAQ,OAAO;CAAS,QAAQ;CAAS,MAAM;AAAQ;AAErF,SAAgB,cAAc,YAAqC,CAAC,GAAc;CAChF,MAAM,SAAS;EACb,aAAa,CAAC;EACd,YAAY,CAAC;EACb,YAAY,CAAC;EACb,aAAa,CAAC;EACd,aAAa;CACf;CACA,MAAM,OAAgC;EACpC,KAAK,aAAa;GAChB,aAAa;GACb,aAAa;EACf,CAAC;EACD;EACA,gBAAgB;GAAE,WAAW,CAAC;GAAG,MAAM,MAAe,OAAmB,GAAG;EAAE;EAC9E,MAAM,EACJ,cAAc,aAAa;GACzB,QAAQ,KAAK;GACb,OAAO,IAAI;GACX,QAAQ,CAAC,GAAG,aAAa;GACzB,UAAU;EACZ,GACF;EACA,eAAe;GACb,SAAS,OAAO,OAAgB,OAAO,IAAI,KAAK,MAAM;GACtD,iBAAiB,OAAO,OAAgB,OAAO,IAAI,aAAa,MAAM;GACtE,YAAY,OAAO,OAAe,WAChC,UAAU,IAAI,MAAM,WAAW,KAAK,KAAK,WAAW;EACxD;EACA,UAAU;GACR,qBAAqB,OAAO,OAAgB,OAAO,KAAK,KAAK,OAAO;GACpE,aAAa,OAAO,OAAgB,OAAO,KAAK,KAAK,OAAO;EAC9D;CACF;CACA,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,GAAG;EACpD,MAAM,UAAU,KAAK;EACrB,KAAK,OACH,WACA,OAAO,YAAY,YACnB,SACA,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,IAChB;GAAE,GAAI;GAAoB,GAAI;EAAiB,IAC/C;CACR;CACA,OAAO;AACT;AAEA,SAAgB,YAAY,WAAsB,OAAkB,SAAsB;CACxF,OAAO;EACL;EACA,OAAO;GAAE,OAAO,IAAI;GAAI,QAAQ,KAAK;GAAI,WAAW,SAAS;GAAI;GAAM,WAAW,IAAI;EAAG;EACzF,WAAW;CACb;AACF;AAEA,MAAM,cAAc;CAClB,2CAA2C;CAC3C,8CAA8C,CAAC;AACjD;AAEA,eAAe,IACb,KACA,OACA,QACA,QACA,MACA;CACA,MAAM,UAAU,uBAAuB,iBAAiB,KAAK,KAAK,CAAC;CACnE,MAAM,UAAkC;EACtC,gBAAgB;EAChB,wBAAwB;EACxB,cAAc;CAChB;CACA,IAAI,MACF,QAAQ,cAAc;CAcxB,MAAM,OAAQ,OAAM,MAZG,QAAQ,MAC7B,IAAI,QAAQ,6BAA6B;EACvC,QAAQ;EACR;EACA,MAAM,KAAK,UAAU;GACnB,SAAS;GACT,IAAI;GACJ;GACA,QAAQ;IAAE,GAAG;IAAQ,OAAO;GAAY;EAC1C,CAAC;CACH,CAAC,CACH,EAAA,CAC6B,KAAK;CAIlC,IAAI,KAAK,OACP,MAAM,IAAI,MAAM,KAAK,MAAM,OAAO;CAEpC,OAAO,KAAK;AACd;AAEA,eAAsB,UAAU,KAAkB,QAAyB,OAAO;CAEhF,QAAO,MADc,IAAI,KAAK,OAAO,cAAc,CAAC,CAAC,EAAA,CACvC;AAKhB;AAEA,eAAsB,SACpB,MACA,MACA,KACA,QAAyB,OACzB;CACA,OAAQ,MAAM,IAAI,KAAK,OAAO,cAAc;EAAE;EAAM,WAAW;CAAK,GAAG,IAAI;AAK7E;AAUA,SAAgB,UAAU,OAAoC;CAC5D,OAAO,MAAM,KAAK,SAAS;EAAC,KAAK;EAAM,KAAK;EAAO,KAAK;EAAM,KAAK;EAAY,KAAK;CAAS,CAAC;AAChG;AAWA,SAAS,YAAY,MAAc,SAAkB,OAA6C;CAChG,MAAM,OAAO,KAAK,MAAM,GAAG;CAE3B,IAAI,OAAgC,GADrB,KAAK,IAAI,KAAK,KAEjB,OAAO,GAAG,SAAoB;EACtC,MAAM,KAAK,IAAI;EACf,OAAO;CACT,EACF;CACA,KAAK,MAAM,OAAO,KAAK,QAAQ,GAC7B,OAAO,GAAG,MAAM,KAAK;CAEvB,OAAO;AACT;AAEA,eAAsB,QAAQ,OAAwB,GAA4B;CAChF,MAAM,QAAqB,CAAC;CAC5B,MAAM,UAAU,aAAa,IAAI,EAAE,UAAU,EAAE,IAAI,SAAS;CAC5D,MAAM,MAAM,YAAY,cAAc,YAAY,EAAE,SAAS,SAAS,KAAK,CAAC,CAAC;CAC7E,MAAM,SAAS,MAAM,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,KAAK;CACxD,aAAO,OAAO,WAAW,KAAK,CAAC,CAAC,KAAK,KAAK;CAC1C,aAAO,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAClC,aAAO,OAAO,iBAAiB,CAAC,CAAC,QAC/B,aAAa,WAAW,IAAI,EAAE,QAAQ,OAAO,CAAC,CAAC,iBACjD;AACF"}
1
+ {"version":3,"file":"test-support.js","names":[],"sources":["../../../src/http/mcp/test-support.ts"],"sourcesContent":["import { expect } from 'vitest';\nimport { createMcpHandler } from '@modelcontextprotocol/server';\nimport { ALL_PERMISSIONS, actorWith } from '@headless-lms/core/organizations';\nimport type { Actor } from '@headless-lms/core/shared/actor';\nimport type { CallerLimit } from '@headless-lms/core/organizations';\nimport type { Container } from '../../app/container.js';\nimport { mcpEndpoints } from '../../app/mcp-endpoints.js';\nimport { createToolServer } from './server.js';\nimport { toToolResult, type Tool, type ToolContext, type ToolKind } from './tool.js';\nimport { TOOLS } from './tools/index.js';\n\nexport const ORG = { id: 'org_1', name: 'Acme', slug: 'acme', externalId: 'org_1' };\nexport const USER = {\n id: 'usr_1',\n email: 'owner@acme.test',\n firstName: 'Ada',\n lastName: 'Lovelace',\n externalId: 'usr_1',\n};\nexport const ORG_USER = { id: 'ou_1', orgId: 'org_1', userId: 'usr_1', status: 'active' };\nexport const OWNER: Actor = actorWith({ id: ORG_USER.id, orgId: ORG.id, owner: true });\n\ntype Lookups = {\n getByExternalId: (id: string) => Promise<{ id: string } | null>;\n getUserByExternalId: (id: string) => Promise<{ id: string } | null>;\n getOrgUser: (orgId: string, userId: string) => Promise<{ id: string } | null>;\n actorFor: (orgId: string, orgUserId: string, limit?: CallerLimit) => Promise<Actor | null>;\n};\n\nexport function fakeContainer(overrides: Record<string, unknown> = {}): Container {\n const logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => logger,\n };\n const base: Record<string, unknown> = {\n mcp: mcpEndpoints({\n authBaseURL: 'http://localhost:8100',\n adminAppUrl: 'http://localhost:8101',\n }),\n logger,\n requestContext: { set: () => {}, run: (_ctx: unknown, fn: () => void) => fn() },\n auth: {\n verifyAccess: async () => ({\n userId: USER.id,\n orgId: ORG.id,\n clientId: 'client_1',\n limit: { scopes: [...ALL_PERMISSIONS] },\n }),\n },\n organizations: {\n getById: async (id: string) => (id === ORG.id ? ORG : null),\n getByExternalId: async (id: string) => (id === ORG.externalId ? ORG : null),\n getOrgUser: async (orgId: string, userId: string) =>\n orgId === ORG.id && userId === USER.id ? ORG_USER : null,\n actorFor: async (orgId: string, orgUserId: string) =>\n orgId === ORG.id && orgUserId === ORG_USER.id ? OWNER : null,\n resolveCaller: async (authOrgId: string, authUserId: string, limit?: CallerLimit) => {\n const orgs = base.organizations as Lookups;\n const people = base.identity as Pick<Lookups, 'getUserByExternalId'>;\n const org = await orgs.getByExternalId(authOrgId);\n if (!org) {\n return null;\n }\n const user = await people.getUserByExternalId(authUserId);\n const orgUser = user ? await orgs.getOrgUser(org.id, user.id) : null;\n const actor = orgUser ? await orgs.actorFor(org.id, orgUser.id, limit) : null;\n return { orgId: org.id, userId: user?.id ?? null, orgUserId: orgUser?.id ?? null, actor };\n },\n },\n identity: {\n getUserByExternalId: async (id: string) => (id === USER.id ? USER : null),\n getUserById: async (id: string) => (id === USER.id ? USER : null),\n },\n };\n for (const [key, value] of Object.entries(overrides)) {\n const current = base[key];\n base[key] =\n current &&\n typeof current === 'object' &&\n value &&\n typeof value === 'object' &&\n !Array.isArray(value)\n ? { ...(current as object), ...(value as object) }\n : value;\n }\n return base as unknown as Container;\n}\n\nexport function toolContext(container: Container, actor: Actor = OWNER): ToolContext {\n return {\n container,\n caller: {\n actor,\n orgId: ORG.id,\n userId: USER.id,\n orgUserId: ORG_USER.id,\n authOrgId: ORG.id,\n authUserId: USER.id,\n },\n requestId: 'req_test',\n };\n}\n\nconst MODERN_META = {\n 'io.modelcontextprotocol/protocolVersion': '2026-07-28',\n 'io.modelcontextprotocol/clientCapabilities': {},\n};\n\nasync function rpc(\n ctx: ToolContext,\n tools: readonly Tool[],\n method: string,\n params: Record<string, unknown>,\n name?: string,\n) {\n const handler = createMcpHandler(() => createToolServer(ctx, tools));\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n 'mcp-protocol-version': '2026-07-28',\n 'mcp-method': method,\n };\n if (name) {\n headers['mcp-name'] = name;\n }\n const response = await handler.fetch(\n new Request('http://localhost:8100/mcp', {\n method: 'POST',\n headers,\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method,\n params: { ...params, _meta: MODERN_META },\n }),\n }),\n );\n const body = (await response.json()) as {\n result?: Record<string, unknown>;\n error?: { message: string };\n };\n if (body.error) {\n throw new Error(body.error.message);\n }\n return body.result as Record<string, unknown>;\n}\n\nexport async function listTools(ctx: ToolContext, tools: readonly Tool[] = TOOLS) {\n const result = await rpc(ctx, tools, 'tools/list', {});\n return result.tools as Array<{\n name: string;\n annotations?: Record<string, unknown>;\n inputSchema: { type?: string; properties?: Record<string, unknown> };\n }>;\n}\n\nexport async function callTool(\n name: string,\n args: unknown,\n ctx: ToolContext,\n tools: readonly Tool[] = TOOLS,\n) {\n return (await rpc(ctx, tools, 'tools/call', { name, arguments: args }, name)) as {\n isError?: boolean;\n structuredContent?: Record<string, unknown>;\n content: Array<{ type: string; text?: string }>;\n };\n}\n\nexport type ToolSpec = [name: string, kind: ToolKind, idempotent: boolean, openWorld: boolean];\n\nexport function toolSpecs(tools: readonly Tool[]): ToolSpec[] {\n return tools.map((tool) => [tool.name, tool.kind, tool.idempotent, tool.openWorld]);\n}\n\nexport type ToolCase = {\n tool: string;\n args: Record<string, unknown>;\n service: string;\n expected: unknown[];\n returns?: unknown;\n shown?: unknown;\n};\n\nfunction serviceFake(path: string, returns: unknown, calls: unknown[][]): Record<string, unknown> {\n const keys = path.split('.');\n const method = keys.pop() ?? '';\n let node: Record<string, unknown> = {\n [method]: async (...args: unknown[]) => {\n calls.push(args);\n return returns;\n },\n };\n for (const key of keys.reverse()) {\n node = { [key]: node };\n }\n return node;\n}\n\nexport async function runCase(tools: readonly Tool[], c: ToolCase): Promise<void> {\n const calls: unknown[][] = [];\n const returns = 'returns' in c ? c.returns : { id: 'result' };\n const ctx = toolContext(fakeContainer(serviceFake(c.service, returns, calls)));\n const result = await callTool(c.tool, c.args, ctx, tools);\n expect(result.isError ?? false).toBe(false);\n expect(calls).toEqual([c.expected]);\n expect(result.structuredContent).toEqual(\n toToolResult('shown' in c ? c.shown : returns).structuredContent,\n );\n}\n"],"mappings":"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"}
@@ -1,11 +1,10 @@
1
1
  import { Container } from "../../app/container.js";
2
- import { OrgScope } from "../scope.js";
3
- import { AccessScope } from "@headless-lms/core/organizations";
2
+ import { Caller } from "../scope.js";
4
3
  import { z } from "zod";
5
4
  //#region src/http/mcp/tool.d.ts
6
5
  interface ToolContext {
7
6
  container: Container;
8
- scope: OrgScope;
7
+ caller: Caller;
9
8
  requestId: string;
10
9
  }
11
10
  type ToolKind = 'read' | 'write' | 'destructive';
@@ -13,7 +12,6 @@ interface Tool {
13
12
  name: string;
14
13
  title: string;
15
14
  description: string;
16
- scope: AccessScope;
17
15
  kind: ToolKind;
18
16
  idempotent: boolean;
19
17
  openWorld: boolean;
@@ -26,10 +24,6 @@ interface ToolDef<Input extends z.ZodObject> extends Omit<Tool, 'input' | 'call'
26
24
  call(input: z.output<Input>, ctx: ToolContext): unknown;
27
25
  }
28
26
  declare const noInput: z.ZodObject<{}, z.core.$strip>;
29
- declare const actor: (scope: OrgScope) => {
30
- id: string;
31
- role: import("@headless-lms/core/organizations").StaffRole;
32
- };
33
27
  declare function defineTool<Input extends z.ZodObject>(def: ToolDef<Input>): Tool;
34
28
  interface ToolResult {
35
29
  [key: string]: unknown;
@@ -44,5 +38,5 @@ declare function toolError(message: string): ToolResult;
44
38
  declare function toToolResult(data: unknown): ToolResult;
45
39
  declare function runTool(tool: Tool, args: unknown, ctx: ToolContext): Promise<ToolResult>;
46
40
  //#endregion
47
- export { Tool, ToolContext, ToolDef, ToolKind, ToolResult, actor, defineTool, noInput, runTool, toToolResult, toolError };
41
+ export { Tool, ToolContext, ToolDef, ToolKind, ToolResult, defineTool, noInput, runTool, toToolResult, toolError };
48
42
  //# sourceMappingURL=tool.d.ts.map