moodle-cli 0.7.0-alpha.3 → 0.7.0-alpha.5
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 +3 -1
- package/SKILL.md +1 -1
- package/agents/openai.yaml +2 -2
- package/dist/moodle.js +688 -126
- package/dist/worker/worker.js +1028 -568
- package/package.json +2 -1
- package/references/downloads.md +5 -1
package/dist/worker/worker.js
CHANGED
|
@@ -3573,7 +3573,7 @@ var require_parse = __commonJS({
|
|
|
3573
3573
|
var whitespace = /* @__PURE__ */ new Set([9, 10, 12, 13, 32]);
|
|
3574
3574
|
var ZERO = "0".charCodeAt(0);
|
|
3575
3575
|
var NINE = "9".charCodeAt(0);
|
|
3576
|
-
function
|
|
3576
|
+
function parse7(formula) {
|
|
3577
3577
|
formula = formula.trim().toLowerCase();
|
|
3578
3578
|
if (formula === "even") {
|
|
3579
3579
|
return [2, 0];
|
|
@@ -3625,7 +3625,7 @@ var require_parse = __commonJS({
|
|
|
3625
3625
|
}
|
|
3626
3626
|
}
|
|
3627
3627
|
}
|
|
3628
|
-
exports.parse =
|
|
3628
|
+
exports.parse = parse7;
|
|
3629
3629
|
}
|
|
3630
3630
|
});
|
|
3631
3631
|
|
|
@@ -5075,7 +5075,7 @@ var require_html = __commonJS({
|
|
|
5075
5075
|
}).join("");
|
|
5076
5076
|
}
|
|
5077
5077
|
set innerHTML(content) {
|
|
5078
|
-
const r =
|
|
5078
|
+
const r = parse7(content, this._parseOptions);
|
|
5079
5079
|
const nodes = r.childNodes.length ? r.childNodes : [new text_1.default(content, this)];
|
|
5080
5080
|
resetParent(nodes, this);
|
|
5081
5081
|
resetParent(this.childNodes, null);
|
|
@@ -5086,7 +5086,7 @@ var require_html = __commonJS({
|
|
|
5086
5086
|
content = [content];
|
|
5087
5087
|
} else if (typeof content == "string") {
|
|
5088
5088
|
options = Object.assign(Object.assign({}, this._parseOptions), options);
|
|
5089
|
-
const r =
|
|
5089
|
+
const r = parse7(content, options);
|
|
5090
5090
|
content = r.childNodes.length ? r.childNodes : [new text_1.default(r.innerHTML, this)];
|
|
5091
5091
|
}
|
|
5092
5092
|
resetParent(this.childNodes, null);
|
|
@@ -5100,7 +5100,7 @@ var require_html = __commonJS({
|
|
|
5100
5100
|
if (node instanceof node_1.default) {
|
|
5101
5101
|
return [node];
|
|
5102
5102
|
} else if (typeof node == "string") {
|
|
5103
|
-
const r =
|
|
5103
|
+
const r = parse7(node, this._parseOptions);
|
|
5104
5104
|
return r.childNodes.length ? r.childNodes : [new text_1.default(node, this)];
|
|
5105
5105
|
}
|
|
5106
5106
|
return [];
|
|
@@ -5484,7 +5484,7 @@ var require_html = __commonJS({
|
|
|
5484
5484
|
if (arguments.length < 2) {
|
|
5485
5485
|
throw new Error("2 arguments required");
|
|
5486
5486
|
}
|
|
5487
|
-
const p =
|
|
5487
|
+
const p = parse7(html, this._parseOptions);
|
|
5488
5488
|
if (where === "afterend") {
|
|
5489
5489
|
this.after(...p.childNodes);
|
|
5490
5490
|
} else if (where === "afterbegin") {
|
|
@@ -5630,7 +5630,7 @@ var require_html = __commonJS({
|
|
|
5630
5630
|
}
|
|
5631
5631
|
/** Clone this Node */
|
|
5632
5632
|
clone() {
|
|
5633
|
-
return
|
|
5633
|
+
return parse7(this.toString(), this._parseOptions).firstChild;
|
|
5634
5634
|
}
|
|
5635
5635
|
};
|
|
5636
5636
|
exports.default = HTMLElement2;
|
|
@@ -5832,7 +5832,7 @@ var require_html = __commonJS({
|
|
|
5832
5832
|
return stack;
|
|
5833
5833
|
}
|
|
5834
5834
|
exports.base_parse = base_parse;
|
|
5835
|
-
function
|
|
5835
|
+
function parse7(data, options = {}) {
|
|
5836
5836
|
const stack = base_parse(data, options);
|
|
5837
5837
|
const [root] = stack;
|
|
5838
5838
|
while (stack.length > 1) {
|
|
@@ -5860,7 +5860,7 @@ var require_html = __commonJS({
|
|
|
5860
5860
|
}
|
|
5861
5861
|
return root;
|
|
5862
5862
|
}
|
|
5863
|
-
exports.parse =
|
|
5863
|
+
exports.parse = parse7;
|
|
5864
5864
|
function resolveInsertable(insertable) {
|
|
5865
5865
|
return insertable.map((val) => {
|
|
5866
5866
|
if (typeof val === "string") {
|
|
@@ -5928,18 +5928,18 @@ var require_dist = __commonJS({
|
|
|
5928
5928
|
var parse_1 = __importDefault(require_parse2());
|
|
5929
5929
|
var valid_1 = __importDefault(require_valid());
|
|
5930
5930
|
exports.valid = valid_1.default;
|
|
5931
|
-
function
|
|
5931
|
+
function parse7(data, options = {}) {
|
|
5932
5932
|
return (0, parse_1.default)(data, options);
|
|
5933
5933
|
}
|
|
5934
|
-
exports.default =
|
|
5935
|
-
exports.parse =
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5934
|
+
exports.default = parse7;
|
|
5935
|
+
exports.parse = parse7;
|
|
5936
|
+
parse7.parse = parse_1.default;
|
|
5937
|
+
parse7.HTMLElement = html_1.default;
|
|
5938
|
+
parse7.CommentNode = comment_1.default;
|
|
5939
|
+
parse7.valid = valid_1.default;
|
|
5940
|
+
parse7.Node = node_1.default;
|
|
5941
|
+
parse7.TextNode = text_1.default;
|
|
5942
|
+
parse7.NodeType = type_1.default;
|
|
5943
5943
|
}
|
|
5944
5944
|
});
|
|
5945
5945
|
|
|
@@ -5995,300 +5995,6 @@ function problemResponse(status, code, title, detail, headers) {
|
|
|
5995
5995
|
);
|
|
5996
5996
|
}
|
|
5997
5997
|
|
|
5998
|
-
// src/version.ts
|
|
5999
|
-
var VERSION = "0.7.0-alpha.3";
|
|
6000
|
-
|
|
6001
|
-
// src/worker/http.ts
|
|
6002
|
-
var HEALTH_PATH = "/healthz";
|
|
6003
|
-
var READY_PATH = "/readyz";
|
|
6004
|
-
var MCP_PATH = "/mcp";
|
|
6005
|
-
var SESSION_PATH = "/session";
|
|
6006
|
-
var SESSION_TOUCH_PATH = "/session/touch";
|
|
6007
|
-
var WORKER_SERVICE_ID = "moodle-mcp";
|
|
6008
|
-
var WORKER_SERVICE_VERSION = VERSION;
|
|
6009
|
-
function createWorkerHandler(dependencies) {
|
|
6010
|
-
return {
|
|
6011
|
-
async fetch(request, env) {
|
|
6012
|
-
const url2 = new URL(request.url);
|
|
6013
|
-
const authorityProblem = validateRequestAuthority(request, url2, env.EXPECTED_HOST);
|
|
6014
|
-
if (authorityProblem) return authorityProblem;
|
|
6015
|
-
if (hasQueryCredential(url2)) return unauthorized("Bearer credentials are not accepted in the query string.");
|
|
6016
|
-
if (url2.pathname === HEALTH_PATH && request.method === "GET") {
|
|
6017
|
-
return Response.json(
|
|
6018
|
-
{ status: "pass", serviceId: WORKER_SERVICE_ID, version: WORKER_SERVICE_VERSION },
|
|
6019
|
-
{ headers: { "content-type": "application/health+json; charset=utf-8" } }
|
|
6020
|
-
);
|
|
6021
|
-
}
|
|
6022
|
-
if (url2.pathname === MCP_PATH && request.method !== "POST") {
|
|
6023
|
-
return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The MCP transport accepts POST requests only.", {
|
|
6024
|
-
allow: "POST"
|
|
6025
|
-
});
|
|
6026
|
-
}
|
|
6027
|
-
if (url2.pathname === MCP_PATH) {
|
|
6028
|
-
if (!await verifyBearerToken(request.headers.get("authorization"), [
|
|
6029
|
-
env.MCP_ACCESS_TOKEN_DIGEST,
|
|
6030
|
-
activePreviousDigest(env.MCP_ACCESS_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
|
|
6031
|
-
])) {
|
|
6032
|
-
return unauthorized();
|
|
6033
|
-
}
|
|
6034
|
-
const server = typeof dependencies.mcpServer === "function" ? dependencies.mcpServer(env) : dependencies.mcpServer;
|
|
6035
|
-
return handleMcpRequest(request, server);
|
|
6036
|
-
}
|
|
6037
|
-
if (url2.pathname === READY_PATH && request.method === "GET") {
|
|
6038
|
-
if (!await authorizeSessionSync(request, env)) return unauthorized();
|
|
6039
|
-
const broker = resolveBroker(dependencies, env);
|
|
6040
|
-
if (broker instanceof Response) return broker;
|
|
6041
|
-
return broker.ready();
|
|
6042
|
-
}
|
|
6043
|
-
if (url2.pathname === SESSION_PATH && request.method === "PUT") {
|
|
6044
|
-
if (!await authorizeSessionSync(request, env)) return unauthorized();
|
|
6045
|
-
const broker = resolveBroker(dependencies, env);
|
|
6046
|
-
if (broker instanceof Response) return broker;
|
|
6047
|
-
return broker.replaceSession(request);
|
|
6048
|
-
}
|
|
6049
|
-
if (url2.pathname === SESSION_TOUCH_PATH && request.method === "POST") {
|
|
6050
|
-
if (!await authorizeSessionSync(request, env)) return unauthorized();
|
|
6051
|
-
const broker = resolveBroker(dependencies, env);
|
|
6052
|
-
if (broker instanceof Response) return broker;
|
|
6053
|
-
return broker.touch();
|
|
6054
|
-
}
|
|
6055
|
-
if (url2.pathname === READY_PATH || url2.pathname === SESSION_PATH || url2.pathname === SESSION_TOUCH_PATH) {
|
|
6056
|
-
const method = url2.pathname === READY_PATH ? "GET" : url2.pathname === SESSION_PATH ? "PUT" : "POST";
|
|
6057
|
-
return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The session route does not accept this method.", {
|
|
6058
|
-
allow: method
|
|
6059
|
-
});
|
|
6060
|
-
}
|
|
6061
|
-
return problemResponse(404, "NOT_FOUND", "Not Found", "The requested route does not exist.");
|
|
6062
|
-
}
|
|
6063
|
-
};
|
|
6064
|
-
}
|
|
6065
|
-
function createDurableObjectBrokerApi(namespace) {
|
|
6066
|
-
const stub = namespace.get(namespace.idFromName("primary"));
|
|
6067
|
-
return {
|
|
6068
|
-
async handleMcp(body, context) {
|
|
6069
|
-
const response = await stub.fetch(new Request("https://session-broker/mcp", {
|
|
6070
|
-
method: "POST",
|
|
6071
|
-
headers: { "content-type": "application/json" },
|
|
6072
|
-
body: JSON.stringify({ request: body, context })
|
|
6073
|
-
}));
|
|
6074
|
-
if (!response.ok) {
|
|
6075
|
-
const problem = await safeProblem(response);
|
|
6076
|
-
throw new SessionBrokerMcpError(response.status, problem?.code);
|
|
6077
|
-
}
|
|
6078
|
-
const envelope = await response.json();
|
|
6079
|
-
return envelope.response ?? null;
|
|
6080
|
-
},
|
|
6081
|
-
ready: () => stub.fetch(new Request("https://session-broker/readyz")),
|
|
6082
|
-
async replaceSession(request) {
|
|
6083
|
-
return stub.fetch(new Request("https://session-broker/session", {
|
|
6084
|
-
method: "PUT",
|
|
6085
|
-
headers: { "content-type": request.headers.get("content-type") ?? "application/json" },
|
|
6086
|
-
body: await request.arrayBuffer()
|
|
6087
|
-
}));
|
|
6088
|
-
},
|
|
6089
|
-
touch: () => stub.fetch(new Request("https://session-broker/session/touch", { method: "POST" }))
|
|
6090
|
-
};
|
|
6091
|
-
}
|
|
6092
|
-
var SessionBrokerMcpError = class extends Error {
|
|
6093
|
-
constructor(status, code) {
|
|
6094
|
-
super("The session broker could not complete the MCP request.");
|
|
6095
|
-
this.status = status;
|
|
6096
|
-
this.code = code;
|
|
6097
|
-
this.name = "SessionBrokerMcpError";
|
|
6098
|
-
}
|
|
6099
|
-
status;
|
|
6100
|
-
code;
|
|
6101
|
-
};
|
|
6102
|
-
function resolveBroker(dependencies, env) {
|
|
6103
|
-
if (dependencies.broker) return dependencies.broker(env);
|
|
6104
|
-
if (env.SESSION_BROKER) return createDurableObjectBrokerApi(env.SESSION_BROKER);
|
|
6105
|
-
return problemResponse(503, "SESSION_BROKER_UNAVAILABLE", "Service Unavailable", "The session broker is unavailable.");
|
|
6106
|
-
}
|
|
6107
|
-
function validateRequestAuthority(request, url2, configuredHost) {
|
|
6108
|
-
const expectedHost = (configuredHost ?? url2.host).toLowerCase();
|
|
6109
|
-
const requestHost = (request.headers.get("host") ?? url2.host).toLowerCase();
|
|
6110
|
-
if (requestHost !== expectedHost) {
|
|
6111
|
-
return problemResponse(403, "INVALID_HOST", "Forbidden", "The request Host is not allowed.");
|
|
6112
|
-
}
|
|
6113
|
-
const origin = request.headers.get("origin");
|
|
6114
|
-
if (!origin) return null;
|
|
6115
|
-
try {
|
|
6116
|
-
const parsed = new URL(origin);
|
|
6117
|
-
if (parsed.origin !== url2.origin || parsed.host.toLowerCase() !== expectedHost) {
|
|
6118
|
-
return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
|
|
6119
|
-
}
|
|
6120
|
-
} catch {
|
|
6121
|
-
return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
|
|
6122
|
-
}
|
|
6123
|
-
return null;
|
|
6124
|
-
}
|
|
6125
|
-
function unauthorized(detail = "A valid Bearer token is required.") {
|
|
6126
|
-
return problemResponse(401, "INVALID_BEARER_TOKEN", "Unauthorized", detail, {
|
|
6127
|
-
"www-authenticate": 'Bearer realm="moodle-mcp"'
|
|
6128
|
-
});
|
|
6129
|
-
}
|
|
6130
|
-
async function handleMcpRequest(request, server) {
|
|
6131
|
-
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
6132
|
-
return problemResponse(415, "UNSUPPORTED_MEDIA_TYPE", "Unsupported Media Type", "MCP requests must use application/json.");
|
|
6133
|
-
}
|
|
6134
|
-
let body;
|
|
6135
|
-
try {
|
|
6136
|
-
body = await request.json();
|
|
6137
|
-
} catch {
|
|
6138
|
-
return problemResponse(400, "INVALID_JSON", "Bad Request", "The request body is not valid JSON.");
|
|
6139
|
-
}
|
|
6140
|
-
const protocolVersion = request.headers.get("mcp-protocol-version");
|
|
6141
|
-
const metadataProblem = validateProtocolMetadata(request.headers, body, protocolVersion);
|
|
6142
|
-
if (metadataProblem) return metadataProblem;
|
|
6143
|
-
try {
|
|
6144
|
-
const response = await server.handle(body, {
|
|
6145
|
-
protocolVersion,
|
|
6146
|
-
method: request.headers.get("mcp-method") ?? void 0,
|
|
6147
|
-
toolName: request.headers.get("mcp-name") ?? void 0
|
|
6148
|
-
});
|
|
6149
|
-
if (response === null) return new Response(null, { status: 202 });
|
|
6150
|
-
if (request.headers.get("accept")?.toLowerCase().includes("text/event-stream")) {
|
|
6151
|
-
return new Response(`event: message
|
|
6152
|
-
data: ${JSON.stringify(response)}
|
|
6153
|
-
|
|
6154
|
-
`, {
|
|
6155
|
-
headers: {
|
|
6156
|
-
"cache-control": "private, no-store",
|
|
6157
|
-
"content-type": "text/event-stream; charset=utf-8"
|
|
6158
|
-
}
|
|
6159
|
-
});
|
|
6160
|
-
}
|
|
6161
|
-
return Response.json(response, { headers: { "cache-control": "private, no-store" } });
|
|
6162
|
-
} catch (error51) {
|
|
6163
|
-
if (error51 instanceof SessionBrokerMcpError) {
|
|
6164
|
-
const status = error51.status === 503 ? 503 : 500;
|
|
6165
|
-
const code = error51.code === "SESSION_MISSING" || error51.code === "SESSION_EXPIRED" ? error51.code : "SESSION_UNAVAILABLE";
|
|
6166
|
-
return problemResponse(status, code, "Service Unavailable", "The Moodle session is not ready.");
|
|
6167
|
-
}
|
|
6168
|
-
if (error51 instanceof Error && error51.name === "UnsupportedProtocolVersionError") {
|
|
6169
|
-
return problemResponse(400, "UNSUPPORTED_PROTOCOL_VERSION", "Unsupported Protocol Version", "The requested MCP protocol version is not supported.");
|
|
6170
|
-
}
|
|
6171
|
-
return problemResponse(500, "MCP_REQUEST_FAILED", "Internal Server Error", "The MCP request could not be completed.");
|
|
6172
|
-
}
|
|
6173
|
-
}
|
|
6174
|
-
async function safeProblem(response) {
|
|
6175
|
-
try {
|
|
6176
|
-
const value = await response.json();
|
|
6177
|
-
return isRecord(value) && (value.code === void 0 || typeof value.code === "string") ? value : null;
|
|
6178
|
-
} catch {
|
|
6179
|
-
return null;
|
|
6180
|
-
}
|
|
6181
|
-
}
|
|
6182
|
-
function validateProtocolMetadata(headers, body, protocolVersion) {
|
|
6183
|
-
if (!protocolVersion) {
|
|
6184
|
-
return problemResponse(400, "MCP_PROTOCOL_METADATA_INVALID", "Bad Request", "MCP-Protocol-Version is required.");
|
|
6185
|
-
}
|
|
6186
|
-
const params = isRecord(body.params) ? body.params : void 0;
|
|
6187
|
-
const metadata = params && isRecord(params._meta) ? params._meta : void 0;
|
|
6188
|
-
const bodyVersion = metadata?.["io.modelcontextprotocol/protocolVersion"];
|
|
6189
|
-
if (bodyVersion !== void 0 && bodyVersion !== protocolVersion) {
|
|
6190
|
-
return problemResponse(400, "MCP_PROTOCOL_METADATA_MISMATCH", "Bad Request", "MCP protocol metadata does not match the HTTP headers.");
|
|
6191
|
-
}
|
|
6192
|
-
if (typeof body.method !== "string") {
|
|
6193
|
-
return problemResponse(400, "MCP_PROTOCOL_METADATA_INVALID", "Bad Request", "The JSON-RPC method is required.");
|
|
6194
|
-
}
|
|
6195
|
-
const headerMethod = headers.get("mcp-method");
|
|
6196
|
-
const headerName = headers.get("mcp-name") ?? void 0;
|
|
6197
|
-
const paramsName = params && typeof params.name === "string" ? params.name : void 0;
|
|
6198
|
-
if (headerMethod !== body.method || headerName !== paramsName) {
|
|
6199
|
-
return problemResponse(400, "MCP_PROTOCOL_METADATA_MISMATCH", "Bad Request", "MCP method metadata does not match the JSON-RPC request.");
|
|
6200
|
-
}
|
|
6201
|
-
return null;
|
|
6202
|
-
}
|
|
6203
|
-
function authorizeSessionSync(request, env) {
|
|
6204
|
-
return verifyBearerToken(request.headers.get("authorization"), [
|
|
6205
|
-
env.SESSION_SYNC_TOKEN_DIGEST,
|
|
6206
|
-
activePreviousDigest(env.SESSION_SYNC_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
|
|
6207
|
-
]);
|
|
6208
|
-
}
|
|
6209
|
-
function activePreviousDigest(digest, expiresAt2) {
|
|
6210
|
-
if (!digest || !expiresAt2) return void 0;
|
|
6211
|
-
const expiration = Number(expiresAt2);
|
|
6212
|
-
return Number.isFinite(expiration) && Date.now() < expiration ? digest : void 0;
|
|
6213
|
-
}
|
|
6214
|
-
function isRecord(value) {
|
|
6215
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6216
|
-
}
|
|
6217
|
-
|
|
6218
|
-
// src/worker/crypto.ts
|
|
6219
|
-
async function createEncryptionKeyring(currentSecret, previousSecret) {
|
|
6220
|
-
if (!currentSecret) throw new Error("SESSION_ENCRYPTION_KEY is required.");
|
|
6221
|
-
const current = await deriveEncryptionKey(currentSecret);
|
|
6222
|
-
const keys = /* @__PURE__ */ new Map([[current.id, current.key]]);
|
|
6223
|
-
if (previousSecret) {
|
|
6224
|
-
const previous = await deriveEncryptionKey(previousSecret);
|
|
6225
|
-
keys.set(previous.id, previous.key);
|
|
6226
|
-
}
|
|
6227
|
-
return { current, keys };
|
|
6228
|
-
}
|
|
6229
|
-
async function encryptValue(value, keyring) {
|
|
6230
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
6231
|
-
const ciphertext = await crypto.subtle.encrypt(
|
|
6232
|
-
{ name: "AES-GCM", iv },
|
|
6233
|
-
keyring.current.key,
|
|
6234
|
-
new TextEncoder().encode(value)
|
|
6235
|
-
);
|
|
6236
|
-
return JSON.stringify({
|
|
6237
|
-
version: 1,
|
|
6238
|
-
keyId: keyring.current.id,
|
|
6239
|
-
iv: toBase64Url(iv),
|
|
6240
|
-
ciphertext: toBase64Url(new Uint8Array(ciphertext))
|
|
6241
|
-
});
|
|
6242
|
-
}
|
|
6243
|
-
async function decryptValue(encrypted, keyring) {
|
|
6244
|
-
const envelope = parseEnvelope(encrypted);
|
|
6245
|
-
const key = keyring.keys.get(envelope.keyId);
|
|
6246
|
-
if (!key) throw new Error("The session encryption key is unavailable.");
|
|
6247
|
-
const plaintext = await crypto.subtle.decrypt(
|
|
6248
|
-
{ name: "AES-GCM", iv: fromBase64Url(envelope.iv) },
|
|
6249
|
-
key,
|
|
6250
|
-
fromBase64Url(envelope.ciphertext)
|
|
6251
|
-
);
|
|
6252
|
-
return {
|
|
6253
|
-
value: new TextDecoder().decode(plaintext),
|
|
6254
|
-
keyId: envelope.keyId,
|
|
6255
|
-
needsRotation: envelope.keyId !== keyring.current.id
|
|
6256
|
-
};
|
|
6257
|
-
}
|
|
6258
|
-
async function deriveEncryptionKey(secret) {
|
|
6259
|
-
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
|
|
6260
|
-
const key = await crypto.subtle.importKey("raw", digest, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
6261
|
-
const id = Array.from(digest.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6262
|
-
return { id, key };
|
|
6263
|
-
}
|
|
6264
|
-
function parseEnvelope(value) {
|
|
6265
|
-
let parsed;
|
|
6266
|
-
try {
|
|
6267
|
-
parsed = JSON.parse(value);
|
|
6268
|
-
} catch {
|
|
6269
|
-
throw new Error("The encrypted session record is invalid.");
|
|
6270
|
-
}
|
|
6271
|
-
if (!isRecord2(parsed) || parsed.version !== 1 || typeof parsed.keyId !== "string" || typeof parsed.iv !== "string" || typeof parsed.ciphertext !== "string") {
|
|
6272
|
-
throw new Error("The encrypted session record is invalid.");
|
|
6273
|
-
}
|
|
6274
|
-
return parsed;
|
|
6275
|
-
}
|
|
6276
|
-
function toBase64Url(bytes) {
|
|
6277
|
-
let binary = "";
|
|
6278
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
6279
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
6280
|
-
}
|
|
6281
|
-
function fromBase64Url(value) {
|
|
6282
|
-
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
6283
|
-
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
6284
|
-
const bytes = new Uint8Array(binary.length);
|
|
6285
|
-
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
6286
|
-
return bytes;
|
|
6287
|
-
}
|
|
6288
|
-
function isRecord2(value) {
|
|
6289
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6290
|
-
}
|
|
6291
|
-
|
|
6292
5998
|
// node_modules/zod/v4/classic/external.js
|
|
6293
5999
|
var external_exports = {};
|
|
6294
6000
|
__export(external_exports, {
|
|
@@ -20678,131 +20384,553 @@ function convertBaseSchema(schema2, ctx) {
|
|
|
20678
20384
|
if (typeof schema2.maxItems === "number") {
|
|
20679
20385
|
arraySchema = arraySchema.max(schema2.maxItems);
|
|
20680
20386
|
}
|
|
20681
|
-
zodSchema = arraySchema;
|
|
20682
|
-
} else {
|
|
20683
|
-
zodSchema = z.array(z.any());
|
|
20387
|
+
zodSchema = arraySchema;
|
|
20388
|
+
} else {
|
|
20389
|
+
zodSchema = z.array(z.any());
|
|
20390
|
+
}
|
|
20391
|
+
break;
|
|
20392
|
+
}
|
|
20393
|
+
default:
|
|
20394
|
+
throw new Error(`Unsupported type: ${type}`);
|
|
20395
|
+
}
|
|
20396
|
+
return zodSchema;
|
|
20397
|
+
}
|
|
20398
|
+
function convertSchema(schema2, ctx) {
|
|
20399
|
+
if (typeof schema2 === "boolean") {
|
|
20400
|
+
return schema2 ? z.any() : z.never();
|
|
20401
|
+
}
|
|
20402
|
+
let baseSchema = convertBaseSchema(schema2, ctx);
|
|
20403
|
+
const hasExplicitType = schema2.type || schema2.enum !== void 0 || schema2.const !== void 0;
|
|
20404
|
+
if (schema2.anyOf && Array.isArray(schema2.anyOf)) {
|
|
20405
|
+
const options = schema2.anyOf.map((s) => convertSchema(s, ctx));
|
|
20406
|
+
const anyOfUnion = z.union(options);
|
|
20407
|
+
baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
|
|
20408
|
+
}
|
|
20409
|
+
if (schema2.oneOf && Array.isArray(schema2.oneOf)) {
|
|
20410
|
+
const options = schema2.oneOf.map((s) => convertSchema(s, ctx));
|
|
20411
|
+
const oneOfUnion = z.xor(options);
|
|
20412
|
+
baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
|
|
20413
|
+
}
|
|
20414
|
+
if (schema2.allOf && Array.isArray(schema2.allOf)) {
|
|
20415
|
+
if (schema2.allOf.length === 0) {
|
|
20416
|
+
baseSchema = hasExplicitType ? baseSchema : z.any();
|
|
20417
|
+
} else {
|
|
20418
|
+
let result = hasExplicitType ? baseSchema : convertSchema(schema2.allOf[0], ctx);
|
|
20419
|
+
const startIdx = hasExplicitType ? 0 : 1;
|
|
20420
|
+
for (let i = startIdx; i < schema2.allOf.length; i++) {
|
|
20421
|
+
result = z.intersection(result, convertSchema(schema2.allOf[i], ctx));
|
|
20422
|
+
}
|
|
20423
|
+
baseSchema = result;
|
|
20424
|
+
}
|
|
20425
|
+
}
|
|
20426
|
+
if (schema2.nullable === true && ctx.version === "openapi-3.0") {
|
|
20427
|
+
baseSchema = z.nullable(baseSchema);
|
|
20428
|
+
}
|
|
20429
|
+
if (schema2.readOnly === true) {
|
|
20430
|
+
baseSchema = z.readonly(baseSchema);
|
|
20431
|
+
}
|
|
20432
|
+
if (schema2.default !== void 0) {
|
|
20433
|
+
baseSchema = baseSchema.default(schema2.default);
|
|
20434
|
+
}
|
|
20435
|
+
const extraMeta = {};
|
|
20436
|
+
const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
|
|
20437
|
+
for (const key of coreMetadataKeys) {
|
|
20438
|
+
if (key in schema2) {
|
|
20439
|
+
extraMeta[key] = schema2[key];
|
|
20440
|
+
}
|
|
20441
|
+
}
|
|
20442
|
+
const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
|
|
20443
|
+
for (const key of contentMetadataKeys) {
|
|
20444
|
+
if (key in schema2) {
|
|
20445
|
+
extraMeta[key] = schema2[key];
|
|
20446
|
+
}
|
|
20447
|
+
}
|
|
20448
|
+
for (const key of Object.keys(schema2)) {
|
|
20449
|
+
if (!RECOGNIZED_KEYS.has(key)) {
|
|
20450
|
+
extraMeta[key] = schema2[key];
|
|
20451
|
+
}
|
|
20452
|
+
}
|
|
20453
|
+
if (Object.keys(extraMeta).length > 0) {
|
|
20454
|
+
ctx.registry.add(baseSchema, extraMeta);
|
|
20455
|
+
}
|
|
20456
|
+
if (schema2.description) {
|
|
20457
|
+
baseSchema = baseSchema.describe(schema2.description);
|
|
20458
|
+
}
|
|
20459
|
+
return baseSchema;
|
|
20460
|
+
}
|
|
20461
|
+
function fromJSONSchema(schema2, params) {
|
|
20462
|
+
if (typeof schema2 === "boolean") {
|
|
20463
|
+
return schema2 ? z.any() : z.never();
|
|
20464
|
+
}
|
|
20465
|
+
let normalized;
|
|
20466
|
+
try {
|
|
20467
|
+
normalized = JSON.parse(JSON.stringify(schema2));
|
|
20468
|
+
} catch {
|
|
20469
|
+
throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
|
|
20470
|
+
}
|
|
20471
|
+
const version2 = detectVersion(normalized, params?.defaultTarget);
|
|
20472
|
+
const defs = normalized.$defs || normalized.definitions || {};
|
|
20473
|
+
const ctx = {
|
|
20474
|
+
version: version2,
|
|
20475
|
+
defs,
|
|
20476
|
+
refs: /* @__PURE__ */ new Map(),
|
|
20477
|
+
processing: /* @__PURE__ */ new Set(),
|
|
20478
|
+
rootSchema: normalized,
|
|
20479
|
+
registry: params?.registry ?? globalRegistry
|
|
20480
|
+
};
|
|
20481
|
+
return convertSchema(normalized, ctx);
|
|
20482
|
+
}
|
|
20483
|
+
|
|
20484
|
+
// node_modules/zod/v4/classic/coerce.js
|
|
20485
|
+
var coerce_exports = {};
|
|
20486
|
+
__export(coerce_exports, {
|
|
20487
|
+
bigint: () => bigint3,
|
|
20488
|
+
boolean: () => boolean3,
|
|
20489
|
+
date: () => date4,
|
|
20490
|
+
number: () => number3,
|
|
20491
|
+
string: () => string3
|
|
20492
|
+
});
|
|
20493
|
+
function string3(params) {
|
|
20494
|
+
return _coercedString(ZodString, params);
|
|
20495
|
+
}
|
|
20496
|
+
function number3(params) {
|
|
20497
|
+
return _coercedNumber(ZodNumber, params);
|
|
20498
|
+
}
|
|
20499
|
+
function boolean3(params) {
|
|
20500
|
+
return _coercedBoolean(ZodBoolean, params);
|
|
20501
|
+
}
|
|
20502
|
+
function bigint3(params) {
|
|
20503
|
+
return _coercedBigint(ZodBigInt, params);
|
|
20504
|
+
}
|
|
20505
|
+
function date4(params) {
|
|
20506
|
+
return _coercedDate(ZodDate, params);
|
|
20507
|
+
}
|
|
20508
|
+
|
|
20509
|
+
// node_modules/zod/v4/classic/external.js
|
|
20510
|
+
config(en_default());
|
|
20511
|
+
|
|
20512
|
+
// src/mcp/protocol.ts
|
|
20513
|
+
var MODERN_PROTOCOL_VERSION = "2026-07-28";
|
|
20514
|
+
var LEGACY_PROTOCOL_VERSION = "2025-11-25";
|
|
20515
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [MODERN_PROTOCOL_VERSION, LEGACY_PROTOCOL_VERSION];
|
|
20516
|
+
var JsonRpcRequestSchema = external_exports.object({
|
|
20517
|
+
jsonrpc: external_exports.literal("2.0"),
|
|
20518
|
+
id: external_exports.union([external_exports.string(), external_exports.number(), external_exports.null()]).optional(),
|
|
20519
|
+
method: external_exports.string().trim().min(1),
|
|
20520
|
+
params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
20521
|
+
}).strict();
|
|
20522
|
+
var ModernClientMetadataSchema = external_exports.object({
|
|
20523
|
+
"io.modelcontextprotocol/clientCapabilities": external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
20524
|
+
"io.modelcontextprotocol/clientInfo": external_exports.object({
|
|
20525
|
+
name: external_exports.string().trim().min(1),
|
|
20526
|
+
version: external_exports.string().trim().min(1)
|
|
20527
|
+
}).passthrough(),
|
|
20528
|
+
"io.modelcontextprotocol/protocolVersion": external_exports.literal(MODERN_PROTOCOL_VERSION)
|
|
20529
|
+
}).passthrough();
|
|
20530
|
+
var UnsupportedProtocolVersionError = class extends Error {
|
|
20531
|
+
protocolVersion;
|
|
20532
|
+
supportedVersions = SUPPORTED_PROTOCOL_VERSIONS;
|
|
20533
|
+
constructor(protocolVersion) {
|
|
20534
|
+
super(`Unsupported MCP protocol version: ${protocolVersion}`);
|
|
20535
|
+
this.name = "UnsupportedProtocolVersionError";
|
|
20536
|
+
this.protocolVersion = protocolVersion;
|
|
20537
|
+
}
|
|
20538
|
+
};
|
|
20539
|
+
var RequestMetadataMismatchError = class extends Error {
|
|
20540
|
+
field;
|
|
20541
|
+
expected;
|
|
20542
|
+
actual;
|
|
20543
|
+
constructor(field, expected, actual) {
|
|
20544
|
+
super(`MCP request metadata mismatch for ${field}.`);
|
|
20545
|
+
this.name = "RequestMetadataMismatchError";
|
|
20546
|
+
this.field = field;
|
|
20547
|
+
this.expected = expected;
|
|
20548
|
+
this.actual = actual;
|
|
20549
|
+
}
|
|
20550
|
+
};
|
|
20551
|
+
function parseJsonRpcRequest(input) {
|
|
20552
|
+
return JsonRpcRequestSchema.parse(input);
|
|
20553
|
+
}
|
|
20554
|
+
function resolveProtocolVersion(request, context = {}) {
|
|
20555
|
+
const meta3 = isRecord(request.params?._meta) ? request.params._meta : void 0;
|
|
20556
|
+
const initializeVersion = request.method === "initialize" && typeof request.params?.protocolVersion === "string" ? request.params.protocolVersion : void 0;
|
|
20557
|
+
const requested = context.protocolVersion ?? stringValue(meta3?.["io.modelcontextprotocol/protocolVersion"]) ?? initializeVersion ?? MODERN_PROTOCOL_VERSION;
|
|
20558
|
+
if (!isSupportedProtocolVersion(requested)) {
|
|
20559
|
+
throw new UnsupportedProtocolVersionError(requested);
|
|
20560
|
+
}
|
|
20561
|
+
return requested;
|
|
20562
|
+
}
|
|
20563
|
+
function assertRequestMetadata(request, protocolVersion, context = {}) {
|
|
20564
|
+
const meta3 = isRecord(request.params?._meta) ? request.params._meta : void 0;
|
|
20565
|
+
const metaVersion = stringValue(meta3?.["io.modelcontextprotocol/protocolVersion"]);
|
|
20566
|
+
if (protocolVersion === MODERN_PROTOCOL_VERSION) {
|
|
20567
|
+
const parsed = ModernClientMetadataSchema.safeParse(meta3);
|
|
20568
|
+
if (!parsed.success) {
|
|
20569
|
+
throw new RequestMetadataMismatchError("_meta", "valid modern client metadata");
|
|
20570
|
+
}
|
|
20571
|
+
}
|
|
20572
|
+
if (context.protocolVersion && metaVersion && context.protocolVersion !== metaVersion) {
|
|
20573
|
+
throw new RequestMetadataMismatchError("protocolVersion", context.protocolVersion, metaVersion);
|
|
20574
|
+
}
|
|
20575
|
+
if (context.method && context.method !== request.method) {
|
|
20576
|
+
throw new RequestMetadataMismatchError("method", context.method, request.method);
|
|
20577
|
+
}
|
|
20578
|
+
const toolName = typeof request.params?.name === "string" ? request.params.name : void 0;
|
|
20579
|
+
if (context.toolName && context.toolName !== toolName) {
|
|
20580
|
+
throw new RequestMetadataMismatchError("name", context.toolName, toolName);
|
|
20581
|
+
}
|
|
20582
|
+
}
|
|
20583
|
+
function isSupportedProtocolVersion(value) {
|
|
20584
|
+
return SUPPORTED_PROTOCOL_VERSIONS.some((version2) => version2 === value);
|
|
20585
|
+
}
|
|
20586
|
+
function jsonRpcSuccess(id, result) {
|
|
20587
|
+
return { jsonrpc: "2.0", id, result };
|
|
20588
|
+
}
|
|
20589
|
+
function jsonRpcFailure(id, error51) {
|
|
20590
|
+
return { jsonrpc: "2.0", id, error: error51 };
|
|
20591
|
+
}
|
|
20592
|
+
function isRecord(value) {
|
|
20593
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20594
|
+
}
|
|
20595
|
+
function stringValue(value) {
|
|
20596
|
+
return typeof value === "string" ? value : void 0;
|
|
20597
|
+
}
|
|
20598
|
+
|
|
20599
|
+
// src/version.ts
|
|
20600
|
+
var VERSION = "0.7.0-alpha.5";
|
|
20601
|
+
|
|
20602
|
+
// src/worker/http.ts
|
|
20603
|
+
var HEALTH_PATH = "/healthz";
|
|
20604
|
+
var READY_PATH = "/readyz";
|
|
20605
|
+
var MCP_PATH = "/mcp";
|
|
20606
|
+
var SESSION_PATH = "/session";
|
|
20607
|
+
var SESSION_TOUCH_PATH = "/session/touch";
|
|
20608
|
+
var WORKER_SERVICE_ID = "moodle-mcp";
|
|
20609
|
+
var WORKER_SERVICE_VERSION = VERSION;
|
|
20610
|
+
function createWorkerHandler(dependencies) {
|
|
20611
|
+
return {
|
|
20612
|
+
async fetch(request, env) {
|
|
20613
|
+
const url2 = new URL(request.url);
|
|
20614
|
+
const authorityProblem = validateRequestAuthority(request, url2, env.EXPECTED_HOSTS ?? env.EXPECTED_HOST);
|
|
20615
|
+
if (authorityProblem) return authorityProblem;
|
|
20616
|
+
if (hasQueryCredential(url2)) return unauthorized("Bearer credentials are not accepted in the query string.");
|
|
20617
|
+
if (url2.pathname === HEALTH_PATH && request.method === "GET") {
|
|
20618
|
+
return Response.json(
|
|
20619
|
+
{ status: "pass", serviceId: WORKER_SERVICE_ID, version: WORKER_SERVICE_VERSION },
|
|
20620
|
+
{ headers: { "content-type": "application/health+json; charset=utf-8" } }
|
|
20621
|
+
);
|
|
20622
|
+
}
|
|
20623
|
+
if (url2.pathname === MCP_PATH && request.method !== "POST") {
|
|
20624
|
+
return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The MCP transport accepts POST requests only.", {
|
|
20625
|
+
allow: "POST"
|
|
20626
|
+
});
|
|
20627
|
+
}
|
|
20628
|
+
if (url2.pathname === MCP_PATH) {
|
|
20629
|
+
if (!await verifyBearerToken(request.headers.get("authorization"), [
|
|
20630
|
+
env.MCP_ACCESS_TOKEN_DIGEST,
|
|
20631
|
+
activePreviousDigest(env.MCP_ACCESS_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
|
|
20632
|
+
])) {
|
|
20633
|
+
return unauthorized();
|
|
20634
|
+
}
|
|
20635
|
+
const server = typeof dependencies.mcpServer === "function" ? dependencies.mcpServer(env) : dependencies.mcpServer;
|
|
20636
|
+
return handleMcpRequest(request, server);
|
|
20684
20637
|
}
|
|
20685
|
-
|
|
20638
|
+
if (url2.pathname === READY_PATH && request.method === "GET") {
|
|
20639
|
+
if (!await authorizeSessionSync(request, env)) return unauthorized();
|
|
20640
|
+
const broker = resolveBroker(dependencies, env);
|
|
20641
|
+
if (broker instanceof Response) return broker;
|
|
20642
|
+
return broker.ready();
|
|
20643
|
+
}
|
|
20644
|
+
if (url2.pathname === SESSION_PATH && request.method === "PUT") {
|
|
20645
|
+
if (!await authorizeSessionSync(request, env)) return unauthorized();
|
|
20646
|
+
const broker = resolveBroker(dependencies, env);
|
|
20647
|
+
if (broker instanceof Response) return broker;
|
|
20648
|
+
return broker.replaceSession(request);
|
|
20649
|
+
}
|
|
20650
|
+
if (url2.pathname === SESSION_TOUCH_PATH && request.method === "POST") {
|
|
20651
|
+
if (!await authorizeSessionSync(request, env)) return unauthorized();
|
|
20652
|
+
const broker = resolveBroker(dependencies, env);
|
|
20653
|
+
if (broker instanceof Response) return broker;
|
|
20654
|
+
return broker.touch();
|
|
20655
|
+
}
|
|
20656
|
+
if (url2.pathname === READY_PATH || url2.pathname === SESSION_PATH || url2.pathname === SESSION_TOUCH_PATH) {
|
|
20657
|
+
const method = url2.pathname === READY_PATH ? "GET" : url2.pathname === SESSION_PATH ? "PUT" : "POST";
|
|
20658
|
+
return problemResponse(405, "METHOD_NOT_ALLOWED", "Method Not Allowed", "The session route does not accept this method.", {
|
|
20659
|
+
allow: method
|
|
20660
|
+
});
|
|
20661
|
+
}
|
|
20662
|
+
return problemResponse(404, "NOT_FOUND", "Not Found", "The requested route does not exist.");
|
|
20686
20663
|
}
|
|
20687
|
-
|
|
20688
|
-
throw new Error(`Unsupported type: ${type}`);
|
|
20689
|
-
}
|
|
20690
|
-
return zodSchema;
|
|
20664
|
+
};
|
|
20691
20665
|
}
|
|
20692
|
-
function
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
|
|
20696
|
-
|
|
20697
|
-
|
|
20698
|
-
|
|
20699
|
-
|
|
20700
|
-
|
|
20701
|
-
|
|
20666
|
+
function createDurableObjectBrokerApi(namespace) {
|
|
20667
|
+
const stub = namespace.get(namespace.idFromName("primary"));
|
|
20668
|
+
return {
|
|
20669
|
+
async handleMcp(body, context) {
|
|
20670
|
+
const response = await stub.fetch(new Request("https://session-broker/mcp", {
|
|
20671
|
+
method: "POST",
|
|
20672
|
+
headers: { "content-type": "application/json" },
|
|
20673
|
+
body: JSON.stringify({ request: body, context })
|
|
20674
|
+
}));
|
|
20675
|
+
if (!response.ok) {
|
|
20676
|
+
const problem = await safeProblem(response);
|
|
20677
|
+
throw new SessionBrokerMcpError(response.status, problem?.code);
|
|
20678
|
+
}
|
|
20679
|
+
const envelope = await response.json();
|
|
20680
|
+
return envelope.response ?? null;
|
|
20681
|
+
},
|
|
20682
|
+
ready: () => stub.fetch(new Request("https://session-broker/readyz")),
|
|
20683
|
+
async replaceSession(request) {
|
|
20684
|
+
return stub.fetch(new Request("https://session-broker/session", {
|
|
20685
|
+
method: "PUT",
|
|
20686
|
+
headers: { "content-type": request.headers.get("content-type") ?? "application/json" },
|
|
20687
|
+
body: await request.arrayBuffer()
|
|
20688
|
+
}));
|
|
20689
|
+
},
|
|
20690
|
+
touch: () => stub.fetch(new Request("https://session-broker/session/touch", { method: "POST" }))
|
|
20691
|
+
};
|
|
20692
|
+
}
|
|
20693
|
+
var SessionBrokerMcpError = class extends Error {
|
|
20694
|
+
constructor(status, code) {
|
|
20695
|
+
super("The session broker could not complete the MCP request.");
|
|
20696
|
+
this.status = status;
|
|
20697
|
+
this.code = code;
|
|
20698
|
+
this.name = "SessionBrokerMcpError";
|
|
20702
20699
|
}
|
|
20703
|
-
|
|
20704
|
-
|
|
20705
|
-
|
|
20706
|
-
|
|
20700
|
+
status;
|
|
20701
|
+
code;
|
|
20702
|
+
};
|
|
20703
|
+
function resolveBroker(dependencies, env) {
|
|
20704
|
+
if (dependencies.broker) return dependencies.broker(env);
|
|
20705
|
+
if (env.SESSION_BROKER) return createDurableObjectBrokerApi(env.SESSION_BROKER);
|
|
20706
|
+
return problemResponse(503, "SESSION_BROKER_UNAVAILABLE", "Service Unavailable", "The session broker is unavailable.");
|
|
20707
|
+
}
|
|
20708
|
+
function validateRequestAuthority(request, url2, configuredHosts) {
|
|
20709
|
+
const expectedHosts = (configuredHosts ?? url2.host).split(",").map((host) => host.trim().toLowerCase()).filter(Boolean);
|
|
20710
|
+
const requestHost = (request.headers.get("host") ?? url2.host).toLowerCase();
|
|
20711
|
+
if (requestHost !== url2.host.toLowerCase() || !expectedHosts.includes(requestHost)) {
|
|
20712
|
+
return problemResponse(403, "INVALID_HOST", "Forbidden", "The request Host is not allowed.");
|
|
20707
20713
|
}
|
|
20708
|
-
|
|
20709
|
-
|
|
20710
|
-
|
|
20711
|
-
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
for (let i = startIdx; i < schema2.allOf.length; i++) {
|
|
20715
|
-
result = z.intersection(result, convertSchema(schema2.allOf[i], ctx));
|
|
20716
|
-
}
|
|
20717
|
-
baseSchema = result;
|
|
20714
|
+
const origin = request.headers.get("origin");
|
|
20715
|
+
if (!origin) return null;
|
|
20716
|
+
try {
|
|
20717
|
+
const parsed = new URL(origin);
|
|
20718
|
+
if (parsed.origin !== url2.origin || !expectedHosts.includes(parsed.host.toLowerCase())) {
|
|
20719
|
+
return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
|
|
20718
20720
|
}
|
|
20721
|
+
} catch {
|
|
20722
|
+
return problemResponse(403, "INVALID_ORIGIN", "Forbidden", "The request Origin is not allowed.");
|
|
20719
20723
|
}
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20723
|
-
|
|
20724
|
-
|
|
20724
|
+
return null;
|
|
20725
|
+
}
|
|
20726
|
+
function unauthorized(detail = "A valid Bearer token is required.") {
|
|
20727
|
+
return problemResponse(401, "INVALID_BEARER_TOKEN", "Unauthorized", detail, {
|
|
20728
|
+
"www-authenticate": 'Bearer realm="moodle-mcp"'
|
|
20729
|
+
});
|
|
20730
|
+
}
|
|
20731
|
+
async function handleMcpRequest(request, server) {
|
|
20732
|
+
if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
|
|
20733
|
+
return problemResponse(415, "UNSUPPORTED_MEDIA_TYPE", "Unsupported Media Type", "MCP requests must use application/json.");
|
|
20725
20734
|
}
|
|
20726
|
-
|
|
20727
|
-
|
|
20735
|
+
let body;
|
|
20736
|
+
try {
|
|
20737
|
+
body = await request.json();
|
|
20738
|
+
} catch {
|
|
20739
|
+
return problemResponse(400, "INVALID_JSON", "Bad Request", "The request body is not valid JSON.");
|
|
20728
20740
|
}
|
|
20729
|
-
const
|
|
20730
|
-
const
|
|
20731
|
-
|
|
20732
|
-
|
|
20733
|
-
|
|
20741
|
+
const protocolVersion = request.headers.get("mcp-protocol-version");
|
|
20742
|
+
const metadataProblem = validateProtocolMetadata(request.headers, body, protocolVersion);
|
|
20743
|
+
if (metadataProblem) return metadataProblem;
|
|
20744
|
+
try {
|
|
20745
|
+
const response = await server.handle(body, {
|
|
20746
|
+
protocolVersion,
|
|
20747
|
+
method: request.headers.get("mcp-method") ?? void 0,
|
|
20748
|
+
toolName: request.headers.get("mcp-name") ?? void 0
|
|
20749
|
+
});
|
|
20750
|
+
if (response === null) return new Response(null, { status: 202 });
|
|
20751
|
+
if (isUnsupportedProtocolVersionResponse(response)) {
|
|
20752
|
+
return Response.json(response, {
|
|
20753
|
+
status: 400,
|
|
20754
|
+
headers: { "cache-control": "private, no-store" }
|
|
20755
|
+
});
|
|
20734
20756
|
}
|
|
20735
|
-
|
|
20736
|
-
|
|
20737
|
-
|
|
20738
|
-
|
|
20739
|
-
|
|
20757
|
+
if (request.headers.get("accept")?.toLowerCase().includes("text/event-stream")) {
|
|
20758
|
+
return new Response(`event: message
|
|
20759
|
+
data: ${JSON.stringify(response)}
|
|
20760
|
+
|
|
20761
|
+
`, {
|
|
20762
|
+
headers: {
|
|
20763
|
+
"cache-control": "private, no-store",
|
|
20764
|
+
"content-type": "text/event-stream; charset=utf-8"
|
|
20765
|
+
}
|
|
20766
|
+
});
|
|
20740
20767
|
}
|
|
20741
|
-
|
|
20742
|
-
|
|
20743
|
-
if (
|
|
20744
|
-
|
|
20768
|
+
return Response.json(response, { headers: { "cache-control": "private, no-store" } });
|
|
20769
|
+
} catch (error51) {
|
|
20770
|
+
if (error51 instanceof SessionBrokerMcpError) {
|
|
20771
|
+
const status = error51.status === 503 ? 503 : 500;
|
|
20772
|
+
const code = error51.code === "SESSION_MISSING" || error51.code === "SESSION_EXPIRED" ? error51.code : "SESSION_UNAVAILABLE";
|
|
20773
|
+
return problemResponse(status, code, "Service Unavailable", "The Moodle session is not ready.");
|
|
20745
20774
|
}
|
|
20775
|
+
if (error51 instanceof Error && error51.name === "UnsupportedProtocolVersionError") {
|
|
20776
|
+
const requested = "protocolVersion" in error51 && typeof error51.protocolVersion === "string" ? error51.protocolVersion : protocolVersion ?? "unknown";
|
|
20777
|
+
return Response.json({
|
|
20778
|
+
jsonrpc: "2.0",
|
|
20779
|
+
id: jsonRpcRequestId(body),
|
|
20780
|
+
error: {
|
|
20781
|
+
code: -32022,
|
|
20782
|
+
message: "Unsupported protocol version",
|
|
20783
|
+
data: { supported: [...SUPPORTED_PROTOCOL_VERSIONS], requested }
|
|
20784
|
+
}
|
|
20785
|
+
}, {
|
|
20786
|
+
status: 400,
|
|
20787
|
+
headers: { "cache-control": "private, no-store" }
|
|
20788
|
+
});
|
|
20789
|
+
}
|
|
20790
|
+
return problemResponse(500, "MCP_REQUEST_FAILED", "Internal Server Error", "The MCP request could not be completed.");
|
|
20746
20791
|
}
|
|
20747
|
-
if (Object.keys(extraMeta).length > 0) {
|
|
20748
|
-
ctx.registry.add(baseSchema, extraMeta);
|
|
20749
|
-
}
|
|
20750
|
-
if (schema2.description) {
|
|
20751
|
-
baseSchema = baseSchema.describe(schema2.description);
|
|
20752
|
-
}
|
|
20753
|
-
return baseSchema;
|
|
20754
20792
|
}
|
|
20755
|
-
function
|
|
20756
|
-
if (typeof schema2 === "boolean") {
|
|
20757
|
-
return schema2 ? z.any() : z.never();
|
|
20758
|
-
}
|
|
20759
|
-
let normalized;
|
|
20793
|
+
async function safeProblem(response) {
|
|
20760
20794
|
try {
|
|
20761
|
-
|
|
20795
|
+
const value = await response.json();
|
|
20796
|
+
return isRecord2(value) && (value.code === void 0 || typeof value.code === "string") ? value : null;
|
|
20762
20797
|
} catch {
|
|
20763
|
-
|
|
20798
|
+
return null;
|
|
20799
|
+
}
|
|
20800
|
+
}
|
|
20801
|
+
function validateProtocolMetadata(headers, body, protocolVersion) {
|
|
20802
|
+
if (!protocolVersion) {
|
|
20803
|
+
return headerMismatchResponse(body, "MCP-Protocol-Version is required.");
|
|
20804
|
+
}
|
|
20805
|
+
const params = isRecord2(body.params) ? body.params : void 0;
|
|
20806
|
+
const metadata = params && isRecord2(params._meta) ? params._meta : void 0;
|
|
20807
|
+
const bodyVersion = metadata?.["io.modelcontextprotocol/protocolVersion"];
|
|
20808
|
+
if (bodyVersion !== void 0 && bodyVersion !== protocolVersion) {
|
|
20809
|
+
return headerMismatchResponse(body, "MCP protocol metadata does not match the HTTP headers.");
|
|
20810
|
+
}
|
|
20811
|
+
if (typeof body.method !== "string") {
|
|
20812
|
+
return problemResponse(400, "MCP_PROTOCOL_METADATA_INVALID", "Bad Request", "The JSON-RPC method is required.");
|
|
20813
|
+
}
|
|
20814
|
+
const headerMethod = headers.get("mcp-method");
|
|
20815
|
+
const headerName = headers.get("mcp-name") ?? void 0;
|
|
20816
|
+
const paramsName = params && typeof params.name === "string" ? params.name : void 0;
|
|
20817
|
+
const requiresModernHeaders = protocolVersion !== LEGACY_PROTOCOL_VERSION;
|
|
20818
|
+
const hasModernHeaders = headerMethod !== null || headerName !== void 0;
|
|
20819
|
+
if ((requiresModernHeaders || hasModernHeaders) && (headerMethod !== body.method || headerName !== paramsName)) {
|
|
20820
|
+
return headerMismatchResponse(body, "MCP method metadata does not match the JSON-RPC request.");
|
|
20821
|
+
}
|
|
20822
|
+
return null;
|
|
20823
|
+
}
|
|
20824
|
+
function headerMismatchResponse(body, detail) {
|
|
20825
|
+
return Response.json({
|
|
20826
|
+
jsonrpc: "2.0",
|
|
20827
|
+
id: jsonRpcRequestId(body),
|
|
20828
|
+
error: {
|
|
20829
|
+
code: -32020,
|
|
20830
|
+
message: "HeaderMismatch",
|
|
20831
|
+
data: { detail }
|
|
20832
|
+
}
|
|
20833
|
+
}, {
|
|
20834
|
+
status: 400,
|
|
20835
|
+
headers: { "cache-control": "private, no-store" }
|
|
20836
|
+
});
|
|
20837
|
+
}
|
|
20838
|
+
function jsonRpcRequestId(body) {
|
|
20839
|
+
return typeof body.id === "string" || typeof body.id === "number" || body.id === null ? body.id : null;
|
|
20840
|
+
}
|
|
20841
|
+
function isUnsupportedProtocolVersionResponse(value) {
|
|
20842
|
+
if (!isRecord2(value) || !isRecord2(value.error)) return false;
|
|
20843
|
+
return value.jsonrpc === "2.0" && value.error.code === -32022;
|
|
20844
|
+
}
|
|
20845
|
+
function authorizeSessionSync(request, env) {
|
|
20846
|
+
return verifyBearerToken(request.headers.get("authorization"), [
|
|
20847
|
+
env.SESSION_SYNC_TOKEN_DIGEST,
|
|
20848
|
+
activePreviousDigest(env.SESSION_SYNC_TOKEN_PREVIOUS_DIGEST, env.TOKEN_OVERLAP_EXPIRES_AT)
|
|
20849
|
+
]);
|
|
20850
|
+
}
|
|
20851
|
+
function activePreviousDigest(digest, expiresAt2) {
|
|
20852
|
+
if (!digest || !expiresAt2) return void 0;
|
|
20853
|
+
const expiration = Number(expiresAt2);
|
|
20854
|
+
return Number.isFinite(expiration) && Date.now() < expiration ? digest : void 0;
|
|
20855
|
+
}
|
|
20856
|
+
function isRecord2(value) {
|
|
20857
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20858
|
+
}
|
|
20859
|
+
|
|
20860
|
+
// src/worker/crypto.ts
|
|
20861
|
+
async function createEncryptionKeyring(currentSecret, previousSecret) {
|
|
20862
|
+
if (!currentSecret) throw new Error("SESSION_ENCRYPTION_KEY is required.");
|
|
20863
|
+
const current = await deriveEncryptionKey(currentSecret);
|
|
20864
|
+
const keys = /* @__PURE__ */ new Map([[current.id, current.key]]);
|
|
20865
|
+
if (previousSecret) {
|
|
20866
|
+
const previous = await deriveEncryptionKey(previousSecret);
|
|
20867
|
+
keys.set(previous.id, previous.key);
|
|
20764
20868
|
}
|
|
20765
|
-
|
|
20766
|
-
|
|
20767
|
-
|
|
20768
|
-
|
|
20769
|
-
|
|
20770
|
-
|
|
20771
|
-
|
|
20772
|
-
|
|
20773
|
-
|
|
20869
|
+
return { current, keys };
|
|
20870
|
+
}
|
|
20871
|
+
async function encryptValue(value, keyring) {
|
|
20872
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
20873
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
20874
|
+
{ name: "AES-GCM", iv },
|
|
20875
|
+
keyring.current.key,
|
|
20876
|
+
new TextEncoder().encode(value)
|
|
20877
|
+
);
|
|
20878
|
+
return JSON.stringify({
|
|
20879
|
+
version: 1,
|
|
20880
|
+
keyId: keyring.current.id,
|
|
20881
|
+
iv: toBase64Url(iv),
|
|
20882
|
+
ciphertext: toBase64Url(new Uint8Array(ciphertext))
|
|
20883
|
+
});
|
|
20884
|
+
}
|
|
20885
|
+
async function decryptValue(encrypted, keyring) {
|
|
20886
|
+
const envelope = parseEnvelope(encrypted);
|
|
20887
|
+
const key = keyring.keys.get(envelope.keyId);
|
|
20888
|
+
if (!key) throw new Error("The session encryption key is unavailable.");
|
|
20889
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
20890
|
+
{ name: "AES-GCM", iv: fromBase64Url(envelope.iv) },
|
|
20891
|
+
key,
|
|
20892
|
+
fromBase64Url(envelope.ciphertext)
|
|
20893
|
+
);
|
|
20894
|
+
return {
|
|
20895
|
+
value: new TextDecoder().decode(plaintext),
|
|
20896
|
+
keyId: envelope.keyId,
|
|
20897
|
+
needsRotation: envelope.keyId !== keyring.current.id
|
|
20774
20898
|
};
|
|
20775
|
-
return convertSchema(normalized, ctx);
|
|
20776
20899
|
}
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
20780
|
-
|
|
20781
|
-
|
|
20782
|
-
boolean: () => boolean3,
|
|
20783
|
-
date: () => date4,
|
|
20784
|
-
number: () => number3,
|
|
20785
|
-
string: () => string3
|
|
20786
|
-
});
|
|
20787
|
-
function string3(params) {
|
|
20788
|
-
return _coercedString(ZodString, params);
|
|
20900
|
+
async function deriveEncryptionKey(secret) {
|
|
20901
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret)));
|
|
20902
|
+
const key = await crypto.subtle.importKey("raw", digest, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
20903
|
+
const id = Array.from(digest.slice(0, 8), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
20904
|
+
return { id, key };
|
|
20789
20905
|
}
|
|
20790
|
-
function
|
|
20791
|
-
|
|
20906
|
+
function parseEnvelope(value) {
|
|
20907
|
+
let parsed;
|
|
20908
|
+
try {
|
|
20909
|
+
parsed = JSON.parse(value);
|
|
20910
|
+
} catch {
|
|
20911
|
+
throw new Error("The encrypted session record is invalid.");
|
|
20912
|
+
}
|
|
20913
|
+
if (!isRecord3(parsed) || parsed.version !== 1 || typeof parsed.keyId !== "string" || typeof parsed.iv !== "string" || typeof parsed.ciphertext !== "string") {
|
|
20914
|
+
throw new Error("The encrypted session record is invalid.");
|
|
20915
|
+
}
|
|
20916
|
+
return parsed;
|
|
20792
20917
|
}
|
|
20793
|
-
function
|
|
20794
|
-
|
|
20918
|
+
function toBase64Url(bytes) {
|
|
20919
|
+
let binary = "";
|
|
20920
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
20921
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
20795
20922
|
}
|
|
20796
|
-
function
|
|
20797
|
-
|
|
20923
|
+
function fromBase64Url(value) {
|
|
20924
|
+
const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
20925
|
+
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
20926
|
+
const bytes = new Uint8Array(binary.length);
|
|
20927
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
20928
|
+
return bytes;
|
|
20798
20929
|
}
|
|
20799
|
-
function
|
|
20800
|
-
return
|
|
20930
|
+
function isRecord3(value) {
|
|
20931
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20801
20932
|
}
|
|
20802
20933
|
|
|
20803
|
-
// node_modules/zod/v4/classic/external.js
|
|
20804
|
-
config(en_default());
|
|
20805
|
-
|
|
20806
20934
|
// src/constants.ts
|
|
20807
20935
|
var PACKAGE_NAME = "moodle-cli";
|
|
20808
20936
|
var NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
@@ -20917,19 +21045,19 @@ function parseUserInfo(value) {
|
|
|
20917
21045
|
const data = asRecord(value);
|
|
20918
21046
|
return {
|
|
20919
21047
|
userid: numberValue(data.userid),
|
|
20920
|
-
username:
|
|
20921
|
-
fullname:
|
|
20922
|
-
sitename:
|
|
20923
|
-
siteurl:
|
|
20924
|
-
lang:
|
|
21048
|
+
username: stringValue2(data.username),
|
|
21049
|
+
fullname: stringValue2(data.fullname),
|
|
21050
|
+
sitename: stringValue2(data.sitename),
|
|
21051
|
+
siteurl: stringValue2(data.siteurl),
|
|
21052
|
+
lang: stringValue2(data.lang)
|
|
20925
21053
|
};
|
|
20926
21054
|
}
|
|
20927
21055
|
function parseCourse(value, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
20928
21056
|
const data = asRecord(value);
|
|
20929
21057
|
const course = {
|
|
20930
21058
|
id: numberValue(data.id),
|
|
20931
|
-
shortname:
|
|
20932
|
-
fullname:
|
|
21059
|
+
shortname: stringValue2(data.shortname),
|
|
21060
|
+
fullname: stringValue2(data.fullname),
|
|
20933
21061
|
category: numberValue(data.category),
|
|
20934
21062
|
visible: booleanValue(data.visible, true),
|
|
20935
21063
|
startdate: numberValue(data.startdate)
|
|
@@ -20947,21 +21075,21 @@ function parseActivity(value) {
|
|
|
20947
21075
|
const data = asRecord(value);
|
|
20948
21076
|
return {
|
|
20949
21077
|
id: numberValue(data.id),
|
|
20950
|
-
name:
|
|
20951
|
-
modname:
|
|
20952
|
-
url:
|
|
21078
|
+
name: stringValue2(data.name),
|
|
21079
|
+
modname: stringValue2(data.modname),
|
|
21080
|
+
url: stringValue2(data.url),
|
|
20953
21081
|
visible: booleanValue(data.visible, true),
|
|
20954
|
-
description:
|
|
21082
|
+
description: stringValue2(data.description)
|
|
20955
21083
|
};
|
|
20956
21084
|
}
|
|
20957
21085
|
function parseSection(value) {
|
|
20958
21086
|
const data = asRecord(value);
|
|
20959
21087
|
return {
|
|
20960
21088
|
id: numberValue(data.id),
|
|
20961
|
-
name:
|
|
21089
|
+
name: stringValue2(data.name),
|
|
20962
21090
|
section: numberValue(data.section),
|
|
20963
21091
|
visible: booleanValue(data.visible, true),
|
|
20964
|
-
summary:
|
|
21092
|
+
summary: stringValue2(data.summary),
|
|
20965
21093
|
activities: asArray(data.modules).map((item) => parseActivity(item))
|
|
20966
21094
|
};
|
|
20967
21095
|
}
|
|
@@ -20975,13 +21103,13 @@ function parseCourseFormatState(value, baseUrl) {
|
|
|
20975
21103
|
for (const item of asArray(state.cm)) {
|
|
20976
21104
|
const data = asRecord(item);
|
|
20977
21105
|
const id = numberValue(data.id);
|
|
20978
|
-
const sectionId =
|
|
20979
|
-
const module =
|
|
21106
|
+
const sectionId = stringValue2(data.sectionid);
|
|
21107
|
+
const module = stringValue2(data.module) || stringValue2(data.plugin).replace(/^mod_/u, "") || stringValue2(data.modname).toLowerCase();
|
|
20980
21108
|
const activity = {
|
|
20981
21109
|
id,
|
|
20982
21110
|
name: htmlText(data.name, baseUrl),
|
|
20983
21111
|
modname: module.toLowerCase(),
|
|
20984
|
-
url:
|
|
21112
|
+
url: stringValue2(data.url) ? resolveUrl(baseUrl, stringValue2(data.url)) : "",
|
|
20985
21113
|
visible: booleanValue(data.visible, true) && booleanValue(data.uservisible, true) && !booleanValue(data.stealth),
|
|
20986
21114
|
description: htmlText(data.content ?? data.description, baseUrl)
|
|
20987
21115
|
};
|
|
@@ -20994,7 +21122,7 @@ function parseCourseFormatState(value, baseUrl) {
|
|
|
20994
21122
|
const data = asRecord(item);
|
|
20995
21123
|
const id = numberValue(data.id);
|
|
20996
21124
|
const hasActivityList = Array.isArray(data.cmlist);
|
|
20997
|
-
const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(
|
|
21125
|
+
const listedActivities = asArray(data.cmlist).map((activityId) => activities.get(stringValue2(activityId))).filter((activity) => activity !== void 0);
|
|
20998
21126
|
return {
|
|
20999
21127
|
id,
|
|
21000
21128
|
name: htmlText(data.title || data.rawtitle, baseUrl),
|
|
@@ -21012,18 +21140,18 @@ function parseTodoItem(value) {
|
|
|
21012
21140
|
const progress = course.progress;
|
|
21013
21141
|
return {
|
|
21014
21142
|
id: numberValue(data.id),
|
|
21015
|
-
name:
|
|
21016
|
-
activity_name:
|
|
21017
|
-
modname:
|
|
21143
|
+
name: stringValue2(data.name),
|
|
21144
|
+
activity_name: stringValue2(data.activityname),
|
|
21145
|
+
modname: stringValue2(data.modulename),
|
|
21018
21146
|
course_id: numberValue(course.id),
|
|
21019
|
-
course_name:
|
|
21147
|
+
course_name: stringValue2(course.fullname),
|
|
21020
21148
|
due_at: numberValue(data.timesort) || numberValue(data.timestart),
|
|
21021
21149
|
overdue: booleanValue(data.overdue),
|
|
21022
21150
|
actionable: booleanValue(action.actionable),
|
|
21023
|
-
action_name:
|
|
21024
|
-
action_url:
|
|
21025
|
-
url:
|
|
21026
|
-
event_type:
|
|
21151
|
+
action_name: stringValue2(action.name),
|
|
21152
|
+
action_url: stringValue2(action.url),
|
|
21153
|
+
url: stringValue2(data.url),
|
|
21154
|
+
event_type: stringValue2(data.eventtype),
|
|
21027
21155
|
course_progress: typeof progress === "number" ? progress : void 0
|
|
21028
21156
|
};
|
|
21029
21157
|
}
|
|
@@ -21034,15 +21162,15 @@ function parseAlertNotification(value) {
|
|
|
21034
21162
|
const data = asRecord(value);
|
|
21035
21163
|
return {
|
|
21036
21164
|
id: numberValue(data.id),
|
|
21037
|
-
subject:
|
|
21038
|
-
short_subject:
|
|
21039
|
-
event_type:
|
|
21040
|
-
component:
|
|
21165
|
+
subject: stringValue2(data.subject),
|
|
21166
|
+
short_subject: stringValue2(data.shortenedsubject),
|
|
21167
|
+
event_type: stringValue2(data.eventtype),
|
|
21168
|
+
component: stringValue2(data.component),
|
|
21041
21169
|
created_at: numberValue(data.timecreated),
|
|
21042
|
-
created_pretty:
|
|
21170
|
+
created_pretty: stringValue2(data.timecreatedpretty),
|
|
21043
21171
|
read: booleanValue(data.read),
|
|
21044
|
-
context_url:
|
|
21045
|
-
context_name:
|
|
21172
|
+
context_url: stringValue2(data.contexturl),
|
|
21173
|
+
context_name: stringValue2(data.contexturlname)
|
|
21046
21174
|
};
|
|
21047
21175
|
}
|
|
21048
21176
|
function parseAlertSummary(notificationsData, countsData, unreadCountsData) {
|
|
@@ -21071,20 +21199,20 @@ function parseForumPostAuthor(value) {
|
|
|
21071
21199
|
const urls = asRecord(data.urls);
|
|
21072
21200
|
return {
|
|
21073
21201
|
id: numberValue(data.id),
|
|
21074
|
-
fullname:
|
|
21075
|
-
profile_url:
|
|
21076
|
-
profile_image_url:
|
|
21202
|
+
fullname: stringValue2(data.fullname),
|
|
21203
|
+
profile_url: stringValue2(urls.profile),
|
|
21204
|
+
profile_image_url: stringValue2(urls.profileimage)
|
|
21077
21205
|
};
|
|
21078
21206
|
}
|
|
21079
21207
|
function parseForumPost(value, baseUrl = "") {
|
|
21080
21208
|
const data = asRecord(value);
|
|
21081
21209
|
const urls = asRecord(data.urls);
|
|
21082
|
-
const messageHtml =
|
|
21083
|
-
const structured = htmlToStructuredContent(messageHtml,
|
|
21210
|
+
const messageHtml = stringValue2(data.message);
|
|
21211
|
+
const structured = htmlToStructuredContent(messageHtml, stringValue2(urls.view || urls.discuss) || baseUrl);
|
|
21084
21212
|
return {
|
|
21085
21213
|
id: numberValue(data.id),
|
|
21086
21214
|
discussion_id: numberValue(data.discussionid),
|
|
21087
|
-
subject:
|
|
21215
|
+
subject: stringValue2(data.subject),
|
|
21088
21216
|
message_html: messageHtml,
|
|
21089
21217
|
message_text: structured.text,
|
|
21090
21218
|
image_urls: structured.image_urls,
|
|
@@ -21098,8 +21226,8 @@ function parseForumPost(value, baseUrl = "") {
|
|
|
21098
21226
|
unread: booleanValue(data.unread),
|
|
21099
21227
|
is_deleted: booleanValue(data.isdeleted),
|
|
21100
21228
|
is_private_reply: booleanValue(data.isprivatereply),
|
|
21101
|
-
url:
|
|
21102
|
-
reply_url:
|
|
21229
|
+
url: stringValue2(urls.view || urls.viewisolated),
|
|
21230
|
+
reply_url: stringValue2(urls.reply)
|
|
21103
21231
|
};
|
|
21104
21232
|
}
|
|
21105
21233
|
function parseForumDiscussion(value, discussionId, baseUrl = "") {
|
|
@@ -21111,7 +21239,7 @@ function parseForumDiscussion(value, discussionId, baseUrl = "") {
|
|
|
21111
21239
|
course_id: numberValue(data.courseid),
|
|
21112
21240
|
forum_id: numberValue(data.forumid),
|
|
21113
21241
|
group_id: numberValue(data.groupid),
|
|
21114
|
-
group_name:
|
|
21242
|
+
group_name: stringValue2(data.groupname),
|
|
21115
21243
|
url: posts[0]?.url ? posts[0].url.split("#", 1)[0] : "",
|
|
21116
21244
|
posts
|
|
21117
21245
|
};
|
|
@@ -21122,7 +21250,7 @@ function asRecord(value) {
|
|
|
21122
21250
|
function asArray(value) {
|
|
21123
21251
|
return Array.isArray(value) ? value : [];
|
|
21124
21252
|
}
|
|
21125
|
-
function
|
|
21253
|
+
function stringValue2(value) {
|
|
21126
21254
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
21127
21255
|
}
|
|
21128
21256
|
function numberValue(value) {
|
|
@@ -21149,15 +21277,41 @@ function parseJsonValue(value) {
|
|
|
21149
21277
|
}
|
|
21150
21278
|
}
|
|
21151
21279
|
function htmlText(value, baseUrl) {
|
|
21152
|
-
return htmlToStructuredContent(
|
|
21280
|
+
return htmlToStructuredContent(stringValue2(value), baseUrl).text;
|
|
21153
21281
|
}
|
|
21154
21282
|
|
|
21155
21283
|
// src/scraper.ts
|
|
21156
21284
|
var import_node_html_parser2 = __toESM(require_dist(), 1);
|
|
21285
|
+
function parseMoodleErrorHtml(html) {
|
|
21286
|
+
const root = (0, import_node_html_parser2.parse)(html);
|
|
21287
|
+
const messageNode = first(root, [
|
|
21288
|
+
".errormessage",
|
|
21289
|
+
".alert-danger .alert-message",
|
|
21290
|
+
"[data-region='error-message']",
|
|
21291
|
+
".alert-danger[role='alert']",
|
|
21292
|
+
".alert-danger"
|
|
21293
|
+
]);
|
|
21294
|
+
if (!messageNode) {
|
|
21295
|
+
return null;
|
|
21296
|
+
}
|
|
21297
|
+
const messageRoot = (0, import_node_html_parser2.parse)(messageNode.toString());
|
|
21298
|
+
for (const unwanted of messageRoot.querySelectorAll(
|
|
21299
|
+
"button, .close, .errorcode, .stacktrace, .debuginfo, .backtrace, a.alert-link, a[href*='/error/']"
|
|
21300
|
+
)) {
|
|
21301
|
+
unwanted.remove();
|
|
21302
|
+
}
|
|
21303
|
+
const message = cleanText(messageRoot.textContent);
|
|
21304
|
+
if (!message) {
|
|
21305
|
+
return null;
|
|
21306
|
+
}
|
|
21307
|
+
const errorCodeText = cleanNodeText(root.querySelector(".errorcode"));
|
|
21308
|
+
const errorCode = errorCodeText.match(/^error\s+code\s*:\s*([a-z][a-z0-9_]*)\s*$/iu)?.[1] ?? moodleDocsErrorCode(root);
|
|
21309
|
+
return { message, ...errorCode ? { code: errorCode } : {} };
|
|
21310
|
+
}
|
|
21157
21311
|
function parsePageContext(html, baseUrl) {
|
|
21158
21312
|
const root = (0, import_node_html_parser2.parse)(html);
|
|
21159
21313
|
const config2 = parseMoodleConfig(html);
|
|
21160
|
-
const sesskey =
|
|
21314
|
+
const sesskey = stringValue3(config2.sesskey).trim();
|
|
21161
21315
|
const userid = numberValue2(config2.userId) || numberValue2(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
|
|
21162
21316
|
if (!sesskey || !userid) {
|
|
21163
21317
|
throw new Error("Session appears invalid: could not load authenticated Moodle context");
|
|
@@ -21170,7 +21324,7 @@ function parsePageContext(html, baseUrl) {
|
|
|
21170
21324
|
fullname: cleanNodeText(root.querySelector(".userfullname")),
|
|
21171
21325
|
sitename: extractSitename(root),
|
|
21172
21326
|
siteurl: baseUrl,
|
|
21173
|
-
lang:
|
|
21327
|
+
lang: stringValue3(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
|
|
21174
21328
|
}
|
|
21175
21329
|
};
|
|
21176
21330
|
}
|
|
@@ -21240,9 +21394,9 @@ function parseCourseSectionNumbers(html, courseId) {
|
|
|
21240
21394
|
for (const href of hrefs) {
|
|
21241
21395
|
const url2 = parseMaybeUrl(href.replace(/&/gu, "&"), "https://moodle.invalid");
|
|
21242
21396
|
const id = url2?.searchParams.get("id");
|
|
21243
|
-
const
|
|
21244
|
-
if (id === String(courseId) &&
|
|
21245
|
-
const section = Number(
|
|
21397
|
+
const sectionValue2 = url2?.searchParams.get("section");
|
|
21398
|
+
if (id === String(courseId) && sectionValue2 && /^\d+$/.test(sectionValue2)) {
|
|
21399
|
+
const section = Number(sectionValue2);
|
|
21246
21400
|
if (!sections.includes(section)) {
|
|
21247
21401
|
sections.push(section);
|
|
21248
21402
|
}
|
|
@@ -21585,6 +21739,16 @@ function selectedGroupName(root, groupId) {
|
|
|
21585
21739
|
}
|
|
21586
21740
|
return "";
|
|
21587
21741
|
}
|
|
21742
|
+
function moodleDocsErrorCode(root) {
|
|
21743
|
+
for (const link of root.querySelectorAll("a[href*='/error/']")) {
|
|
21744
|
+
const href = link.getAttribute("href") ?? "";
|
|
21745
|
+
const code = href.match(/\/error\/[^/]+\/([a-z][a-z0-9_]*)/iu)?.[1];
|
|
21746
|
+
if (code) {
|
|
21747
|
+
return code;
|
|
21748
|
+
}
|
|
21749
|
+
}
|
|
21750
|
+
return void 0;
|
|
21751
|
+
}
|
|
21588
21752
|
function cleanNodeText(node) {
|
|
21589
21753
|
return cleanText(node?.textContent ?? "");
|
|
21590
21754
|
}
|
|
@@ -21707,7 +21871,7 @@ function numberValue2(value) {
|
|
|
21707
21871
|
}
|
|
21708
21872
|
return 0;
|
|
21709
21873
|
}
|
|
21710
|
-
function
|
|
21874
|
+
function stringValue3(value) {
|
|
21711
21875
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
21712
21876
|
}
|
|
21713
21877
|
function fileEntry(name, url2, baseUrl) {
|
|
@@ -21834,13 +21998,13 @@ var ForumModule = class {
|
|
|
21834
21998
|
}
|
|
21835
21999
|
};
|
|
21836
22000
|
function shouldFallbackForumAjax(error51) {
|
|
21837
|
-
if (!
|
|
22001
|
+
if (!isRecord4(error51)) {
|
|
21838
22002
|
return false;
|
|
21839
22003
|
}
|
|
21840
22004
|
const code = typeof error51.moodleErrorCode === "string" ? error51.moodleErrorCode : "";
|
|
21841
22005
|
return code === "servicenotavailable" || code === "accessexception" || error51 instanceof Error && error51.message.includes("Web service is not available");
|
|
21842
22006
|
}
|
|
21843
|
-
function
|
|
22007
|
+
function isRecord4(value) {
|
|
21844
22008
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21845
22009
|
}
|
|
21846
22010
|
|
|
@@ -22074,7 +22238,7 @@ var MoodleClientCoreApiError = class extends MoodleClientCoreError {
|
|
|
22074
22238
|
moodleErrorCode;
|
|
22075
22239
|
constructor(message, moodleErrorCode) {
|
|
22076
22240
|
const auth = isLoginErrorCode(moodleErrorCode);
|
|
22077
|
-
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") ||
|
|
22241
|
+
const notFound = ["invalidrecord", "invalidcoursemodule"].includes(moodleErrorCode ?? "") || /\bHTTP 404\b/.test(message);
|
|
22078
22242
|
super(
|
|
22079
22243
|
auth ? "auth" : notFound ? "not_found" : "upstream",
|
|
22080
22244
|
message,
|
|
@@ -22141,7 +22305,7 @@ var MoodleClientCore = class {
|
|
|
22141
22305
|
await this.ensureSession();
|
|
22142
22306
|
try {
|
|
22143
22307
|
const data = await this.call(FUNC_GET_SITE_INFO);
|
|
22144
|
-
if (
|
|
22308
|
+
if (isRecord5(data) && "userid" in data) {
|
|
22145
22309
|
const info = parseUserInfo(data);
|
|
22146
22310
|
this.sesskey = typeof data.sesskey === "string" ? data.sesskey : this.sesskey;
|
|
22147
22311
|
this.userid = info.userid;
|
|
@@ -22226,9 +22390,9 @@ var MoodleClientCore = class {
|
|
|
22226
22390
|
let type = "";
|
|
22227
22391
|
try {
|
|
22228
22392
|
const data = await this.call(FUNC_GET_COURSE_MODULE, { cmid: id });
|
|
22229
|
-
const module =
|
|
22230
|
-
type =
|
|
22231
|
-
courseId =
|
|
22393
|
+
const module = isRecord5(data) && isRecord5(data.cm) ? data.cm : data;
|
|
22394
|
+
type = isRecord5(module) && typeof module.modname === "string" ? module.modname : "";
|
|
22395
|
+
courseId = isRecord5(module) && typeof module.course === "number" ? module.course : void 0;
|
|
22232
22396
|
} catch (error51) {
|
|
22233
22397
|
if (!this.errors.isApi(error51) || error51.moodleErrorCode !== "servicenotavailable") {
|
|
22234
22398
|
throw error51;
|
|
@@ -22261,7 +22425,7 @@ var MoodleClientCore = class {
|
|
|
22261
22425
|
aftereventid: 0,
|
|
22262
22426
|
limittononsuspendedevents: true
|
|
22263
22427
|
});
|
|
22264
|
-
const events =
|
|
22428
|
+
const events = isRecord5(data) && Array.isArray(data.events) ? data.events : [];
|
|
22265
22429
|
return parseTodoItems(events);
|
|
22266
22430
|
}
|
|
22267
22431
|
async getAlerts(limit = 20) {
|
|
@@ -22505,7 +22669,15 @@ var MoodleClientCore = class {
|
|
|
22505
22669
|
throw this.errors.api("Session expired", "servicerequireslogin");
|
|
22506
22670
|
}
|
|
22507
22671
|
if (!response.ok) {
|
|
22508
|
-
|
|
22672
|
+
const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
|
|
22673
|
+
const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
22674
|
+
if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
|
|
22675
|
+
const moodleError = await response.text().then(parseMoodleErrorHtml).catch(() => null);
|
|
22676
|
+
if (moodleError) {
|
|
22677
|
+
throw this.errors.api(`${moodleError.message} (${context})`, moodleError.code);
|
|
22678
|
+
}
|
|
22679
|
+
}
|
|
22680
|
+
throw this.errors.api(context);
|
|
22509
22681
|
}
|
|
22510
22682
|
return response;
|
|
22511
22683
|
}
|
|
@@ -22514,7 +22686,7 @@ var MoodleClientCore = class {
|
|
|
22514
22686
|
let offset = 0;
|
|
22515
22687
|
while (true) {
|
|
22516
22688
|
const data = await this.call(FUNC_GET_COURSES_BY_TIMELINE, { classification: "all", limit: 100, offset });
|
|
22517
|
-
if (!
|
|
22689
|
+
if (!isRecord5(data) || !Array.isArray(data.courses) || !data.courses.length) {
|
|
22518
22690
|
break;
|
|
22519
22691
|
}
|
|
22520
22692
|
courses.push(...data.courses);
|
|
@@ -22679,7 +22851,7 @@ function queryMatches(text, query) {
|
|
|
22679
22851
|
return needle ? haystack.includes(needle) || needle.split(" ").every((token) => haystack.includes(token)) : true;
|
|
22680
22852
|
}
|
|
22681
22853
|
function parseTodoPayload(value) {
|
|
22682
|
-
return parseTodoItems(
|
|
22854
|
+
return parseTodoItems(isRecord5(value) && Array.isArray(value.events) ? value.events : []);
|
|
22683
22855
|
}
|
|
22684
22856
|
function errorMessage(value) {
|
|
22685
22857
|
return value instanceof Error ? value.message : "Unknown Moodle error";
|
|
@@ -22691,7 +22863,7 @@ function chunks(values, size) {
|
|
|
22691
22863
|
}
|
|
22692
22864
|
return result;
|
|
22693
22865
|
}
|
|
22694
|
-
function
|
|
22866
|
+
function isRecord5(value) {
|
|
22695
22867
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
22696
22868
|
}
|
|
22697
22869
|
function isLoginErrorCode(code) {
|
|
@@ -22714,6 +22886,8 @@ function safeUrl(value) {
|
|
|
22714
22886
|
}
|
|
22715
22887
|
|
|
22716
22888
|
// src/mcp/gateway.ts
|
|
22889
|
+
var import_node_html_parser3 = __toESM(require_dist(), 1);
|
|
22890
|
+
var MAX_MCP_FILE_BYTES = 16 * 1024 * 1024;
|
|
22717
22891
|
var MoodleGatewayError = class extends Error {
|
|
22718
22892
|
code;
|
|
22719
22893
|
constructor(code, message) {
|
|
@@ -22749,95 +22923,211 @@ function createMoodleGateway(client) {
|
|
|
22749
22923
|
return limit === void 0 ? forums : forums.slice(0, limit);
|
|
22750
22924
|
},
|
|
22751
22925
|
searchForums: ({ forumId, ...input }) => client.searchForumContent({ ...input, forumCmid: forumId }),
|
|
22752
|
-
getThread: ({ discussionId }) => client.getForumDiscussion(discussionId)
|
|
22926
|
+
getThread: ({ discussionId }) => client.getForumDiscussion(discussionId),
|
|
22927
|
+
async getFile({ source }) {
|
|
22928
|
+
let target = await resolveFileTarget(client, source);
|
|
22929
|
+
let response = await client.requestAbsolute(target.url);
|
|
22930
|
+
if (isHtml(response)) {
|
|
22931
|
+
const html = await response.text();
|
|
22932
|
+
if (looksLikeLoginPage(html)) throw fileAuthenticationRequired();
|
|
22933
|
+
const links = resourceLinks(html, target.url);
|
|
22934
|
+
if (links.length !== 1) {
|
|
22935
|
+
const code = links.length ? "MOODLE_FILE_SOURCE_AMBIGUOUS" : "MOODLE_FILE_NOT_FOUND";
|
|
22936
|
+
throw new MoodleGatewayError(code, "The Moodle resource did not resolve to exactly one file.");
|
|
22937
|
+
}
|
|
22938
|
+
const resolved = await resolveFileTarget(client, links[0].url);
|
|
22939
|
+
target = { ...resolved, name: target.name || links[0].name || resolved.name };
|
|
22940
|
+
response = await client.requestAbsolute(target.url);
|
|
22941
|
+
if (isHtml(response)) {
|
|
22942
|
+
if (looksLikeLoginPage(await response.text())) throw fileAuthenticationRequired();
|
|
22943
|
+
throw new MoodleGatewayError("MOODLE_FILE_NOT_FOUND", "Moodle did not return a downloadable file.");
|
|
22944
|
+
}
|
|
22945
|
+
}
|
|
22946
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
22947
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_MCP_FILE_BYTES) {
|
|
22948
|
+
throw fileTooLarge();
|
|
22949
|
+
}
|
|
22950
|
+
const content = await readBoundedBody(response, MAX_MCP_FILE_BYTES);
|
|
22951
|
+
const name = safeFilename(contentDispositionFilename(response.headers.get("content-disposition"))) ?? safeFilename(target.name) ?? safeFilename(urlFilename(response.url || target.url));
|
|
22952
|
+
if (!name) {
|
|
22953
|
+
throw new MoodleGatewayError("MOODLE_FILE_NAME_MISSING", "Moodle did not provide a safe filename.");
|
|
22954
|
+
}
|
|
22955
|
+
return {
|
|
22956
|
+
name,
|
|
22957
|
+
mimeType: contentType(response),
|
|
22958
|
+
bytes: content.byteLength,
|
|
22959
|
+
uri: publicFileUrl(response.url || target.url),
|
|
22960
|
+
blob: encodeBase64(content)
|
|
22961
|
+
};
|
|
22962
|
+
}
|
|
22753
22963
|
};
|
|
22754
22964
|
}
|
|
22755
|
-
|
|
22756
|
-
|
|
22757
|
-
|
|
22758
|
-
|
|
22759
|
-
|
|
22760
|
-
|
|
22761
|
-
jsonrpc: external_exports.literal("2.0"),
|
|
22762
|
-
id: external_exports.union([external_exports.string(), external_exports.number(), external_exports.null()]).optional(),
|
|
22763
|
-
method: external_exports.string().trim().min(1),
|
|
22764
|
-
params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
|
|
22765
|
-
}).strict();
|
|
22766
|
-
var ModernClientMetadataSchema = external_exports.object({
|
|
22767
|
-
"io.modelcontextprotocol/clientCapabilities": external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
22768
|
-
"io.modelcontextprotocol/clientInfo": external_exports.object({
|
|
22769
|
-
name: external_exports.string().trim().min(1),
|
|
22770
|
-
version: external_exports.string().trim().min(1)
|
|
22771
|
-
}).passthrough(),
|
|
22772
|
-
"io.modelcontextprotocol/protocolVersion": external_exports.literal(MODERN_PROTOCOL_VERSION)
|
|
22773
|
-
}).passthrough();
|
|
22774
|
-
var UnsupportedProtocolVersionError = class extends Error {
|
|
22775
|
-
protocolVersion;
|
|
22776
|
-
supportedVersions = SUPPORTED_PROTOCOL_VERSIONS;
|
|
22777
|
-
constructor(protocolVersion) {
|
|
22778
|
-
super(`Unsupported MCP protocol version: ${protocolVersion}`);
|
|
22779
|
-
this.name = "UnsupportedProtocolVersionError";
|
|
22780
|
-
this.protocolVersion = protocolVersion;
|
|
22965
|
+
async function resolveFileTarget(client, rawSource) {
|
|
22966
|
+
const source = String(rawSource).trim();
|
|
22967
|
+
if (/^\d+$/u.test(source)) {
|
|
22968
|
+
const activityId = Number(source);
|
|
22969
|
+
if (!Number.isSafeInteger(activityId) || activityId < 1) throw invalidFileSource();
|
|
22970
|
+
return validateFileTarget(client, await fileFromActivity(client, activityId));
|
|
22781
22971
|
}
|
|
22782
|
-
|
|
22783
|
-
|
|
22784
|
-
|
|
22785
|
-
|
|
22786
|
-
|
|
22787
|
-
constructor(field, expected, actual) {
|
|
22788
|
-
super(`MCP request metadata mismatch for ${field}.`);
|
|
22789
|
-
this.name = "RequestMetadataMismatchError";
|
|
22790
|
-
this.field = field;
|
|
22791
|
-
this.expected = expected;
|
|
22792
|
-
this.actual = actual;
|
|
22972
|
+
let url2;
|
|
22973
|
+
try {
|
|
22974
|
+
url2 = new URL(source);
|
|
22975
|
+
} catch {
|
|
22976
|
+
throw invalidFileSource();
|
|
22793
22977
|
}
|
|
22794
|
-
|
|
22795
|
-
|
|
22796
|
-
|
|
22797
|
-
}
|
|
22798
|
-
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
const requested = context.protocolVersion ?? stringValue3(meta3?.["io.modelcontextprotocol/protocolVersion"]) ?? initializeVersion ?? MODERN_PROTOCOL_VERSION;
|
|
22802
|
-
if (!isSupportedProtocolVersion(requested)) {
|
|
22803
|
-
throw new UnsupportedProtocolVersionError(requested);
|
|
22978
|
+
const site = new URL(client.baseUrl);
|
|
22979
|
+
const sitePath = site.pathname.replace(/\/$/u, "");
|
|
22980
|
+
if (url2.origin !== site.origin) throw invalidFileSource();
|
|
22981
|
+
if (url2.pathname === `${sitePath}/mod/resource/view.php`) {
|
|
22982
|
+
const activityId = Number(url2.searchParams.get("id"));
|
|
22983
|
+
if (!Number.isSafeInteger(activityId) || activityId < 1) throw invalidFileSource();
|
|
22984
|
+
return validateFileTarget(client, await fileFromActivity(client, activityId));
|
|
22804
22985
|
}
|
|
22805
|
-
|
|
22986
|
+
if (!url2.pathname.startsWith(`${sitePath}/pluginfile.php/`)) throw invalidFileSource();
|
|
22987
|
+
return { url: url2.toString(), name: urlFilename(url2.toString()) };
|
|
22806
22988
|
}
|
|
22807
|
-
function
|
|
22808
|
-
|
|
22809
|
-
|
|
22810
|
-
|
|
22811
|
-
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22816
|
-
|
|
22817
|
-
|
|
22818
|
-
|
|
22819
|
-
|
|
22820
|
-
|
|
22989
|
+
function validateFileTarget(client, target) {
|
|
22990
|
+
let url2;
|
|
22991
|
+
try {
|
|
22992
|
+
url2 = new URL(target.url);
|
|
22993
|
+
} catch {
|
|
22994
|
+
throw invalidFileSource();
|
|
22995
|
+
}
|
|
22996
|
+
const site = new URL(client.baseUrl);
|
|
22997
|
+
const sitePath = site.pathname.replace(/\/$/u, "");
|
|
22998
|
+
const supportedPath = url2.pathname === `${sitePath}/mod/resource/view.php` || url2.pathname.startsWith(`${sitePath}/pluginfile.php/`);
|
|
22999
|
+
if (url2.origin !== site.origin || !supportedPath) throw invalidFileSource();
|
|
23000
|
+
return target;
|
|
23001
|
+
}
|
|
23002
|
+
async function fileFromActivity(client, activityId) {
|
|
23003
|
+
const activity = await client.getActivity(activityId);
|
|
23004
|
+
if (activity.type !== "resource" || !("file_entries" in activity) || !Array.isArray(activity.file_entries)) {
|
|
23005
|
+
throw new MoodleGatewayError(
|
|
23006
|
+
"MOODLE_FILE_SOURCE_INVALID",
|
|
23007
|
+
`Activity ${activityId} is not a downloadable resource.`
|
|
23008
|
+
);
|
|
22821
23009
|
}
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
23010
|
+
if (activity.file_entries.length !== 1) {
|
|
23011
|
+
if (activity.file_entries.length === 0) {
|
|
23012
|
+
const resource = activity;
|
|
23013
|
+
const url2 = resource.target_url || resource.url;
|
|
23014
|
+
if (url2) return { url: url2, name: resource.target_name || void 0 };
|
|
23015
|
+
}
|
|
23016
|
+
throw new MoodleGatewayError(
|
|
23017
|
+
"MOODLE_FILE_SOURCE_AMBIGUOUS",
|
|
23018
|
+
`Activity ${activityId} did not resolve to exactly one file. Inspect file_entries and request one URL.`
|
|
23019
|
+
);
|
|
22825
23020
|
}
|
|
23021
|
+
const [entry] = activity.file_entries;
|
|
23022
|
+
return { url: entry.url, name: entry.name };
|
|
23023
|
+
}
|
|
23024
|
+
function resourceLinks(html, baseUrl) {
|
|
23025
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
23026
|
+
const entries = root.querySelectorAll(".resourceworkaround a[href], .resourcecontent a[href], a.resourceworkaround[href]").map((link) => ({
|
|
23027
|
+
name: link.textContent.trim(),
|
|
23028
|
+
url: new URL(link.getAttribute("href") ?? "", baseUrl).toString()
|
|
23029
|
+
})).filter((entry) => entry.url !== baseUrl);
|
|
23030
|
+
return entries.filter((entry, index) => entries.findIndex((candidate) => candidate.url === entry.url) === index);
|
|
23031
|
+
}
|
|
23032
|
+
async function readBoundedBody(response, maxBytes) {
|
|
23033
|
+
if (!response.body) return new Uint8Array();
|
|
23034
|
+
const reader = response.body.getReader();
|
|
23035
|
+
const chunks2 = [];
|
|
23036
|
+
let bytes = 0;
|
|
23037
|
+
while (true) {
|
|
23038
|
+
const { done, value } = await reader.read();
|
|
23039
|
+
if (done) break;
|
|
23040
|
+
bytes += value.byteLength;
|
|
23041
|
+
if (bytes > maxBytes) {
|
|
23042
|
+
await reader.cancel().catch(() => void 0);
|
|
23043
|
+
throw fileTooLarge();
|
|
23044
|
+
}
|
|
23045
|
+
chunks2.push(value);
|
|
23046
|
+
}
|
|
23047
|
+
const content = new Uint8Array(bytes);
|
|
23048
|
+
let offset = 0;
|
|
23049
|
+
for (const chunk of chunks2) {
|
|
23050
|
+
content.set(chunk, offset);
|
|
23051
|
+
offset += chunk.byteLength;
|
|
23052
|
+
}
|
|
23053
|
+
return content;
|
|
23054
|
+
}
|
|
23055
|
+
function invalidFileSource() {
|
|
23056
|
+
return new MoodleGatewayError(
|
|
23057
|
+
"MOODLE_FILE_SOURCE_INVALID",
|
|
23058
|
+
"Use a positive resource activity ID, a same-site resource URL, or a same-site pluginfile URL."
|
|
23059
|
+
);
|
|
22826
23060
|
}
|
|
22827
|
-
function
|
|
22828
|
-
return
|
|
23061
|
+
function fileTooLarge() {
|
|
23062
|
+
return new MoodleGatewayError(
|
|
23063
|
+
"MOODLE_FILE_TOO_LARGE",
|
|
23064
|
+
`Moodle files returned through MCP cannot exceed ${MAX_MCP_FILE_BYTES / 1024 / 1024} MiB.`
|
|
23065
|
+
);
|
|
22829
23066
|
}
|
|
22830
|
-
function
|
|
22831
|
-
return
|
|
23067
|
+
function fileAuthenticationRequired() {
|
|
23068
|
+
return new MoodleGatewayError(
|
|
23069
|
+
"MOODLE_AUTH_REQUIRED",
|
|
23070
|
+
"Moodle returned a login page instead of the requested file."
|
|
23071
|
+
);
|
|
22832
23072
|
}
|
|
22833
|
-
function
|
|
22834
|
-
|
|
23073
|
+
function isHtml(response) {
|
|
23074
|
+
if (/\battachment\b/iu.test(response.headers.get("content-disposition") ?? "")) return false;
|
|
23075
|
+
const type = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
23076
|
+
return type.includes("text/html") || type.includes("application/xhtml+xml");
|
|
22835
23077
|
}
|
|
22836
|
-
function
|
|
22837
|
-
|
|
23078
|
+
function looksLikeLoginPage(html) {
|
|
23079
|
+
const root = (0, import_node_html_parser3.parse)(html);
|
|
23080
|
+
return root.querySelector('form[action*="/login/"], input[name="password"], #page-login-index') !== null || /<title>\s*(?:log in|login)/iu.test(html);
|
|
22838
23081
|
}
|
|
22839
|
-
function
|
|
22840
|
-
return
|
|
23082
|
+
function contentType(response) {
|
|
23083
|
+
return response.headers.get("content-type")?.split(";", 1)[0]?.trim() || "application/octet-stream";
|
|
23084
|
+
}
|
|
23085
|
+
function contentDispositionFilename(value) {
|
|
23086
|
+
if (!value) return void 0;
|
|
23087
|
+
const extended = value.match(/filename\*\s*=\s*([^;]+)/iu)?.[1]?.trim().replace(/^"|"$/gu, "");
|
|
23088
|
+
if (extended) {
|
|
23089
|
+
const encoded = extended.replace(/^[^']*'[^']*'/u, "");
|
|
23090
|
+
try {
|
|
23091
|
+
return decodeURIComponent(encoded);
|
|
23092
|
+
} catch {
|
|
23093
|
+
return encoded;
|
|
23094
|
+
}
|
|
23095
|
+
}
|
|
23096
|
+
const quoted = value.match(/filename\s*=\s*"((?:\\.|[^"])*)"/iu)?.[1];
|
|
23097
|
+
if (quoted !== void 0) return quoted.replace(/\\([\\"])/gu, "$1");
|
|
23098
|
+
return value.match(/filename\s*=\s*([^;]+)/iu)?.[1]?.trim();
|
|
23099
|
+
}
|
|
23100
|
+
function urlFilename(value) {
|
|
23101
|
+
try {
|
|
23102
|
+
const encoded = new URL(value).pathname.split("/").at(-1) ?? "";
|
|
23103
|
+
try {
|
|
23104
|
+
return decodeURIComponent(encoded);
|
|
23105
|
+
} catch {
|
|
23106
|
+
return encoded;
|
|
23107
|
+
}
|
|
23108
|
+
} catch {
|
|
23109
|
+
return void 0;
|
|
23110
|
+
}
|
|
23111
|
+
}
|
|
23112
|
+
function safeFilename(value) {
|
|
23113
|
+
const name = value?.split(/[\\/]/u).at(-1)?.replace(/[\u0000-\u001f\u007f]/gu, "").trim();
|
|
23114
|
+
return name && name !== "." && name !== ".." ? name : void 0;
|
|
23115
|
+
}
|
|
23116
|
+
function publicFileUrl(value) {
|
|
23117
|
+
const url2 = new URL(value);
|
|
23118
|
+
url2.username = "";
|
|
23119
|
+
url2.password = "";
|
|
23120
|
+
for (const key of [...url2.searchParams.keys()]) {
|
|
23121
|
+
if (key !== "forcedownload" && key !== "download") url2.searchParams.delete(key);
|
|
23122
|
+
}
|
|
23123
|
+
return url2.toString();
|
|
23124
|
+
}
|
|
23125
|
+
function encodeBase64(content) {
|
|
23126
|
+
let binary = "";
|
|
23127
|
+
for (let offset = 0; offset < content.byteLength; offset += 32768) {
|
|
23128
|
+
binary += String.fromCharCode(...content.subarray(offset, offset + 32768));
|
|
23129
|
+
}
|
|
23130
|
+
return btoa(binary);
|
|
22841
23131
|
}
|
|
22842
23132
|
|
|
22843
23133
|
// src/mcp/server.ts
|
|
@@ -22850,14 +23140,122 @@ var READ_ONLY_ANNOTATIONS = {
|
|
|
22850
23140
|
};
|
|
22851
23141
|
var emptyInput = external_exports.object({}).strict();
|
|
22852
23142
|
var positiveId = external_exports.number().int().positive();
|
|
22853
|
-
var
|
|
22854
|
-
var
|
|
23143
|
+
var integer2 = external_exports.number().int();
|
|
23144
|
+
var userValue = external_exports.looseObject({
|
|
23145
|
+
userid: integer2,
|
|
23146
|
+
username: external_exports.string(),
|
|
23147
|
+
fullname: external_exports.string(),
|
|
23148
|
+
sitename: external_exports.string(),
|
|
23149
|
+
siteurl: external_exports.string(),
|
|
23150
|
+
lang: external_exports.string().optional()
|
|
23151
|
+
});
|
|
23152
|
+
var courseValue = external_exports.looseObject({
|
|
23153
|
+
id: integer2,
|
|
23154
|
+
shortname: external_exports.string(),
|
|
23155
|
+
fullname: external_exports.string(),
|
|
23156
|
+
category: external_exports.number().int(),
|
|
23157
|
+
visible: external_exports.boolean(),
|
|
23158
|
+
startdate: external_exports.number(),
|
|
23159
|
+
enddate: external_exports.number().optional()
|
|
23160
|
+
});
|
|
23161
|
+
var activityValue = external_exports.looseObject({
|
|
23162
|
+
id: integer2,
|
|
23163
|
+
name: external_exports.string(),
|
|
23164
|
+
modname: external_exports.string(),
|
|
23165
|
+
url: external_exports.string(),
|
|
23166
|
+
visible: external_exports.boolean(),
|
|
23167
|
+
description: external_exports.string()
|
|
23168
|
+
});
|
|
23169
|
+
var fileEntryValue = external_exports.looseObject({
|
|
23170
|
+
name: external_exports.string(),
|
|
23171
|
+
url: external_exports.string(),
|
|
23172
|
+
requires_authentication: external_exports.boolean()
|
|
23173
|
+
});
|
|
23174
|
+
var activityDetailValue = external_exports.looseObject({
|
|
23175
|
+
id: positiveId,
|
|
23176
|
+
name: external_exports.string(),
|
|
23177
|
+
type: external_exports.string(),
|
|
23178
|
+
url: external_exports.string().optional(),
|
|
23179
|
+
target_name: external_exports.string().optional(),
|
|
23180
|
+
target_url: external_exports.string().optional(),
|
|
23181
|
+
file_entries: external_exports.array(fileEntryValue).optional()
|
|
23182
|
+
});
|
|
23183
|
+
var sectionValue = external_exports.looseObject({
|
|
23184
|
+
id: external_exports.number().int(),
|
|
23185
|
+
name: external_exports.string(),
|
|
23186
|
+
section: external_exports.number().int(),
|
|
23187
|
+
visible: external_exports.boolean(),
|
|
23188
|
+
summary: external_exports.string(),
|
|
23189
|
+
activities: external_exports.array(activityValue)
|
|
23190
|
+
});
|
|
23191
|
+
var todoValue = external_exports.looseObject({
|
|
23192
|
+
id: external_exports.number().int(),
|
|
23193
|
+
name: external_exports.string(),
|
|
23194
|
+
course_id: external_exports.number().int(),
|
|
23195
|
+
course_name: external_exports.string(),
|
|
23196
|
+
due_at: external_exports.number(),
|
|
23197
|
+
url: external_exports.string()
|
|
23198
|
+
});
|
|
23199
|
+
var gradeItemValue = external_exports.looseObject({
|
|
23200
|
+
name: external_exports.string(),
|
|
23201
|
+
item_type: external_exports.string(),
|
|
23202
|
+
grade: external_exports.string(),
|
|
23203
|
+
range: external_exports.string(),
|
|
23204
|
+
percentage: external_exports.string(),
|
|
23205
|
+
feedback: external_exports.string(),
|
|
23206
|
+
url: external_exports.string()
|
|
23207
|
+
});
|
|
23208
|
+
var gradesValue = external_exports.looseObject({
|
|
23209
|
+
course_id: integer2,
|
|
23210
|
+
course_name: external_exports.string(),
|
|
23211
|
+
learner_name: external_exports.string(),
|
|
23212
|
+
total_grade: external_exports.string(),
|
|
23213
|
+
total_range: external_exports.string(),
|
|
23214
|
+
total_percentage: external_exports.string(),
|
|
23215
|
+
items: external_exports.array(gradeItemValue)
|
|
23216
|
+
});
|
|
23217
|
+
var forumValue = external_exports.looseObject({
|
|
23218
|
+
id: integer2,
|
|
23219
|
+
name: external_exports.string(),
|
|
23220
|
+
course_id: integer2,
|
|
23221
|
+
course_name: external_exports.string(),
|
|
23222
|
+
url: external_exports.string()
|
|
23223
|
+
});
|
|
23224
|
+
var forumSearchValue = external_exports.looseObject({
|
|
23225
|
+
course_id: integer2,
|
|
23226
|
+
course_name: external_exports.string(),
|
|
23227
|
+
forum_id: integer2,
|
|
23228
|
+
forum_name: external_exports.string(),
|
|
23229
|
+
discussion_id: integer2,
|
|
23230
|
+
discussion_subject: external_exports.string(),
|
|
23231
|
+
post_id: integer2,
|
|
23232
|
+
snippet: external_exports.string(),
|
|
23233
|
+
url: external_exports.string()
|
|
23234
|
+
});
|
|
23235
|
+
var forumPostValue = external_exports.looseObject({
|
|
23236
|
+
id: integer2,
|
|
23237
|
+
discussion_id: integer2,
|
|
23238
|
+
subject: external_exports.string(),
|
|
23239
|
+
message_text: external_exports.string(),
|
|
23240
|
+
author: external_exports.looseObject({ id: integer2, fullname: external_exports.string() }),
|
|
23241
|
+
url: external_exports.string()
|
|
23242
|
+
});
|
|
23243
|
+
var threadValue = external_exports.looseObject({
|
|
23244
|
+
id: integer2,
|
|
23245
|
+
subject: external_exports.string(),
|
|
23246
|
+
course_id: integer2,
|
|
23247
|
+
forum_id: integer2,
|
|
23248
|
+
group_id: external_exports.number().int(),
|
|
23249
|
+
group_name: external_exports.string(),
|
|
23250
|
+
url: external_exports.string(),
|
|
23251
|
+
posts: external_exports.array(forumPostValue)
|
|
23252
|
+
});
|
|
22855
23253
|
var TOOL_REGISTRATIONS = [
|
|
22856
23254
|
{
|
|
22857
23255
|
name: "get_user",
|
|
22858
23256
|
description: "Get the authenticated Moodle user and site.",
|
|
22859
23257
|
input: emptyInput,
|
|
22860
|
-
output: external_exports.object({ user:
|
|
23258
|
+
output: external_exports.object({ user: userValue })
|
|
22861
23259
|
},
|
|
22862
23260
|
{
|
|
22863
23261
|
name: "get_overview",
|
|
@@ -22867,19 +23265,31 @@ var TOOL_REGISTRATIONS = [
|
|
|
22867
23265
|
todoDays: external_exports.number().int().min(1).max(365).optional(),
|
|
22868
23266
|
alertsLimit: external_exports.number().int().min(1).max(100).optional().default(5)
|
|
22869
23267
|
}).strict(),
|
|
22870
|
-
output: external_exports.object({
|
|
23268
|
+
output: external_exports.object({
|
|
23269
|
+
overview: external_exports.looseObject({
|
|
23270
|
+
user: userValue,
|
|
23271
|
+
courses: external_exports.array(courseValue),
|
|
23272
|
+
todo: external_exports.array(todoValue),
|
|
23273
|
+
errors: external_exports.array(external_exports.string())
|
|
23274
|
+
})
|
|
23275
|
+
})
|
|
22871
23276
|
},
|
|
22872
23277
|
{
|
|
22873
23278
|
name: "list_courses",
|
|
22874
23279
|
description: "List the authenticated user's Moodle courses.",
|
|
22875
23280
|
input: external_exports.object({ limit: external_exports.number().int().min(1).max(200).optional().default(100) }).strict(),
|
|
22876
|
-
output: external_exports.object({ courses:
|
|
23281
|
+
output: external_exports.object({ courses: external_exports.array(courseValue) })
|
|
22877
23282
|
},
|
|
22878
23283
|
{
|
|
22879
23284
|
name: "get_course",
|
|
22880
23285
|
description: "Get one Moodle course and its sections.",
|
|
22881
23286
|
input: external_exports.object({ courseId: positiveId }).strict(),
|
|
22882
|
-
output: external_exports.object({
|
|
23287
|
+
output: external_exports.object({
|
|
23288
|
+
course: external_exports.looseObject({
|
|
23289
|
+
course: courseValue,
|
|
23290
|
+
sections: external_exports.array(sectionValue)
|
|
23291
|
+
})
|
|
23292
|
+
})
|
|
22883
23293
|
},
|
|
22884
23294
|
{
|
|
22885
23295
|
name: "list_activities",
|
|
@@ -22888,19 +23298,19 @@ var TOOL_REGISTRATIONS = [
|
|
|
22888
23298
|
courseId: positiveId,
|
|
22889
23299
|
limit: external_exports.number().int().min(1).max(200).optional().default(100)
|
|
22890
23300
|
}).strict(),
|
|
22891
|
-
output: external_exports.object({ activities:
|
|
23301
|
+
output: external_exports.object({ activities: external_exports.array(activityValue) })
|
|
22892
23302
|
},
|
|
22893
23303
|
{
|
|
22894
23304
|
name: "get_activity",
|
|
22895
23305
|
description: "Get the supported details for one Moodle activity.",
|
|
22896
23306
|
input: external_exports.object({ activityId: positiveId }).strict(),
|
|
22897
|
-
output: external_exports.object({ activity:
|
|
23307
|
+
output: external_exports.object({ activity: activityDetailValue })
|
|
22898
23308
|
},
|
|
22899
23309
|
{
|
|
22900
23310
|
name: "get_grades",
|
|
22901
23311
|
description: "Get the authenticated user's grades for one Moodle course.",
|
|
22902
23312
|
input: external_exports.object({ courseId: positiveId }).strict(),
|
|
22903
|
-
output: external_exports.object({ grades:
|
|
23313
|
+
output: external_exports.object({ grades: gradesValue })
|
|
22904
23314
|
},
|
|
22905
23315
|
{
|
|
22906
23316
|
name: "list_forums",
|
|
@@ -22909,7 +23319,7 @@ var TOOL_REGISTRATIONS = [
|
|
|
22909
23319
|
courseId: positiveId.optional(),
|
|
22910
23320
|
limit: external_exports.number().int().min(1).max(100).optional().default(50)
|
|
22911
23321
|
}).strict(),
|
|
22912
|
-
output: external_exports.object({ forums:
|
|
23322
|
+
output: external_exports.object({ forums: external_exports.array(forumValue) })
|
|
22913
23323
|
},
|
|
22914
23324
|
{
|
|
22915
23325
|
name: "search_forums",
|
|
@@ -22925,13 +23335,28 @@ var TOOL_REGISTRATIONS = [
|
|
|
22925
23335
|
maxForums: external_exports.number().int().min(1).max(50).optional(),
|
|
22926
23336
|
maxDiscussionsPerForum: external_exports.number().int().min(1).max(100).optional()
|
|
22927
23337
|
}).strict(),
|
|
22928
|
-
output: external_exports.object({ results:
|
|
23338
|
+
output: external_exports.object({ results: external_exports.array(forumSearchValue) })
|
|
22929
23339
|
},
|
|
22930
23340
|
{
|
|
22931
23341
|
name: "get_thread",
|
|
22932
23342
|
description: "Get one Moodle forum discussion and its posts.",
|
|
22933
23343
|
input: external_exports.object({ discussionId: positiveId }).strict(),
|
|
22934
|
-
output: external_exports.object({ thread:
|
|
23344
|
+
output: external_exports.object({ thread: threadValue })
|
|
23345
|
+
},
|
|
23346
|
+
{
|
|
23347
|
+
name: "get_file",
|
|
23348
|
+
description: "Fetch one authenticated Moodle file and return its content directly (maximum 16 MiB).",
|
|
23349
|
+
input: external_exports.object({
|
|
23350
|
+
source: external_exports.union([positiveId, external_exports.string().trim().min(1).max(2048)])
|
|
23351
|
+
}).strict(),
|
|
23352
|
+
output: external_exports.object({
|
|
23353
|
+
file: external_exports.object({
|
|
23354
|
+
name: external_exports.string(),
|
|
23355
|
+
mime_type: external_exports.string(),
|
|
23356
|
+
bytes: external_exports.number().int().nonnegative(),
|
|
23357
|
+
uri: external_exports.string()
|
|
23358
|
+
})
|
|
23359
|
+
})
|
|
22935
23360
|
}
|
|
22936
23361
|
];
|
|
22937
23362
|
var TOOL_CATALOG = TOOL_REGISTRATIONS.map(({ name, description, input, output }) => ({
|
|
@@ -22999,12 +23424,11 @@ function createMoodleMcpServer(gateway, options = {}) {
|
|
|
22999
23424
|
}
|
|
23000
23425
|
if (error51 instanceof UnsupportedProtocolVersionError) {
|
|
23001
23426
|
return jsonRpcFailure(id, {
|
|
23002
|
-
code: -
|
|
23003
|
-
message:
|
|
23427
|
+
code: -32022,
|
|
23428
|
+
message: "Unsupported protocol version",
|
|
23004
23429
|
data: {
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
supportedVersions: [...error51.supportedVersions]
|
|
23430
|
+
supported: [...error51.supportedVersions],
|
|
23431
|
+
requested: error51.protocolVersion
|
|
23008
23432
|
}
|
|
23009
23433
|
});
|
|
23010
23434
|
}
|
|
@@ -23052,7 +23476,7 @@ async function callTool(gateway, params) {
|
|
|
23052
23476
|
const payload = await runGatewayTool(gateway, name, input);
|
|
23053
23477
|
const structuredContent = registration.output.parse(wrapToolOutput(name, payload));
|
|
23054
23478
|
return {
|
|
23055
|
-
content:
|
|
23479
|
+
content: toolContent(name, payload),
|
|
23056
23480
|
structuredContent,
|
|
23057
23481
|
resultType: "complete",
|
|
23058
23482
|
_meta: RESULT_META
|
|
@@ -23113,6 +23537,8 @@ async function runGatewayTool(gateway, name, input) {
|
|
|
23113
23537
|
});
|
|
23114
23538
|
case "get_thread":
|
|
23115
23539
|
return gateway.getThread({ discussionId: numberValue3(input.discussionId) });
|
|
23540
|
+
case "get_file":
|
|
23541
|
+
return gateway.getFile({ source: fileSource(input.source) });
|
|
23116
23542
|
default:
|
|
23117
23543
|
throw new McpCallError("TOOL_NOT_FOUND", `Unknown Moodle tool: ${name}`);
|
|
23118
23544
|
}
|
|
@@ -23130,8 +23556,33 @@ function wrapToolOutput(name, payload) {
|
|
|
23130
23556
|
search_forums: "results",
|
|
23131
23557
|
get_thread: "thread"
|
|
23132
23558
|
};
|
|
23559
|
+
if (name === "get_file" && isMoodleFile(payload)) {
|
|
23560
|
+
return {
|
|
23561
|
+
file: {
|
|
23562
|
+
name: payload.name,
|
|
23563
|
+
mime_type: payload.mimeType,
|
|
23564
|
+
bytes: payload.bytes,
|
|
23565
|
+
uri: payload.uri
|
|
23566
|
+
}
|
|
23567
|
+
};
|
|
23568
|
+
}
|
|
23133
23569
|
return { [keys[name] ?? "result"]: payload };
|
|
23134
23570
|
}
|
|
23571
|
+
function toolContent(name, payload) {
|
|
23572
|
+
const text = { type: "text", text: summarizeToolOutput(name, payload) };
|
|
23573
|
+
if (name !== "get_file" || !isMoodleFile(payload)) return [text];
|
|
23574
|
+
return [
|
|
23575
|
+
text,
|
|
23576
|
+
{
|
|
23577
|
+
type: "resource",
|
|
23578
|
+
resource: {
|
|
23579
|
+
uri: payload.uri,
|
|
23580
|
+
mimeType: payload.mimeType,
|
|
23581
|
+
blob: payload.blob
|
|
23582
|
+
}
|
|
23583
|
+
}
|
|
23584
|
+
];
|
|
23585
|
+
}
|
|
23135
23586
|
function summarizeToolOutput(name, payload) {
|
|
23136
23587
|
if (Array.isArray(payload)) {
|
|
23137
23588
|
const labels = {
|
|
@@ -23163,6 +23614,9 @@ function summarizeToolOutput(name, payload) {
|
|
|
23163
23614
|
if (name === "get_thread" && isRecord6(payload)) {
|
|
23164
23615
|
return `Loaded forum thread ${stringValue4(payload.subject) || numberValue3(payload.id)}.`;
|
|
23165
23616
|
}
|
|
23617
|
+
if (name === "get_file" && isMoodleFile(payload)) {
|
|
23618
|
+
return `Loaded Moodle file ${payload.name} (${payload.bytes} bytes).`;
|
|
23619
|
+
}
|
|
23166
23620
|
return "Moodle request completed.";
|
|
23167
23621
|
}
|
|
23168
23622
|
function mapMoodleError(error51) {
|
|
@@ -23198,6 +23652,9 @@ function optionalNumber(value) {
|
|
|
23198
23652
|
function stringValue4(value) {
|
|
23199
23653
|
return typeof value === "string" ? value : "";
|
|
23200
23654
|
}
|
|
23655
|
+
function fileSource(value) {
|
|
23656
|
+
return typeof value === "number" || typeof value === "string" ? value : "";
|
|
23657
|
+
}
|
|
23201
23658
|
function booleanValue2(value) {
|
|
23202
23659
|
return value === true;
|
|
23203
23660
|
}
|
|
@@ -23207,6 +23664,9 @@ function enumValue(value, values) {
|
|
|
23207
23664
|
function isRecord6(value) {
|
|
23208
23665
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23209
23666
|
}
|
|
23667
|
+
function isMoodleFile(value) {
|
|
23668
|
+
return isRecord6(value) && typeof value.name === "string" && typeof value.mimeType === "string" && typeof value.bytes === "number" && typeof value.uri === "string" && typeof value.blob === "string";
|
|
23669
|
+
}
|
|
23210
23670
|
|
|
23211
23671
|
// src/worker/moodle-upstream.ts
|
|
23212
23672
|
var DASHBOARD_PATH2 = "/my/";
|