moodle-cli 0.7.0-alpha.4 → 0.7.0-alpha.6

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.
@@ -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 parse6(formula) {
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 = parse6;
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 = parse6(content, this._parseOptions);
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 = parse6(content, options);
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 = parse6(node, this._parseOptions);
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 = parse6(html, this._parseOptions);
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 parse6(this.toString(), this._parseOptions).firstChild;
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 parse6(data, options = {}) {
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 = parse6;
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 parse6(data, options = {}) {
5931
+ function parse7(data, options = {}) {
5932
5932
  return (0, parse_1.default)(data, options);
5933
5933
  }
5934
- exports.default = parse6;
5935
- exports.parse = parse6;
5936
- parse6.parse = parse_1.default;
5937
- parse6.HTMLElement = html_1.default;
5938
- parse6.CommentNode = comment_1.default;
5939
- parse6.valid = valid_1.default;
5940
- parse6.Node = node_1.default;
5941
- parse6.TextNode = text_1.default;
5942
- parse6.NodeType = type_1.default;
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.4";
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, {
@@ -20383,426 +20089,848 @@ function detectVersion(schema2, defaultTarget) {
20383
20089
  if ($schema === "http://json-schema.org/draft-04/schema#") {
20384
20090
  return "draft-4";
20385
20091
  }
20386
- return defaultTarget ?? "draft-2020-12";
20092
+ return defaultTarget ?? "draft-2020-12";
20093
+ }
20094
+ function resolveRef(ref, ctx) {
20095
+ if (!ref.startsWith("#")) {
20096
+ throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
20097
+ }
20098
+ const path = ref.slice(1).split("/").filter(Boolean);
20099
+ if (path.length === 0) {
20100
+ return ctx.rootSchema;
20101
+ }
20102
+ const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
20103
+ if (path[0] === defsKey) {
20104
+ const key = path[1];
20105
+ if (!key || !ctx.defs[key]) {
20106
+ throw new Error(`Reference not found: ${ref}`);
20107
+ }
20108
+ return ctx.defs[key];
20109
+ }
20110
+ throw new Error(`Reference not found: ${ref}`);
20111
+ }
20112
+ function convertBaseSchema(schema2, ctx) {
20113
+ if (schema2.not !== void 0) {
20114
+ if (typeof schema2.not === "object" && Object.keys(schema2.not).length === 0) {
20115
+ return z.never();
20116
+ }
20117
+ throw new Error("not is not supported in Zod (except { not: {} } for never)");
20118
+ }
20119
+ if (schema2.unevaluatedItems !== void 0) {
20120
+ throw new Error("unevaluatedItems is not supported");
20121
+ }
20122
+ if (schema2.unevaluatedProperties !== void 0) {
20123
+ throw new Error("unevaluatedProperties is not supported");
20124
+ }
20125
+ if (schema2.if !== void 0 || schema2.then !== void 0 || schema2.else !== void 0) {
20126
+ throw new Error("Conditional schemas (if/then/else) are not supported");
20127
+ }
20128
+ if (schema2.dependentSchemas !== void 0 || schema2.dependentRequired !== void 0) {
20129
+ throw new Error("dependentSchemas and dependentRequired are not supported");
20130
+ }
20131
+ if (schema2.$ref) {
20132
+ const refPath = schema2.$ref;
20133
+ if (ctx.refs.has(refPath)) {
20134
+ return ctx.refs.get(refPath);
20135
+ }
20136
+ if (ctx.processing.has(refPath)) {
20137
+ return z.lazy(() => {
20138
+ if (!ctx.refs.has(refPath)) {
20139
+ throw new Error(`Circular reference not resolved: ${refPath}`);
20140
+ }
20141
+ return ctx.refs.get(refPath);
20142
+ });
20143
+ }
20144
+ ctx.processing.add(refPath);
20145
+ const resolved = resolveRef(refPath, ctx);
20146
+ const zodSchema2 = convertSchema(resolved, ctx);
20147
+ ctx.refs.set(refPath, zodSchema2);
20148
+ ctx.processing.delete(refPath);
20149
+ return zodSchema2;
20150
+ }
20151
+ if (schema2.enum !== void 0) {
20152
+ const enumValues = schema2.enum;
20153
+ if (ctx.version === "openapi-3.0" && schema2.nullable === true && enumValues.length === 1 && enumValues[0] === null) {
20154
+ return z.null();
20155
+ }
20156
+ if (enumValues.length === 0) {
20157
+ return z.never();
20158
+ }
20159
+ if (enumValues.length === 1) {
20160
+ return z.literal(enumValues[0]);
20161
+ }
20162
+ if (enumValues.every((v) => typeof v === "string")) {
20163
+ return z.enum(enumValues);
20164
+ }
20165
+ const literalSchemas = enumValues.map((v) => z.literal(v));
20166
+ if (literalSchemas.length < 2) {
20167
+ return literalSchemas[0];
20168
+ }
20169
+ return z.union([literalSchemas[0], literalSchemas[1], ...literalSchemas.slice(2)]);
20170
+ }
20171
+ if (schema2.const !== void 0) {
20172
+ return z.literal(schema2.const);
20173
+ }
20174
+ const type = schema2.type;
20175
+ if (Array.isArray(type)) {
20176
+ const typeSchemas = type.map((t) => {
20177
+ const typeSchema = { ...schema2, type: t };
20178
+ return convertBaseSchema(typeSchema, ctx);
20179
+ });
20180
+ if (typeSchemas.length === 0) {
20181
+ return z.never();
20182
+ }
20183
+ if (typeSchemas.length === 1) {
20184
+ return typeSchemas[0];
20185
+ }
20186
+ return z.union(typeSchemas);
20187
+ }
20188
+ if (!type) {
20189
+ return z.any();
20190
+ }
20191
+ let zodSchema;
20192
+ switch (type) {
20193
+ case "string": {
20194
+ let stringSchema = z.string();
20195
+ if (schema2.format) {
20196
+ const format = schema2.format;
20197
+ if (format === "email") {
20198
+ stringSchema = stringSchema.check(z.email());
20199
+ } else if (format === "uri" || format === "uri-reference") {
20200
+ stringSchema = stringSchema.check(z.url());
20201
+ } else if (format === "uuid" || format === "guid") {
20202
+ stringSchema = stringSchema.check(z.uuid());
20203
+ } else if (format === "date-time") {
20204
+ stringSchema = stringSchema.check(z.iso.datetime());
20205
+ } else if (format === "date") {
20206
+ stringSchema = stringSchema.check(z.iso.date());
20207
+ } else if (format === "time") {
20208
+ stringSchema = stringSchema.check(z.iso.time());
20209
+ } else if (format === "duration") {
20210
+ stringSchema = stringSchema.check(z.iso.duration());
20211
+ } else if (format === "ipv4") {
20212
+ stringSchema = stringSchema.check(z.ipv4());
20213
+ } else if (format === "ipv6") {
20214
+ stringSchema = stringSchema.check(z.ipv6());
20215
+ } else if (format === "mac") {
20216
+ stringSchema = stringSchema.check(z.mac());
20217
+ } else if (format === "cidr") {
20218
+ stringSchema = stringSchema.check(z.cidrv4());
20219
+ } else if (format === "cidr-v6") {
20220
+ stringSchema = stringSchema.check(z.cidrv6());
20221
+ } else if (format === "base64") {
20222
+ stringSchema = stringSchema.check(z.base64());
20223
+ } else if (format === "base64url") {
20224
+ stringSchema = stringSchema.check(z.base64url());
20225
+ } else if (format === "e164") {
20226
+ stringSchema = stringSchema.check(z.e164());
20227
+ } else if (format === "jwt") {
20228
+ stringSchema = stringSchema.check(z.jwt());
20229
+ } else if (format === "emoji") {
20230
+ stringSchema = stringSchema.check(z.emoji());
20231
+ } else if (format === "nanoid") {
20232
+ stringSchema = stringSchema.check(z.nanoid());
20233
+ } else if (format === "cuid") {
20234
+ stringSchema = stringSchema.check(z.cuid());
20235
+ } else if (format === "cuid2") {
20236
+ stringSchema = stringSchema.check(z.cuid2());
20237
+ } else if (format === "ulid") {
20238
+ stringSchema = stringSchema.check(z.ulid());
20239
+ } else if (format === "xid") {
20240
+ stringSchema = stringSchema.check(z.xid());
20241
+ } else if (format === "ksuid") {
20242
+ stringSchema = stringSchema.check(z.ksuid());
20243
+ }
20244
+ }
20245
+ if (typeof schema2.minLength === "number") {
20246
+ stringSchema = stringSchema.min(schema2.minLength);
20247
+ }
20248
+ if (typeof schema2.maxLength === "number") {
20249
+ stringSchema = stringSchema.max(schema2.maxLength);
20250
+ }
20251
+ if (schema2.pattern) {
20252
+ stringSchema = stringSchema.regex(new RegExp(schema2.pattern));
20253
+ }
20254
+ zodSchema = stringSchema;
20255
+ break;
20256
+ }
20257
+ case "number":
20258
+ case "integer": {
20259
+ let numberSchema = type === "integer" ? z.number().int() : z.number();
20260
+ if (typeof schema2.minimum === "number") {
20261
+ numberSchema = numberSchema.min(schema2.minimum);
20262
+ }
20263
+ if (typeof schema2.maximum === "number") {
20264
+ numberSchema = numberSchema.max(schema2.maximum);
20265
+ }
20266
+ if (typeof schema2.exclusiveMinimum === "number") {
20267
+ numberSchema = numberSchema.gt(schema2.exclusiveMinimum);
20268
+ } else if (schema2.exclusiveMinimum === true && typeof schema2.minimum === "number") {
20269
+ numberSchema = numberSchema.gt(schema2.minimum);
20270
+ }
20271
+ if (typeof schema2.exclusiveMaximum === "number") {
20272
+ numberSchema = numberSchema.lt(schema2.exclusiveMaximum);
20273
+ } else if (schema2.exclusiveMaximum === true && typeof schema2.maximum === "number") {
20274
+ numberSchema = numberSchema.lt(schema2.maximum);
20275
+ }
20276
+ if (typeof schema2.multipleOf === "number") {
20277
+ numberSchema = numberSchema.multipleOf(schema2.multipleOf);
20278
+ }
20279
+ zodSchema = numberSchema;
20280
+ break;
20281
+ }
20282
+ case "boolean": {
20283
+ zodSchema = z.boolean();
20284
+ break;
20285
+ }
20286
+ case "null": {
20287
+ zodSchema = z.null();
20288
+ break;
20289
+ }
20290
+ case "object": {
20291
+ const shape = {};
20292
+ const properties = schema2.properties || {};
20293
+ const requiredSet = new Set(schema2.required || []);
20294
+ for (const [key, propSchema] of Object.entries(properties)) {
20295
+ const propZodSchema = convertSchema(propSchema, ctx);
20296
+ shape[key] = requiredSet.has(key) ? propZodSchema : propZodSchema.optional();
20297
+ }
20298
+ if (schema2.propertyNames) {
20299
+ const keySchema = convertSchema(schema2.propertyNames, ctx);
20300
+ const valueSchema = schema2.additionalProperties && typeof schema2.additionalProperties === "object" ? convertSchema(schema2.additionalProperties, ctx) : z.any();
20301
+ if (Object.keys(shape).length === 0) {
20302
+ zodSchema = z.record(keySchema, valueSchema);
20303
+ break;
20304
+ }
20305
+ const objectSchema2 = z.object(shape).passthrough();
20306
+ const recordSchema = z.looseRecord(keySchema, valueSchema);
20307
+ zodSchema = z.intersection(objectSchema2, recordSchema);
20308
+ break;
20309
+ }
20310
+ if (schema2.patternProperties) {
20311
+ const patternProps = schema2.patternProperties;
20312
+ const patternKeys = Object.keys(patternProps);
20313
+ const looseRecords = [];
20314
+ for (const pattern of patternKeys) {
20315
+ const patternValue = convertSchema(patternProps[pattern], ctx);
20316
+ const keySchema = z.string().regex(new RegExp(pattern));
20317
+ looseRecords.push(z.looseRecord(keySchema, patternValue));
20318
+ }
20319
+ const schemasToIntersect = [];
20320
+ if (Object.keys(shape).length > 0) {
20321
+ schemasToIntersect.push(z.object(shape).passthrough());
20322
+ }
20323
+ schemasToIntersect.push(...looseRecords);
20324
+ if (schemasToIntersect.length === 0) {
20325
+ zodSchema = z.object({}).passthrough();
20326
+ } else if (schemasToIntersect.length === 1) {
20327
+ zodSchema = schemasToIntersect[0];
20328
+ } else {
20329
+ let result = z.intersection(schemasToIntersect[0], schemasToIntersect[1]);
20330
+ for (let i = 2; i < schemasToIntersect.length; i++) {
20331
+ result = z.intersection(result, schemasToIntersect[i]);
20332
+ }
20333
+ zodSchema = result;
20334
+ }
20335
+ break;
20336
+ }
20337
+ const objectSchema = z.object(shape);
20338
+ if (schema2.additionalProperties === false) {
20339
+ zodSchema = objectSchema.strict();
20340
+ } else if (typeof schema2.additionalProperties === "object") {
20341
+ zodSchema = objectSchema.catchall(convertSchema(schema2.additionalProperties, ctx));
20342
+ } else {
20343
+ zodSchema = objectSchema.passthrough();
20344
+ }
20345
+ break;
20346
+ }
20347
+ case "array": {
20348
+ const prefixItems = schema2.prefixItems;
20349
+ const items = schema2.items;
20350
+ if (prefixItems && Array.isArray(prefixItems)) {
20351
+ const tupleItems = prefixItems.map((item) => convertSchema(item, ctx));
20352
+ const rest = items && typeof items === "object" && !Array.isArray(items) ? convertSchema(items, ctx) : void 0;
20353
+ if (rest) {
20354
+ zodSchema = z.tuple(tupleItems).rest(rest);
20355
+ } else {
20356
+ zodSchema = z.tuple(tupleItems);
20357
+ }
20358
+ if (typeof schema2.minItems === "number") {
20359
+ zodSchema = zodSchema.check(z.minLength(schema2.minItems));
20360
+ }
20361
+ if (typeof schema2.maxItems === "number") {
20362
+ zodSchema = zodSchema.check(z.maxLength(schema2.maxItems));
20363
+ }
20364
+ } else if (Array.isArray(items)) {
20365
+ const tupleItems = items.map((item) => convertSchema(item, ctx));
20366
+ const rest = schema2.additionalItems && typeof schema2.additionalItems === "object" ? convertSchema(schema2.additionalItems, ctx) : void 0;
20367
+ if (rest) {
20368
+ zodSchema = z.tuple(tupleItems).rest(rest);
20369
+ } else {
20370
+ zodSchema = z.tuple(tupleItems);
20371
+ }
20372
+ if (typeof schema2.minItems === "number") {
20373
+ zodSchema = zodSchema.check(z.minLength(schema2.minItems));
20374
+ }
20375
+ if (typeof schema2.maxItems === "number") {
20376
+ zodSchema = zodSchema.check(z.maxLength(schema2.maxItems));
20377
+ }
20378
+ } else if (items !== void 0) {
20379
+ const element = convertSchema(items, ctx);
20380
+ let arraySchema = z.array(element);
20381
+ if (typeof schema2.minItems === "number") {
20382
+ arraySchema = arraySchema.min(schema2.minItems);
20383
+ }
20384
+ if (typeof schema2.maxItems === "number") {
20385
+ arraySchema = arraySchema.max(schema2.maxItems);
20386
+ }
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;
20387
20397
  }
20388
- function resolveRef(ref, ctx) {
20389
- if (!ref.startsWith("#")) {
20390
- throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
20398
+ function convertSchema(schema2, ctx) {
20399
+ if (typeof schema2 === "boolean") {
20400
+ return schema2 ? z.any() : z.never();
20391
20401
  }
20392
- const path = ref.slice(1).split("/").filter(Boolean);
20393
- if (path.length === 0) {
20394
- return ctx.rootSchema;
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;
20395
20408
  }
20396
- const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
20397
- if (path[0] === defsKey) {
20398
- const key = path[1];
20399
- if (!key || !ctx.defs[key]) {
20400
- throw new Error(`Reference not found: ${ref}`);
20401
- }
20402
- return ctx.defs[key];
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;
20403
20413
  }
20404
- throw new Error(`Reference not found: ${ref}`);
20405
- }
20406
- function convertBaseSchema(schema2, ctx) {
20407
- if (schema2.not !== void 0) {
20408
- if (typeof schema2.not === "object" && Object.keys(schema2.not).length === 0) {
20409
- return z.never();
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;
20410
20424
  }
20411
- throw new Error("not is not supported in Zod (except { not: {} } for never)");
20412
20425
  }
20413
- if (schema2.unevaluatedItems !== void 0) {
20414
- throw new Error("unevaluatedItems is not supported");
20426
+ if (schema2.nullable === true && ctx.version === "openapi-3.0") {
20427
+ baseSchema = z.nullable(baseSchema);
20415
20428
  }
20416
- if (schema2.unevaluatedProperties !== void 0) {
20417
- throw new Error("unevaluatedProperties is not supported");
20429
+ if (schema2.readOnly === true) {
20430
+ baseSchema = z.readonly(baseSchema);
20418
20431
  }
20419
- if (schema2.if !== void 0 || schema2.then !== void 0 || schema2.else !== void 0) {
20420
- throw new Error("Conditional schemas (if/then/else) are not supported");
20432
+ if (schema2.default !== void 0) {
20433
+ baseSchema = baseSchema.default(schema2.default);
20421
20434
  }
20422
- if (schema2.dependentSchemas !== void 0 || schema2.dependentRequired !== void 0) {
20423
- throw new Error("dependentSchemas and dependentRequired are not supported");
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
+ }
20424
20441
  }
20425
- if (schema2.$ref) {
20426
- const refPath = schema2.$ref;
20427
- if (ctx.refs.has(refPath)) {
20428
- return ctx.refs.get(refPath);
20442
+ const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
20443
+ for (const key of contentMetadataKeys) {
20444
+ if (key in schema2) {
20445
+ extraMeta[key] = schema2[key];
20429
20446
  }
20430
- if (ctx.processing.has(refPath)) {
20431
- return z.lazy(() => {
20432
- if (!ctx.refs.has(refPath)) {
20433
- throw new Error(`Circular reference not resolved: ${refPath}`);
20434
- }
20435
- return ctx.refs.get(refPath);
20436
- });
20447
+ }
20448
+ for (const key of Object.keys(schema2)) {
20449
+ if (!RECOGNIZED_KEYS.has(key)) {
20450
+ extraMeta[key] = schema2[key];
20437
20451
  }
20438
- ctx.processing.add(refPath);
20439
- const resolved = resolveRef(refPath, ctx);
20440
- const zodSchema2 = convertSchema(resolved, ctx);
20441
- ctx.refs.set(refPath, zodSchema2);
20442
- ctx.processing.delete(refPath);
20443
- return zodSchema2;
20444
20452
  }
20445
- if (schema2.enum !== void 0) {
20446
- const enumValues = schema2.enum;
20447
- if (ctx.version === "openapi-3.0" && schema2.nullable === true && enumValues.length === 1 && enumValues[0] === null) {
20448
- return z.null();
20449
- }
20450
- if (enumValues.length === 0) {
20451
- return z.never();
20452
- }
20453
- if (enumValues.length === 1) {
20454
- return z.literal(enumValues[0]);
20455
- }
20456
- if (enumValues.every((v) => typeof v === "string")) {
20457
- return z.enum(enumValues);
20458
- }
20459
- const literalSchemas = enumValues.map((v) => z.literal(v));
20460
- if (literalSchemas.length < 2) {
20461
- return literalSchemas[0];
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");
20462
20570
  }
20463
- return z.union([literalSchemas[0], literalSchemas[1], ...literalSchemas.slice(2)]);
20464
20571
  }
20465
- if (schema2.const !== void 0) {
20466
- return z.literal(schema2.const);
20572
+ if (context.protocolVersion && metaVersion && context.protocolVersion !== metaVersion) {
20573
+ throw new RequestMetadataMismatchError("protocolVersion", context.protocolVersion, metaVersion);
20467
20574
  }
20468
- const type = schema2.type;
20469
- if (Array.isArray(type)) {
20470
- const typeSchemas = type.map((t) => {
20471
- const typeSchema = { ...schema2, type: t };
20472
- return convertBaseSchema(typeSchema, ctx);
20473
- });
20474
- if (typeSchemas.length === 0) {
20475
- return z.never();
20476
- }
20477
- if (typeSchemas.length === 1) {
20478
- return typeSchemas[0];
20479
- }
20480
- return z.union(typeSchemas);
20575
+ if (context.method && context.method !== request.method) {
20576
+ throw new RequestMetadataMismatchError("method", context.method, request.method);
20481
20577
  }
20482
- if (!type) {
20483
- return z.any();
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);
20484
20581
  }
20485
- let zodSchema;
20486
- switch (type) {
20487
- case "string": {
20488
- let stringSchema = z.string();
20489
- if (schema2.format) {
20490
- const format = schema2.format;
20491
- if (format === "email") {
20492
- stringSchema = stringSchema.check(z.email());
20493
- } else if (format === "uri" || format === "uri-reference") {
20494
- stringSchema = stringSchema.check(z.url());
20495
- } else if (format === "uuid" || format === "guid") {
20496
- stringSchema = stringSchema.check(z.uuid());
20497
- } else if (format === "date-time") {
20498
- stringSchema = stringSchema.check(z.iso.datetime());
20499
- } else if (format === "date") {
20500
- stringSchema = stringSchema.check(z.iso.date());
20501
- } else if (format === "time") {
20502
- stringSchema = stringSchema.check(z.iso.time());
20503
- } else if (format === "duration") {
20504
- stringSchema = stringSchema.check(z.iso.duration());
20505
- } else if (format === "ipv4") {
20506
- stringSchema = stringSchema.check(z.ipv4());
20507
- } else if (format === "ipv6") {
20508
- stringSchema = stringSchema.check(z.ipv6());
20509
- } else if (format === "mac") {
20510
- stringSchema = stringSchema.check(z.mac());
20511
- } else if (format === "cidr") {
20512
- stringSchema = stringSchema.check(z.cidrv4());
20513
- } else if (format === "cidr-v6") {
20514
- stringSchema = stringSchema.check(z.cidrv6());
20515
- } else if (format === "base64") {
20516
- stringSchema = stringSchema.check(z.base64());
20517
- } else if (format === "base64url") {
20518
- stringSchema = stringSchema.check(z.base64url());
20519
- } else if (format === "e164") {
20520
- stringSchema = stringSchema.check(z.e164());
20521
- } else if (format === "jwt") {
20522
- stringSchema = stringSchema.check(z.jwt());
20523
- } else if (format === "emoji") {
20524
- stringSchema = stringSchema.check(z.emoji());
20525
- } else if (format === "nanoid") {
20526
- stringSchema = stringSchema.check(z.nanoid());
20527
- } else if (format === "cuid") {
20528
- stringSchema = stringSchema.check(z.cuid());
20529
- } else if (format === "cuid2") {
20530
- stringSchema = stringSchema.check(z.cuid2());
20531
- } else if (format === "ulid") {
20532
- stringSchema = stringSchema.check(z.ulid());
20533
- } else if (format === "xid") {
20534
- stringSchema = stringSchema.check(z.xid());
20535
- } else if (format === "ksuid") {
20536
- stringSchema = stringSchema.check(z.ksuid());
20537
- }
20538
- }
20539
- if (typeof schema2.minLength === "number") {
20540
- stringSchema = stringSchema.min(schema2.minLength);
20541
- }
20542
- if (typeof schema2.maxLength === "number") {
20543
- stringSchema = stringSchema.max(schema2.maxLength);
20544
- }
20545
- if (schema2.pattern) {
20546
- stringSchema = stringSchema.regex(new RegExp(schema2.pattern));
20547
- }
20548
- zodSchema = stringSchema;
20549
- break;
20550
- }
20551
- case "number":
20552
- case "integer": {
20553
- let numberSchema = type === "integer" ? z.number().int() : z.number();
20554
- if (typeof schema2.minimum === "number") {
20555
- numberSchema = numberSchema.min(schema2.minimum);
20556
- }
20557
- if (typeof schema2.maximum === "number") {
20558
- numberSchema = numberSchema.max(schema2.maximum);
20559
- }
20560
- if (typeof schema2.exclusiveMinimum === "number") {
20561
- numberSchema = numberSchema.gt(schema2.exclusiveMinimum);
20562
- } else if (schema2.exclusiveMinimum === true && typeof schema2.minimum === "number") {
20563
- numberSchema = numberSchema.gt(schema2.minimum);
20564
- }
20565
- if (typeof schema2.exclusiveMaximum === "number") {
20566
- numberSchema = numberSchema.lt(schema2.exclusiveMaximum);
20567
- } else if (schema2.exclusiveMaximum === true && typeof schema2.maximum === "number") {
20568
- numberSchema = numberSchema.lt(schema2.maximum);
20569
- }
20570
- if (typeof schema2.multipleOf === "number") {
20571
- numberSchema = numberSchema.multipleOf(schema2.multipleOf);
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.6";
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
+ );
20572
20622
  }
20573
- zodSchema = numberSchema;
20574
- break;
20575
- }
20576
- case "boolean": {
20577
- zodSchema = z.boolean();
20578
- break;
20579
- }
20580
- case "null": {
20581
- zodSchema = z.null();
20582
- break;
20583
- }
20584
- case "object": {
20585
- const shape = {};
20586
- const properties = schema2.properties || {};
20587
- const requiredSet = new Set(schema2.required || []);
20588
- for (const [key, propSchema] of Object.entries(properties)) {
20589
- const propZodSchema = convertSchema(propSchema, ctx);
20590
- shape[key] = requiredSet.has(key) ? propZodSchema : propZodSchema.optional();
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
+ });
20591
20627
  }
20592
- if (schema2.propertyNames) {
20593
- const keySchema = convertSchema(schema2.propertyNames, ctx);
20594
- const valueSchema = schema2.additionalProperties && typeof schema2.additionalProperties === "object" ? convertSchema(schema2.additionalProperties, ctx) : z.any();
20595
- if (Object.keys(shape).length === 0) {
20596
- zodSchema = z.record(keySchema, valueSchema);
20597
- break;
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();
20598
20634
  }
20599
- const objectSchema2 = z.object(shape).passthrough();
20600
- const recordSchema = z.looseRecord(keySchema, valueSchema);
20601
- zodSchema = z.intersection(objectSchema2, recordSchema);
20602
- break;
20635
+ const server = typeof dependencies.mcpServer === "function" ? dependencies.mcpServer(env) : dependencies.mcpServer;
20636
+ return handleMcpRequest(request, server);
20603
20637
  }
20604
- if (schema2.patternProperties) {
20605
- const patternProps = schema2.patternProperties;
20606
- const patternKeys = Object.keys(patternProps);
20607
- const looseRecords = [];
20608
- for (const pattern of patternKeys) {
20609
- const patternValue = convertSchema(patternProps[pattern], ctx);
20610
- const keySchema = z.string().regex(new RegExp(pattern));
20611
- looseRecords.push(z.looseRecord(keySchema, patternValue));
20612
- }
20613
- const schemasToIntersect = [];
20614
- if (Object.keys(shape).length > 0) {
20615
- schemasToIntersect.push(z.object(shape).passthrough());
20616
- }
20617
- schemasToIntersect.push(...looseRecords);
20618
- if (schemasToIntersect.length === 0) {
20619
- zodSchema = z.object({}).passthrough();
20620
- } else if (schemasToIntersect.length === 1) {
20621
- zodSchema = schemasToIntersect[0];
20622
- } else {
20623
- let result = z.intersection(schemasToIntersect[0], schemasToIntersect[1]);
20624
- for (let i = 2; i < schemasToIntersect.length; i++) {
20625
- result = z.intersection(result, schemasToIntersect[i]);
20626
- }
20627
- zodSchema = result;
20628
- }
20629
- break;
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);
20630
20649
  }
20631
- const objectSchema = z.object(shape);
20632
- if (schema2.additionalProperties === false) {
20633
- zodSchema = objectSchema.strict();
20634
- } else if (typeof schema2.additionalProperties === "object") {
20635
- zodSchema = objectSchema.catchall(convertSchema(schema2.additionalProperties, ctx));
20636
- } else {
20637
- zodSchema = objectSchema.passthrough();
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();
20638
20655
  }
20639
- break;
20640
- }
20641
- case "array": {
20642
- const prefixItems = schema2.prefixItems;
20643
- const items = schema2.items;
20644
- if (prefixItems && Array.isArray(prefixItems)) {
20645
- const tupleItems = prefixItems.map((item) => convertSchema(item, ctx));
20646
- const rest = items && typeof items === "object" && !Array.isArray(items) ? convertSchema(items, ctx) : void 0;
20647
- if (rest) {
20648
- zodSchema = z.tuple(tupleItems).rest(rest);
20649
- } else {
20650
- zodSchema = z.tuple(tupleItems);
20651
- }
20652
- if (typeof schema2.minItems === "number") {
20653
- zodSchema = zodSchema.check(z.minLength(schema2.minItems));
20654
- }
20655
- if (typeof schema2.maxItems === "number") {
20656
- zodSchema = zodSchema.check(z.maxLength(schema2.maxItems));
20657
- }
20658
- } else if (Array.isArray(items)) {
20659
- const tupleItems = items.map((item) => convertSchema(item, ctx));
20660
- const rest = schema2.additionalItems && typeof schema2.additionalItems === "object" ? convertSchema(schema2.additionalItems, ctx) : void 0;
20661
- if (rest) {
20662
- zodSchema = z.tuple(tupleItems).rest(rest);
20663
- } else {
20664
- zodSchema = z.tuple(tupleItems);
20665
- }
20666
- if (typeof schema2.minItems === "number") {
20667
- zodSchema = zodSchema.check(z.minLength(schema2.minItems));
20668
- }
20669
- if (typeof schema2.maxItems === "number") {
20670
- zodSchema = zodSchema.check(z.maxLength(schema2.maxItems));
20671
- }
20672
- } else if (items !== void 0) {
20673
- const element = convertSchema(items, ctx);
20674
- let arraySchema = z.array(element);
20675
- if (typeof schema2.minItems === "number") {
20676
- arraySchema = arraySchema.min(schema2.minItems);
20677
- }
20678
- if (typeof schema2.maxItems === "number") {
20679
- arraySchema = arraySchema.max(schema2.maxItems);
20680
- }
20681
- zodSchema = arraySchema;
20682
- } else {
20683
- zodSchema = z.array(z.any());
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
+ });
20684
20661
  }
20685
- break;
20662
+ return problemResponse(404, "NOT_FOUND", "Not Found", "The requested route does not exist.");
20686
20663
  }
20687
- default:
20688
- throw new Error(`Unsupported type: ${type}`);
20689
- }
20690
- return zodSchema;
20664
+ };
20691
20665
  }
20692
- function convertSchema(schema2, ctx) {
20693
- if (typeof schema2 === "boolean") {
20694
- return schema2 ? z.any() : z.never();
20695
- }
20696
- let baseSchema = convertBaseSchema(schema2, ctx);
20697
- const hasExplicitType = schema2.type || schema2.enum !== void 0 || schema2.const !== void 0;
20698
- if (schema2.anyOf && Array.isArray(schema2.anyOf)) {
20699
- const options = schema2.anyOf.map((s) => convertSchema(s, ctx));
20700
- const anyOfUnion = z.union(options);
20701
- baseSchema = hasExplicitType ? z.intersection(baseSchema, anyOfUnion) : anyOfUnion;
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
- if (schema2.oneOf && Array.isArray(schema2.oneOf)) {
20704
- const options = schema2.oneOf.map((s) => convertSchema(s, ctx));
20705
- const oneOfUnion = z.xor(options);
20706
- baseSchema = hasExplicitType ? z.intersection(baseSchema, oneOfUnion) : oneOfUnion;
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
- if (schema2.allOf && Array.isArray(schema2.allOf)) {
20709
- if (schema2.allOf.length === 0) {
20710
- baseSchema = hasExplicitType ? baseSchema : z.any();
20711
- } else {
20712
- let result = hasExplicitType ? baseSchema : convertSchema(schema2.allOf[0], ctx);
20713
- const startIdx = hasExplicitType ? 0 : 1;
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
- if (schema2.nullable === true && ctx.version === "openapi-3.0") {
20721
- baseSchema = z.nullable(baseSchema);
20722
- }
20723
- if (schema2.readOnly === true) {
20724
- baseSchema = z.readonly(baseSchema);
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
- if (schema2.default !== void 0) {
20727
- baseSchema = baseSchema.default(schema2.default);
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 extraMeta = {};
20730
- const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
20731
- for (const key of coreMetadataKeys) {
20732
- if (key in schema2) {
20733
- extraMeta[key] = schema2[key];
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
- const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
20737
- for (const key of contentMetadataKeys) {
20738
- if (key in schema2) {
20739
- extraMeta[key] = schema2[key];
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
- for (const key of Object.keys(schema2)) {
20743
- if (!RECOGNIZED_KEYS.has(key)) {
20744
- extraMeta[key] = schema2[key];
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.");
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
+ });
20745
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);
20792
+ }
20793
+ async function safeProblem(response) {
20794
+ try {
20795
+ const value = await response.json();
20796
+ return isRecord2(value) && (value.code === void 0 || typeof value.code === "string") ? value : null;
20797
+ } catch {
20798
+ return null;
20749
20799
  }
20750
- if (schema2.description) {
20751
- baseSchema = baseSchema.describe(schema2.description);
20800
+ }
20801
+ function validateProtocolMetadata(headers, body, protocolVersion) {
20802
+ if (!protocolVersion) {
20803
+ return headerMismatchResponse(body, "MCP-Protocol-Version is required.");
20752
20804
  }
20753
- return baseSchema;
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;
20754
20855
  }
20755
- function fromJSONSchema(schema2, params) {
20756
- if (typeof schema2 === "boolean") {
20757
- return schema2 ? z.any() : z.never();
20758
- }
20759
- let normalized;
20760
- try {
20761
- normalized = JSON.parse(JSON.stringify(schema2));
20762
- } catch {
20763
- throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
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
- const version2 = detectVersion(normalized, params?.defaultTarget);
20766
- const defs = normalized.$defs || normalized.definitions || {};
20767
- const ctx = {
20768
- version: version2,
20769
- defs,
20770
- refs: /* @__PURE__ */ new Map(),
20771
- processing: /* @__PURE__ */ new Set(),
20772
- rootSchema: normalized,
20773
- registry: params?.registry ?? globalRegistry
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
- // node_modules/zod/v4/classic/coerce.js
20779
- var coerce_exports = {};
20780
- __export(coerce_exports, {
20781
- bigint: () => bigint3,
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 number3(params) {
20791
- return _coercedNumber(ZodNumber, params);
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 boolean3(params) {
20794
- return _coercedBoolean(ZodBoolean, params);
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 bigint3(params) {
20797
- return _coercedBigint(ZodBigInt, params);
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 date4(params) {
20800
- return _coercedDate(ZodDate, params);
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: stringValue(data.username),
20921
- fullname: stringValue(data.fullname),
20922
- sitename: stringValue(data.sitename),
20923
- siteurl: stringValue(data.siteurl),
20924
- lang: stringValue(data.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: stringValue(data.shortname),
20932
- fullname: stringValue(data.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: stringValue(data.name),
20951
- modname: stringValue(data.modname),
20952
- url: stringValue(data.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: stringValue(data.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: stringValue(data.name),
21089
+ name: stringValue2(data.name),
20962
21090
  section: numberValue(data.section),
20963
21091
  visible: booleanValue(data.visible, true),
20964
- summary: stringValue(data.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 = stringValue(data.sectionid);
20979
- const module = stringValue(data.module) || stringValue(data.plugin).replace(/^mod_/u, "") || stringValue(data.modname).toLowerCase();
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: stringValue(data.url) ? resolveUrl(baseUrl, stringValue(data.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(stringValue(activityId))).filter((activity) => activity !== void 0);
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: stringValue(data.name),
21016
- activity_name: stringValue(data.activityname),
21017
- modname: stringValue(data.modulename),
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: stringValue(course.fullname),
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: stringValue(action.name),
21024
- action_url: stringValue(action.url),
21025
- url: stringValue(data.url),
21026
- event_type: stringValue(data.eventtype),
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: stringValue(data.subject),
21038
- short_subject: stringValue(data.shortenedsubject),
21039
- event_type: stringValue(data.eventtype),
21040
- component: stringValue(data.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: stringValue(data.timecreatedpretty),
21170
+ created_pretty: stringValue2(data.timecreatedpretty),
21043
21171
  read: booleanValue(data.read),
21044
- context_url: stringValue(data.contexturl),
21045
- context_name: stringValue(data.contexturlname)
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: stringValue(data.fullname),
21075
- profile_url: stringValue(urls.profile),
21076
- profile_image_url: stringValue(urls.profileimage)
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 = stringValue(data.message);
21083
- const structured = htmlToStructuredContent(messageHtml, stringValue(urls.view || urls.discuss) || baseUrl);
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: stringValue(data.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: stringValue(urls.view || urls.viewisolated),
21102
- reply_url: stringValue(urls.reply)
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: stringValue(data.groupname),
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 stringValue(value) {
21253
+ function stringValue2(value) {
21126
21254
  return typeof value === "string" ? value : value == null ? "" : String(value);
21127
21255
  }
21128
21256
  function numberValue(value) {
@@ -21149,7 +21277,7 @@ function parseJsonValue(value) {
21149
21277
  }
21150
21278
  }
21151
21279
  function htmlText(value, baseUrl) {
21152
- return htmlToStructuredContent(stringValue(value), baseUrl).text;
21280
+ return htmlToStructuredContent(stringValue2(value), baseUrl).text;
21153
21281
  }
21154
21282
 
21155
21283
  // src/scraper.ts
@@ -21183,7 +21311,7 @@ function parseMoodleErrorHtml(html) {
21183
21311
  function parsePageContext(html, baseUrl) {
21184
21312
  const root = (0, import_node_html_parser2.parse)(html);
21185
21313
  const config2 = parseMoodleConfig(html);
21186
- const sesskey = stringValue2(config2.sesskey).trim();
21314
+ const sesskey = stringValue3(config2.sesskey).trim();
21187
21315
  const userid = numberValue2(config2.userId) || numberValue2(root.querySelector("[data-user-id]")?.getAttribute("data-user-id"));
21188
21316
  if (!sesskey || !userid) {
21189
21317
  throw new Error("Session appears invalid: could not load authenticated Moodle context");
@@ -21196,7 +21324,7 @@ function parsePageContext(html, baseUrl) {
21196
21324
  fullname: cleanNodeText(root.querySelector(".userfullname")),
21197
21325
  sitename: extractSitename(root),
21198
21326
  siteurl: baseUrl,
21199
- lang: stringValue2(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
21327
+ lang: stringValue3(config2.language) || root.querySelector("html")?.getAttribute("lang") || ""
21200
21328
  }
21201
21329
  };
21202
21330
  }
@@ -21266,9 +21394,9 @@ function parseCourseSectionNumbers(html, courseId) {
21266
21394
  for (const href of hrefs) {
21267
21395
  const url2 = parseMaybeUrl(href.replace(/&amp;/gu, "&"), "https://moodle.invalid");
21268
21396
  const id = url2?.searchParams.get("id");
21269
- const sectionValue = url2?.searchParams.get("section");
21270
- if (id === String(courseId) && sectionValue && /^\d+$/.test(sectionValue)) {
21271
- const section = Number(sectionValue);
21397
+ const sectionValue2 = url2?.searchParams.get("section");
21398
+ if (id === String(courseId) && sectionValue2 && /^\d+$/.test(sectionValue2)) {
21399
+ const section = Number(sectionValue2);
21272
21400
  if (!sections.includes(section)) {
21273
21401
  sections.push(section);
21274
21402
  }
@@ -21743,7 +21871,7 @@ function numberValue2(value) {
21743
21871
  }
21744
21872
  return 0;
21745
21873
  }
21746
- function stringValue2(value) {
21874
+ function stringValue3(value) {
21747
21875
  return typeof value === "string" ? value : value == null ? "" : String(value);
21748
21876
  }
21749
21877
  function fileEntry(name, url2, baseUrl) {
@@ -21870,13 +21998,13 @@ var ForumModule = class {
21870
21998
  }
21871
21999
  };
21872
22000
  function shouldFallbackForumAjax(error51) {
21873
- if (!isRecord3(error51)) {
22001
+ if (!isRecord4(error51)) {
21874
22002
  return false;
21875
22003
  }
21876
22004
  const code = typeof error51.moodleErrorCode === "string" ? error51.moodleErrorCode : "";
21877
22005
  return code === "servicenotavailable" || code === "accessexception" || error51 instanceof Error && error51.message.includes("Web service is not available");
21878
22006
  }
21879
- function isRecord3(value) {
22007
+ function isRecord4(value) {
21880
22008
  return typeof value === "object" && value !== null && !Array.isArray(value);
21881
22009
  }
21882
22010
 
@@ -22177,7 +22305,7 @@ var MoodleClientCore = class {
22177
22305
  await this.ensureSession();
22178
22306
  try {
22179
22307
  const data = await this.call(FUNC_GET_SITE_INFO);
22180
- if (isRecord4(data) && "userid" in data) {
22308
+ if (isRecord5(data) && "userid" in data) {
22181
22309
  const info = parseUserInfo(data);
22182
22310
  this.sesskey = typeof data.sesskey === "string" ? data.sesskey : this.sesskey;
22183
22311
  this.userid = info.userid;
@@ -22262,9 +22390,9 @@ var MoodleClientCore = class {
22262
22390
  let type = "";
22263
22391
  try {
22264
22392
  const data = await this.call(FUNC_GET_COURSE_MODULE, { cmid: id });
22265
- const module = isRecord4(data) && isRecord4(data.cm) ? data.cm : data;
22266
- type = isRecord4(module) && typeof module.modname === "string" ? module.modname : "";
22267
- courseId = isRecord4(module) && typeof module.course === "number" ? module.course : void 0;
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;
22268
22396
  } catch (error51) {
22269
22397
  if (!this.errors.isApi(error51) || error51.moodleErrorCode !== "servicenotavailable") {
22270
22398
  throw error51;
@@ -22297,7 +22425,7 @@ var MoodleClientCore = class {
22297
22425
  aftereventid: 0,
22298
22426
  limittononsuspendedevents: true
22299
22427
  });
22300
- const events = isRecord4(data) && Array.isArray(data.events) ? data.events : [];
22428
+ const events = isRecord5(data) && Array.isArray(data.events) ? data.events : [];
22301
22429
  return parseTodoItems(events);
22302
22430
  }
22303
22431
  async getAlerts(limit = 20) {
@@ -22542,8 +22670,8 @@ var MoodleClientCore = class {
22542
22670
  }
22543
22671
  if (!response.ok) {
22544
22672
  const context = `HTTP ${response.status} loading ${safeUrl(url2)}`;
22545
- const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
22546
- if (contentType.includes("text/html") || contentType.includes("application/xhtml+xml")) {
22673
+ const contentType2 = response.headers.get("content-type")?.toLowerCase() ?? "";
22674
+ if (contentType2.includes("text/html") || contentType2.includes("application/xhtml+xml")) {
22547
22675
  const moodleError = await response.text().then(parseMoodleErrorHtml).catch(() => null);
22548
22676
  if (moodleError) {
22549
22677
  throw this.errors.api(`${moodleError.message} (${context})`, moodleError.code);
@@ -22558,7 +22686,7 @@ var MoodleClientCore = class {
22558
22686
  let offset = 0;
22559
22687
  while (true) {
22560
22688
  const data = await this.call(FUNC_GET_COURSES_BY_TIMELINE, { classification: "all", limit: 100, offset });
22561
- if (!isRecord4(data) || !Array.isArray(data.courses) || !data.courses.length) {
22689
+ if (!isRecord5(data) || !Array.isArray(data.courses) || !data.courses.length) {
22562
22690
  break;
22563
22691
  }
22564
22692
  courses.push(...data.courses);
@@ -22723,7 +22851,7 @@ function queryMatches(text, query) {
22723
22851
  return needle ? haystack.includes(needle) || needle.split(" ").every((token) => haystack.includes(token)) : true;
22724
22852
  }
22725
22853
  function parseTodoPayload(value) {
22726
- return parseTodoItems(isRecord4(value) && Array.isArray(value.events) ? value.events : []);
22854
+ return parseTodoItems(isRecord5(value) && Array.isArray(value.events) ? value.events : []);
22727
22855
  }
22728
22856
  function errorMessage(value) {
22729
22857
  return value instanceof Error ? value.message : "Unknown Moodle error";
@@ -22735,7 +22863,7 @@ function chunks(values, size) {
22735
22863
  }
22736
22864
  return result;
22737
22865
  }
22738
- function isRecord4(value) {
22866
+ function isRecord5(value) {
22739
22867
  return !!value && typeof value === "object" && !Array.isArray(value);
22740
22868
  }
22741
22869
  function isLoginErrorCode(code) {
@@ -22758,6 +22886,8 @@ function safeUrl(value) {
22758
22886
  }
22759
22887
 
22760
22888
  // src/mcp/gateway.ts
22889
+ var import_node_html_parser3 = __toESM(require_dist(), 1);
22890
+ var MAX_MCP_FILE_BYTES = 16 * 1024 * 1024;
22761
22891
  var MoodleGatewayError = class extends Error {
22762
22892
  code;
22763
22893
  constructor(code, message) {
@@ -22793,95 +22923,211 @@ function createMoodleGateway(client) {
22793
22923
  return limit === void 0 ? forums : forums.slice(0, limit);
22794
22924
  },
22795
22925
  searchForums: ({ forumId, ...input }) => client.searchForumContent({ ...input, forumCmid: forumId }),
22796
- 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
+ }
22797
22963
  };
22798
22964
  }
22799
-
22800
- // src/mcp/protocol.ts
22801
- var MODERN_PROTOCOL_VERSION = "2026-07-28";
22802
- var LEGACY_PROTOCOL_VERSION = "2025-11-25";
22803
- var SUPPORTED_PROTOCOL_VERSIONS = [MODERN_PROTOCOL_VERSION, LEGACY_PROTOCOL_VERSION];
22804
- var JsonRpcRequestSchema = external_exports.object({
22805
- jsonrpc: external_exports.literal("2.0"),
22806
- id: external_exports.union([external_exports.string(), external_exports.number(), external_exports.null()]).optional(),
22807
- method: external_exports.string().trim().min(1),
22808
- params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
22809
- }).strict();
22810
- var ModernClientMetadataSchema = external_exports.object({
22811
- "io.modelcontextprotocol/clientCapabilities": external_exports.record(external_exports.string(), external_exports.unknown()),
22812
- "io.modelcontextprotocol/clientInfo": external_exports.object({
22813
- name: external_exports.string().trim().min(1),
22814
- version: external_exports.string().trim().min(1)
22815
- }).passthrough(),
22816
- "io.modelcontextprotocol/protocolVersion": external_exports.literal(MODERN_PROTOCOL_VERSION)
22817
- }).passthrough();
22818
- var UnsupportedProtocolVersionError = class extends Error {
22819
- protocolVersion;
22820
- supportedVersions = SUPPORTED_PROTOCOL_VERSIONS;
22821
- constructor(protocolVersion) {
22822
- super(`Unsupported MCP protocol version: ${protocolVersion}`);
22823
- this.name = "UnsupportedProtocolVersionError";
22824
- 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));
22825
22971
  }
22826
- };
22827
- var RequestMetadataMismatchError = class extends Error {
22828
- field;
22829
- expected;
22830
- actual;
22831
- constructor(field, expected, actual) {
22832
- super(`MCP request metadata mismatch for ${field}.`);
22833
- this.name = "RequestMetadataMismatchError";
22834
- this.field = field;
22835
- this.expected = expected;
22836
- this.actual = actual;
22972
+ let url2;
22973
+ try {
22974
+ url2 = new URL(source);
22975
+ } catch {
22976
+ throw invalidFileSource();
22837
22977
  }
22838
- };
22839
- function parseJsonRpcRequest(input) {
22840
- return JsonRpcRequestSchema.parse(input);
22841
- }
22842
- function resolveProtocolVersion(request, context = {}) {
22843
- const meta3 = isRecord5(request.params?._meta) ? request.params._meta : void 0;
22844
- const initializeVersion = request.method === "initialize" && typeof request.params?.protocolVersion === "string" ? request.params.protocolVersion : void 0;
22845
- const requested = context.protocolVersion ?? stringValue3(meta3?.["io.modelcontextprotocol/protocolVersion"]) ?? initializeVersion ?? MODERN_PROTOCOL_VERSION;
22846
- if (!isSupportedProtocolVersion(requested)) {
22847
- 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));
22848
22985
  }
22849
- return requested;
22986
+ if (!url2.pathname.startsWith(`${sitePath}/pluginfile.php/`)) throw invalidFileSource();
22987
+ return { url: url2.toString(), name: urlFilename(url2.toString()) };
22850
22988
  }
22851
- function assertRequestMetadata(request, protocolVersion, context = {}) {
22852
- const meta3 = isRecord5(request.params?._meta) ? request.params._meta : void 0;
22853
- const metaVersion = stringValue3(meta3?.["io.modelcontextprotocol/protocolVersion"]);
22854
- if (protocolVersion === MODERN_PROTOCOL_VERSION) {
22855
- const parsed = ModernClientMetadataSchema.safeParse(meta3);
22856
- if (!parsed.success) {
22857
- throw new RequestMetadataMismatchError("_meta", "valid modern client metadata");
22858
- }
22859
- }
22860
- if (context.protocolVersion && metaVersion && context.protocolVersion !== metaVersion) {
22861
- throw new RequestMetadataMismatchError("protocolVersion", context.protocolVersion, metaVersion);
22862
- }
22863
- if (context.method && context.method !== request.method) {
22864
- throw new RequestMetadataMismatchError("method", context.method, request.method);
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
+ );
22865
23009
  }
22866
- const toolName = typeof request.params?.name === "string" ? request.params.name : void 0;
22867
- if (context.toolName && context.toolName !== toolName) {
22868
- throw new RequestMetadataMismatchError("name", context.toolName, toolName);
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
+ );
22869
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
+ );
22870
23060
  }
22871
- function isSupportedProtocolVersion(value) {
22872
- return SUPPORTED_PROTOCOL_VERSIONS.some((version2) => version2 === value);
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
+ );
22873
23066
  }
22874
- function jsonRpcSuccess(id, result) {
22875
- return { jsonrpc: "2.0", id, result };
23067
+ function fileAuthenticationRequired() {
23068
+ return new MoodleGatewayError(
23069
+ "MOODLE_AUTH_REQUIRED",
23070
+ "Moodle returned a login page instead of the requested file."
23071
+ );
22876
23072
  }
22877
- function jsonRpcFailure(id, error51) {
22878
- return { jsonrpc: "2.0", id, error: error51 };
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");
22879
23077
  }
22880
- function isRecord5(value) {
22881
- return typeof value === "object" && value !== null && !Array.isArray(value);
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);
22882
23081
  }
22883
- function stringValue3(value) {
22884
- return typeof value === "string" ? value : void 0;
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);
22885
23131
  }
22886
23132
 
22887
23133
  // src/mcp/server.ts
@@ -22894,14 +23140,122 @@ var READ_ONLY_ANNOTATIONS = {
22894
23140
  };
22895
23141
  var emptyInput = external_exports.object({}).strict();
22896
23142
  var positiveId = external_exports.number().int().positive();
22897
- var objectOutput = external_exports.looseObject({});
22898
- var listOutput = external_exports.array(objectOutput);
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
+ });
22899
23253
  var TOOL_REGISTRATIONS = [
22900
23254
  {
22901
23255
  name: "get_user",
22902
23256
  description: "Get the authenticated Moodle user and site.",
22903
23257
  input: emptyInput,
22904
- output: external_exports.object({ user: objectOutput })
23258
+ output: external_exports.object({ user: userValue })
22905
23259
  },
22906
23260
  {
22907
23261
  name: "get_overview",
@@ -22911,19 +23265,31 @@ var TOOL_REGISTRATIONS = [
22911
23265
  todoDays: external_exports.number().int().min(1).max(365).optional(),
22912
23266
  alertsLimit: external_exports.number().int().min(1).max(100).optional().default(5)
22913
23267
  }).strict(),
22914
- output: external_exports.object({ overview: objectOutput })
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
+ })
22915
23276
  },
22916
23277
  {
22917
23278
  name: "list_courses",
22918
23279
  description: "List the authenticated user's Moodle courses.",
22919
23280
  input: external_exports.object({ limit: external_exports.number().int().min(1).max(200).optional().default(100) }).strict(),
22920
- output: external_exports.object({ courses: listOutput })
23281
+ output: external_exports.object({ courses: external_exports.array(courseValue) })
22921
23282
  },
22922
23283
  {
22923
23284
  name: "get_course",
22924
23285
  description: "Get one Moodle course and its sections.",
22925
23286
  input: external_exports.object({ courseId: positiveId }).strict(),
22926
- output: external_exports.object({ course: objectOutput })
23287
+ output: external_exports.object({
23288
+ course: external_exports.looseObject({
23289
+ course: courseValue,
23290
+ sections: external_exports.array(sectionValue)
23291
+ })
23292
+ })
22927
23293
  },
22928
23294
  {
22929
23295
  name: "list_activities",
@@ -22932,19 +23298,19 @@ var TOOL_REGISTRATIONS = [
22932
23298
  courseId: positiveId,
22933
23299
  limit: external_exports.number().int().min(1).max(200).optional().default(100)
22934
23300
  }).strict(),
22935
- output: external_exports.object({ activities: listOutput })
23301
+ output: external_exports.object({ activities: external_exports.array(activityValue) })
22936
23302
  },
22937
23303
  {
22938
23304
  name: "get_activity",
22939
23305
  description: "Get the supported details for one Moodle activity.",
22940
23306
  input: external_exports.object({ activityId: positiveId }).strict(),
22941
- output: external_exports.object({ activity: objectOutput })
23307
+ output: external_exports.object({ activity: activityDetailValue })
22942
23308
  },
22943
23309
  {
22944
23310
  name: "get_grades",
22945
23311
  description: "Get the authenticated user's grades for one Moodle course.",
22946
23312
  input: external_exports.object({ courseId: positiveId }).strict(),
22947
- output: external_exports.object({ grades: objectOutput })
23313
+ output: external_exports.object({ grades: gradesValue })
22948
23314
  },
22949
23315
  {
22950
23316
  name: "list_forums",
@@ -22953,7 +23319,7 @@ var TOOL_REGISTRATIONS = [
22953
23319
  courseId: positiveId.optional(),
22954
23320
  limit: external_exports.number().int().min(1).max(100).optional().default(50)
22955
23321
  }).strict(),
22956
- output: external_exports.object({ forums: listOutput })
23322
+ output: external_exports.object({ forums: external_exports.array(forumValue) })
22957
23323
  },
22958
23324
  {
22959
23325
  name: "search_forums",
@@ -22969,13 +23335,28 @@ var TOOL_REGISTRATIONS = [
22969
23335
  maxForums: external_exports.number().int().min(1).max(50).optional(),
22970
23336
  maxDiscussionsPerForum: external_exports.number().int().min(1).max(100).optional()
22971
23337
  }).strict(),
22972
- output: external_exports.object({ results: listOutput })
23338
+ output: external_exports.object({ results: external_exports.array(forumSearchValue) })
22973
23339
  },
22974
23340
  {
22975
23341
  name: "get_thread",
22976
23342
  description: "Get one Moodle forum discussion and its posts.",
22977
23343
  input: external_exports.object({ discussionId: positiveId }).strict(),
22978
- output: external_exports.object({ thread: objectOutput })
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
+ })
22979
23360
  }
22980
23361
  ];
22981
23362
  var TOOL_CATALOG = TOOL_REGISTRATIONS.map(({ name, description, input, output }) => ({
@@ -23043,12 +23424,11 @@ function createMoodleMcpServer(gateway, options = {}) {
23043
23424
  }
23044
23425
  if (error51 instanceof UnsupportedProtocolVersionError) {
23045
23426
  return jsonRpcFailure(id, {
23046
- code: -32602,
23047
- message: error51.message,
23427
+ code: -32022,
23428
+ message: "Unsupported protocol version",
23048
23429
  data: {
23049
- type: "UNSUPPORTED_PROTOCOL_VERSION",
23050
- protocolVersion: error51.protocolVersion,
23051
- supportedVersions: [...error51.supportedVersions]
23430
+ supported: [...error51.supportedVersions],
23431
+ requested: error51.protocolVersion
23052
23432
  }
23053
23433
  });
23054
23434
  }
@@ -23096,7 +23476,7 @@ async function callTool(gateway, params) {
23096
23476
  const payload = await runGatewayTool(gateway, name, input);
23097
23477
  const structuredContent = registration.output.parse(wrapToolOutput(name, payload));
23098
23478
  return {
23099
- content: [{ type: "text", text: summarizeToolOutput(name, payload) }],
23479
+ content: toolContent(name, payload),
23100
23480
  structuredContent,
23101
23481
  resultType: "complete",
23102
23482
  _meta: RESULT_META
@@ -23157,6 +23537,8 @@ async function runGatewayTool(gateway, name, input) {
23157
23537
  });
23158
23538
  case "get_thread":
23159
23539
  return gateway.getThread({ discussionId: numberValue3(input.discussionId) });
23540
+ case "get_file":
23541
+ return gateway.getFile({ source: fileSource(input.source) });
23160
23542
  default:
23161
23543
  throw new McpCallError("TOOL_NOT_FOUND", `Unknown Moodle tool: ${name}`);
23162
23544
  }
@@ -23174,8 +23556,33 @@ function wrapToolOutput(name, payload) {
23174
23556
  search_forums: "results",
23175
23557
  get_thread: "thread"
23176
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
+ }
23177
23569
  return { [keys[name] ?? "result"]: payload };
23178
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
+ }
23179
23586
  function summarizeToolOutput(name, payload) {
23180
23587
  if (Array.isArray(payload)) {
23181
23588
  const labels = {
@@ -23207,6 +23614,9 @@ function summarizeToolOutput(name, payload) {
23207
23614
  if (name === "get_thread" && isRecord6(payload)) {
23208
23615
  return `Loaded forum thread ${stringValue4(payload.subject) || numberValue3(payload.id)}.`;
23209
23616
  }
23617
+ if (name === "get_file" && isMoodleFile(payload)) {
23618
+ return `Loaded Moodle file ${payload.name} (${payload.bytes} bytes).`;
23619
+ }
23210
23620
  return "Moodle request completed.";
23211
23621
  }
23212
23622
  function mapMoodleError(error51) {
@@ -23242,6 +23652,9 @@ function optionalNumber(value) {
23242
23652
  function stringValue4(value) {
23243
23653
  return typeof value === "string" ? value : "";
23244
23654
  }
23655
+ function fileSource(value) {
23656
+ return typeof value === "number" || typeof value === "string" ? value : "";
23657
+ }
23245
23658
  function booleanValue2(value) {
23246
23659
  return value === true;
23247
23660
  }
@@ -23251,6 +23664,9 @@ function enumValue(value, values) {
23251
23664
  function isRecord6(value) {
23252
23665
  return typeof value === "object" && value !== null && !Array.isArray(value);
23253
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
+ }
23254
23670
 
23255
23671
  // src/worker/moodle-upstream.ts
23256
23672
  var DASHBOARD_PATH2 = "/my/";