brightspace-mcp-server 3.3.0 → 3.4.1

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.
@@ -15,9 +15,75 @@ function getTurndownService() {
15
15
  headingStyle: "atx",
16
16
  codeBlockStyle: "fenced",
17
17
  });
18
+ // Turndown has no rule for <script>, <style> or <noscript>, so it falls
19
+ // back to emitting their text: a D2L description carrying an inline
20
+ // stylesheet or an analytics snippet put raw CSS and JavaScript source
21
+ // into the markdown the model reads. None of it is content.
22
+ turndownService.remove(["script", "style", "noscript"]);
23
+ // Turndown has no table rules either, and its fallback treats every cell
24
+ // as a standalone block -- a syllabus grading table came out as a flat
25
+ // run of paragraphs ("Homework", "30%", "Exams", "70%") with nothing
26
+ // left to say which weight belonged to which item. Emit GFM pipe tables
27
+ // so the pairing survives into the markdown a model is asked to read.
28
+ // Rules are registered once here (not at module scope) because the
29
+ // service itself is now lazily constructed on first use.
30
+ turndownService.addRule("tableCell", {
31
+ filter: ["th", "td"],
32
+ replacement: (content) => ` ${cellText(content)} |`,
33
+ });
34
+ turndownService.addRule("tableRow", {
35
+ filter: "tr",
36
+ replacement: (content, node) => {
37
+ const row = `|${content}`;
38
+ const table = closestTable(node);
39
+ const rows = table
40
+ ? Array.from(table.querySelectorAll("tr")).filter((candidate) => closestTable(candidate) === table)
41
+ : [];
42
+ // GFM needs a delimiter line after the first row, header row or not:
43
+ // without one the whole block renders as a paragraph and the
44
+ // columns are lost again.
45
+ if (rows.length > 0 && rows[0] === node) {
46
+ const columns = cellsOf(node).length || 1;
47
+ return `\n${row}\n|${" --- |".repeat(columns)}`;
48
+ }
49
+ return `\n${row}`;
50
+ },
51
+ });
52
+ turndownService.addRule("tableSection", {
53
+ filter: ["thead", "tbody", "tfoot"],
54
+ replacement: (content) => content,
55
+ });
56
+ turndownService.addRule("table", {
57
+ filter: "table",
58
+ replacement: (content) => {
59
+ const body = content.trim();
60
+ return body ? `\n\n${body}\n\n` : "";
61
+ },
62
+ });
18
63
  }
19
64
  return turndownService;
20
65
  }
66
+ /** A cell's text, flattened so one table cell stays one table cell. */
67
+ function cellText(content) {
68
+ return content
69
+ .replace(/\|/g, "\\|")
70
+ .replace(/\s*\n+\s*/g, " ")
71
+ .trim();
72
+ }
73
+ /** The nearest enclosing <table>, so a nested table is scoped to itself. */
74
+ function closestTable(node) {
75
+ let current = node.parentNode;
76
+ while (current) {
77
+ if (current.nodeName === "TABLE")
78
+ return current;
79
+ current = current.parentNode;
80
+ }
81
+ return null;
82
+ }
83
+ /** Direct <th>/<td> children of a row. */
84
+ function cellsOf(row) {
85
+ return Array.from(row.childNodes).filter((child) => child.nodeName === "TH" || child.nodeName === "TD");
86
+ }
21
87
  /**
22
88
  * Convert D2L HTML content to clean markdown.
23
89
  * Returns both markdown (for LLM readability) and raw HTML (for fallback).
@@ -19,16 +19,32 @@ export function setLogLevel(level) {
19
19
  *
20
20
  * Deliberately no blanket uppercase or base32 rule: it would eat course codes
21
21
  * like ECE264, which are exactly what a useful log line contains.
22
+ *
23
+ * Order matters: the JSON-shaped rules run before the free-text ones, so a
24
+ * serialized object is redacted by key and the looser rules never see the
25
+ * value they would otherwise re-match across the quoting.
22
26
  */
23
27
  function redact(value) {
24
28
  // JSON web tokens, wherever they appear
25
29
  value = value.replace(/eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]*)?/g, "eyJ...REDACTED");
26
30
  // Redact Bearer tokens
27
31
  value = value.replace(/Bearer\s+([A-Za-z0-9._~+/=-]{8})[A-Za-z0-9._~+/=-]*/g, "Bearer $1...REDACTED");
28
- // Redact cookie: prefixed tokens
29
- value = value.replace(/cookie:([^\s]{8})[^\s]*/g, "cookie:$1...REDACTED");
30
- // JSON-serialized header fields: {"Authorization":"..."}, {"Cookie":"..."}
32
+ // JSON-serialized header fields: {"Authorization":"..."}, {"Cookie":"..."}.
33
+ // Runs before the raw-header rule below so a serialized object is redacted
34
+ // as JSON and the raw-header rule cannot then chew through the quoting.
31
35
  value = value.replace(/("(?:authorization|cookie|set-cookie)"\s*:\s*")[^"]*(")/gi, "$1...REDACTED$2");
36
+ // JSON-serialized secret fields: {"password":"..."}, {"accessToken":"..."},
37
+ // {"refresh_token":"..."}, {"xsrfToken":"..."}. Only string values match, so
38
+ // a numeric field like "tokenExpiry": 1750000000 is left readable.
39
+ value = value.replace(/("[^"]*(?:password|passwd|secret|token|credential|api[_-]?key)[^"]*"\s*:\s*")[^"]*(")/gi, "$1...REDACTED$2");
40
+ // Raw header lines: "Cookie: d2lSessionVal=..." / "Set-Cookie: ...".
41
+ // The whole remainder of the line goes, because a cookie header carries
42
+ // every cookie for the host, not just the first pair. Case-insensitive and
43
+ // tolerant of the space HTTP actually puts after the colon -- without both,
44
+ // this rule only ever fired on a form no real header takes.
45
+ value = value.replace(/(^|[\s{[(,;])((?:set-)?cookie)\s*:[ \t]*([^\r\n]+)/gi, (_match, prefix, name, secret) => `${prefix}${name}: ${secret.slice(0, 8)}...REDACTED`);
46
+ // password=... / secret=... in a query string, form body, or free text.
47
+ value = value.replace(/\b(password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token)\b(\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s&;,"']+)/gi, (_match, key, separator) => `${key}${separator}...REDACTED`);
32
48
  // Credentials embedded in a URL: scheme://user:pass@host
33
49
  value = value.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi, "$1***:***@");
34
50
  // Redact anything that looks like a long token (40+ chars of base64-like)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brightspace-mcp-server",
3
- "version": "3.3.0",
3
+ "version": "3.4.1",
4
4
  "mcpName": "io.github.rohanmuppa/brightspace",
5
5
  "description": "MCP server for Brightspace (D2L). Check grades, due dates, assignments, announcements, syllabus, rosters and more via Claude, ChatGPT, Cursor, Windsurf, or any MCP client.",
6
6
  "type": "module",