@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.
- package/README.md +14 -0
- package/dist/app/community-mail.d.ts +10 -10
- package/dist/app/community-mail.js +72 -28
- package/dist/app/community-mail.js.map +1 -1
- package/dist/app/container.d.ts +48 -10
- package/dist/app/container.js +268 -48
- package/dist/app/container.js.map +1 -1
- package/dist/app/notification-channels.d.ts +47 -0
- package/dist/app/notification-channels.js +175 -0
- package/dist/app/notification-channels.js.map +1 -0
- package/dist/app/notification-reads.d.ts +23 -0
- package/dist/app/notification-reads.js +74 -0
- package/dist/app/notification-reads.js.map +1 -0
- package/dist/app/platform.d.ts +14 -0
- package/dist/app/platform.js +14 -0
- package/dist/app/platform.js.map +1 -0
- package/dist/http/config.d.ts +4 -0
- package/dist/http/errors.d.ts +1 -0
- package/dist/http/errors.js +24 -4
- package/dist/http/errors.js.map +1 -1
- package/dist/http/fastify.d.ts +8 -0
- package/dist/http/mcp/catalogue.d.ts +11 -9
- package/dist/http/mcp/catalogue.js +15 -21
- package/dist/http/mcp/catalogue.js.map +1 -1
- package/dist/http/mcp/challenge.js +3 -3
- package/dist/http/mcp/challenge.js.map +1 -1
- package/dist/http/mcp/route.js +7 -7
- package/dist/http/mcp/route.js.map +1 -1
- package/dist/http/mcp/test-support.d.ts +6 -5
- package/dist/http/mcp/test-support.js +32 -11
- package/dist/http/mcp/test-support.js.map +1 -1
- package/dist/http/mcp/tool.d.ts +3 -9
- package/dist/http/mcp/tool.js +14 -6
- package/dist/http/mcp/tool.js.map +1 -1
- package/dist/http/mcp/tools/automations.js +10 -20
- package/dist/http/mcp/tools/automations.js.map +1 -1
- package/dist/http/mcp/tools/catalog.js +25 -42
- package/dist/http/mcp/tools/catalog.js.map +1 -1
- package/dist/http/mcp/tools/comments.js +9 -19
- package/dist/http/mcp/tools/comments.js.map +1 -1
- package/dist/http/mcp/tools/communities.js +16 -31
- package/dist/http/mcp/tools/communities.js.map +1 -1
- package/dist/http/mcp/tools/community-members.js +9 -16
- package/dist/http/mcp/tools/community-members.js.map +1 -1
- package/dist/http/mcp/tools/community-moderation.js +13 -25
- package/dist/http/mcp/tools/community-moderation.js.map +1 -1
- package/dist/http/mcp/tools/courses.js +11 -21
- package/dist/http/mcp/tools/courses.js.map +1 -1
- package/dist/http/mcp/tools/events.d.ts +6 -0
- package/dist/http/mcp/tools/events.js +170 -0
- package/dist/http/mcp/tools/events.js.map +1 -0
- package/dist/http/mcp/tools/index.d.ts +5 -1
- package/dist/http/mcp/tools/index.js +45 -10
- package/dist/http/mcp/tools/index.js.map +1 -1
- package/dist/http/mcp/tools/organization.d.ts +6 -0
- package/dist/http/mcp/tools/organization.js +214 -0
- package/dist/http/mcp/tools/organization.js.map +1 -0
- package/dist/http/mcp/tools/people.js +30 -49
- package/dist/http/mcp/tools/people.js.map +1 -1
- package/dist/http/plugins/auth.js +37 -8
- package/dist/http/plugins/auth.js.map +1 -1
- package/dist/http/plugins/error-handler.d.ts +5 -1
- package/dist/http/plugins/error-handler.js +13 -3
- package/dist/http/plugins/error-handler.js.map +1 -1
- package/dist/http/plugins/openapi.js +18 -2
- package/dist/http/plugins/openapi.js.map +1 -1
- package/dist/http/plugins/platform.d.ts +7 -0
- package/dist/http/plugins/platform.js +13 -0
- package/dist/http/plugins/platform.js.map +1 -0
- package/dist/http/routes/access.js +54 -36
- package/dist/http/routes/access.js.map +1 -1
- package/dist/http/routes/activities.js +45 -45
- package/dist/http/routes/activities.js.map +1 -1
- package/dist/http/routes/assets.js +30 -65
- package/dist/http/routes/assets.js.map +1 -1
- package/dist/http/routes/automations.js +37 -42
- package/dist/http/routes/automations.js.map +1 -1
- package/dist/http/routes/bundles.js +33 -36
- package/dist/http/routes/bundles.js.map +1 -1
- package/dist/http/routes/communities.js +144 -136
- package/dist/http/routes/communities.js.map +1 -1
- package/dist/http/routes/community-moderation.js +83 -137
- package/dist/http/routes/community-moderation.js.map +1 -1
- package/dist/http/routes/course-analytics.js +32 -15
- package/dist/http/routes/course-analytics.js.map +1 -1
- package/dist/http/routes/courses.js +25 -32
- package/dist/http/routes/courses.js.map +1 -1
- package/dist/http/routes/dashboard.js +9 -9
- package/dist/http/routes/dashboard.js.map +1 -1
- package/dist/http/routes/discussion.js +21 -39
- package/dist/http/routes/discussion.js.map +1 -1
- package/dist/http/routes/downloads.js +41 -47
- package/dist/http/routes/downloads.js.map +1 -1
- package/dist/http/routes/entitlements.js +19 -23
- package/dist/http/routes/entitlements.js.map +1 -1
- package/dist/http/routes/events.d.ts +7 -0
- package/dist/http/routes/events.js +333 -0
- package/dist/http/routes/events.js.map +1 -0
- package/dist/http/routes/integrations.js +40 -73
- package/dist/http/routes/integrations.js.map +1 -1
- package/dist/http/routes/member/assets.js +10 -13
- package/dist/http/routes/member/assets.js.map +1 -1
- package/dist/http/routes/member/chats.js +177 -75
- package/dist/http/routes/member/chats.js.map +1 -1
- package/dist/http/routes/member/comments.js +65 -26
- package/dist/http/routes/member/comments.js.map +1 -1
- package/dist/http/routes/member/community.js +68 -51
- package/dist/http/routes/member/community.js.map +1 -1
- package/dist/http/routes/member/courses.js +12 -19
- package/dist/http/routes/member/courses.js.map +1 -1
- package/dist/http/routes/member/devices.d.ts +7 -0
- package/dist/http/routes/member/devices.js +97 -0
- package/dist/http/routes/member/devices.js.map +1 -0
- package/dist/http/routes/member/downloads.js +6 -9
- package/dist/http/routes/member/downloads.js.map +1 -1
- package/dist/http/routes/member/events.d.ts +7 -0
- package/dist/http/routes/member/events.js +386 -0
- package/dist/http/routes/member/events.js.map +1 -0
- package/dist/http/routes/member/files.js +71 -46
- package/dist/http/routes/member/files.js.map +1 -1
- package/dist/http/routes/member/index.js +6 -0
- package/dist/http/routes/member/index.js.map +1 -1
- package/dist/http/routes/member/notification-settings.d.ts +7 -0
- package/dist/http/routes/member/notification-settings.js +100 -0
- package/dist/http/routes/member/notification-settings.js.map +1 -0
- package/dist/http/routes/member/notifications.js +69 -45
- package/dist/http/routes/member/notifications.js.map +1 -1
- package/dist/http/routes/member/org.js +5 -15
- package/dist/http/routes/member/org.js.map +1 -1
- package/dist/http/routes/member/posts.js +94 -95
- package/dist/http/routes/member/posts.js.map +1 -1
- package/dist/http/routes/member/profile.js +15 -6
- package/dist/http/routes/member/profile.js.map +1 -1
- package/dist/http/routes/member/progress.js +7 -11
- package/dist/http/routes/member/progress.js.map +1 -1
- package/dist/http/routes/organizations.js +415 -110
- package/dist/http/routes/organizations.js.map +1 -1
- package/dist/http/routes/platform.d.ts +7 -0
- package/dist/http/routes/platform.js +61 -0
- package/dist/http/routes/platform.js.map +1 -0
- package/dist/http/routes/students.js +12 -75
- package/dist/http/routes/students.js.map +1 -1
- package/dist/http/routes/webhooks.js +30 -39
- package/dist/http/routes/webhooks.js.map +1 -1
- package/dist/http/routes.js +9 -0
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schemas/access.d.ts +346 -64
- package/dist/http/schemas/access.js +39 -12
- package/dist/http/schemas/access.js.map +1 -1
- package/dist/http/schemas/assets.d.ts +13 -8
- package/dist/http/schemas/assets.js +6 -5
- package/dist/http/schemas/assets.js.map +1 -1
- package/dist/http/schemas/automations.d.ts +4 -8
- package/dist/http/schemas/chats.d.ts +238 -36
- package/dist/http/schemas/chats.js +14 -7
- package/dist/http/schemas/chats.js.map +1 -1
- package/dist/http/schemas/community.d.ts +140 -33
- package/dist/http/schemas/community.js +25 -9
- package/dist/http/schemas/community.js.map +1 -1
- package/dist/http/schemas/course-analytics.d.ts +51 -1
- package/dist/http/schemas/course-analytics.js +19 -1
- package/dist/http/schemas/course-analytics.js.map +1 -1
- package/dist/http/schemas/dashboard.d.ts +1 -0
- package/dist/http/schemas/dashboard.js +2 -1
- package/dist/http/schemas/dashboard.js.map +1 -1
- package/dist/http/schemas/discussion.d.ts +45 -73
- package/dist/http/schemas/discussion.js +16 -4
- package/dist/http/schemas/discussion.js.map +1 -1
- package/dist/http/schemas/entitlements.d.ts +9 -12
- package/dist/http/schemas/entitlements.js +11 -5
- package/dist/http/schemas/entitlements.js.map +1 -1
- package/dist/http/schemas/events.d.ts +527 -0
- package/dist/http/schemas/events.js +64 -0
- package/dist/http/schemas/events.js.map +1 -0
- package/dist/http/schemas/files.d.ts +15 -5
- package/dist/http/schemas/files.js +10 -2
- package/dist/http/schemas/files.js.map +1 -1
- package/dist/http/schemas/index.d.ts +14 -12
- package/dist/http/schemas/index.js +14 -12
- package/dist/http/schemas/invites.d.ts +3 -47
- package/dist/http/schemas/invites.js +4 -25
- package/dist/http/schemas/invites.js.map +1 -1
- package/dist/http/schemas/member.d.ts +94 -7
- package/dist/http/schemas/member.js +7 -3
- package/dist/http/schemas/member.js.map +1 -1
- package/dist/http/schemas/members.d.ts +329 -65
- package/dist/http/schemas/members.js +67 -25
- package/dist/http/schemas/members.js.map +1 -1
- package/dist/http/schemas/notifications.d.ts +502 -175
- package/dist/http/schemas/notifications.js +148 -28
- package/dist/http/schemas/notifications.js.map +1 -1
- package/dist/http/schemas/platform.d.ts +37 -0
- package/dist/http/schemas/platform.js +19 -0
- package/dist/http/schemas/platform.js.map +1 -0
- package/dist/http/schemas/posts.d.ts +92 -38
- package/dist/http/schemas/posts.js +4 -4
- package/dist/http/schemas/posts.js.map +1 -1
- package/dist/http/schemas/profile.d.ts +1 -1
- package/dist/http/schemas/shared.d.ts +10 -13
- package/dist/http/schemas/shared.js +3 -11
- package/dist/http/schemas/shared.js.map +1 -1
- package/dist/http/schemas/students.d.ts +22 -0
- package/dist/http/schemas/students.js +3 -1
- package/dist/http/schemas/students.js.map +1 -1
- package/dist/http/scope.d.ts +9 -12
- package/dist/http/scope.js +30 -35
- package/dist/http/scope.js.map +1 -1
- package/dist/http/server.js +9 -0
- package/dist/http/server.js.map +1 -1
- package/dist/http/shutdown.d.ts +11 -0
- package/dist/http/shutdown.js +40 -0
- package/dist/http/shutdown.js.map +1 -0
- package/dist/http/sse.d.ts +19 -0
- package/dist/http/sse.js +104 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/http/test-caller.d.ts +13 -0
- package/dist/http/test-caller.js +14 -0
- package/dist/http/test-caller.js.map +1 -0
- package/dist/http/well-known.js +2 -2
- package/dist/http/well-known.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/packages/server/package.js +1 -1
- package/package.json +7 -6
- package/dist/http/routes/member/actor.d.ts +0 -14
- package/dist/http/routes/member/actor.js +0 -11
- package/dist/http/routes/member/actor.js.map +0 -1
package/dist/http/errors.d.ts
CHANGED
package/dist/http/errors.js
CHANGED
|
@@ -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
|
|
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",
|
package/dist/http/errors.js.map
CHANGED
|
@@ -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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"}
|
package/dist/http/fastify.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
20
|
+
interface CataloguePermission {
|
|
21
|
+
permission: Permission;
|
|
23
22
|
resource: string;
|
|
24
23
|
action: string;
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
+
permissions: CataloguePermission[];
|
|
32
34
|
tools: CatalogueTool[];
|
|
33
35
|
}
|
|
34
36
|
declare function mcpCatalogue(): McpCatalogue;
|
|
35
37
|
//#endregion
|
|
36
|
-
export { CatalogueField,
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
43
|
-
const [resource =
|
|
33
|
+
permissions: ALL_PERMISSIONS.map((permission) => {
|
|
34
|
+
const [resource = permission, action = ""] = permission.split(":");
|
|
35
|
+
const definition = PERMISSIONS[permission];
|
|
44
36
|
return {
|
|
45
|
-
|
|
37
|
+
permission,
|
|
46
38
|
resource,
|
|
47
39
|
action,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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="${
|
|
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
|
|
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 {
|
|
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"}
|
package/dist/http/mcp/route.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { NoActiveOrgError,
|
|
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 {
|
|
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
|
|
32
|
+
let caller;
|
|
33
33
|
try {
|
|
34
|
-
|
|
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
|
-
|
|
42
|
+
caller,
|
|
43
43
|
requestId: request.id
|
|
44
44
|
},
|
|
45
|
-
tools
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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,
|
|
92
|
+
function toolContext(container, actor = OWNER) {
|
|
72
93
|
return {
|
|
73
94
|
container,
|
|
74
|
-
|
|
95
|
+
caller: {
|
|
96
|
+
actor,
|
|
75
97
|
orgId: ORG.id,
|
|
76
98
|
userId: USER.id,
|
|
77
99
|
orgUserId: ORG_USER.id,
|
|
78
|
-
|
|
79
|
-
|
|
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":"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"}
|
|
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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"}
|
package/dist/http/mcp/tool.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Container } from "../../app/container.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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,
|
|
41
|
+
export { Tool, ToolContext, ToolDef, ToolKind, ToolResult, defineTool, noInput, runTool, toToolResult, toolError };
|
|
48
42
|
//# sourceMappingURL=tool.d.ts.map
|