mcp-software-design 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 7f60e6c: `check_smells` no longer reports rows of multi-line object and array literals, or calls and throws whose arguments were all string literals, as duplicated logic. Lookup tables, message lines and state names used to account for a third of duplication findings on real code. A line that starts with a closing brace before a control keyword (`} else if (...) {`, `} catch (error) {`) is no longer mistaken for a method header, which produced phantom long-method and too-many-params findings measured from the middle of a function.
8
+
9
+ ## 0.1.4
10
+
11
+ ### Patch Changes
12
+
13
+ - b3fc1f0: Releases are now automated with Changesets and published from GitHub Actions with provenance.
14
+
15
+ All notable changes to this project are documented here.
16
+
17
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
18
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
19
+
20
+ ## [Unreleased]
21
+
22
+ ### Changed
23
+
24
+ - Extracted an escape-aware `scanToUnescaped` scanner in the smell
25
+ tokenizer, collapsing three near-identical scan loops into one linear,
26
+ no-backtracking helper — internal refactor, no behavior change. (#13)
27
+
28
+ ## [0.1.3] - 2026-07-31
29
+
30
+ ### Added
31
+
32
+ - `clean-code` umbrella concept in the catalog, tying the individual
33
+ cleanliness practices together. (#11)
34
+
35
+ ### Changed
36
+
37
+ - README: added badges and moved the install guide to the top, then
38
+ de-duplicated the install instructions so they live in one place.
39
+ (#9, #10)
40
+
41
+ ## [0.1.2] - 2026-07-31
42
+
43
+ ### Added
44
+
45
+ - `solid` and `oop` filters for the `list_catalog` tool. (#5)
46
+ - `meaningful-names` concept to the catalog, with refreshed docs. (#6)
47
+
48
+ ### Changed
49
+
50
+ - Expanded the npm keywords for discoverability. (#7)
51
+
52
+ ### Fixed
53
+
54
+ - Derive the server version from `package.json` so the reported version
55
+ always matches the published one. (#4)
56
+
57
+ ## [0.1.1] - 2026-07-31
58
+
59
+ ### Fixed
60
+
61
+ - Corrected the publish metadata so the package resolves cleanly on both
62
+ the npm and MCP registries. (#1)
63
+
64
+ ## [0.1.0] - 2026-07-31
65
+
66
+ ### Added
67
+
68
+ - Initial release: the software-design MCP server — a language-agnostic
69
+ catalog of SOLID / OOP / DRY principles and the 23 GoF design patterns,
70
+ the `list_catalog`, `explain_concept`, `scaffold_pattern`, and
71
+ `check_smells` tools, and the `design://` resources.
72
+
73
+ [Unreleased]: https://github.com/qwertymuzaffar/mcp-software-design/compare/v0.1.3...HEAD
74
+ [0.1.3]: https://github.com/qwertymuzaffar/mcp-software-design/releases/tag/v0.1.3
package/build/index.js CHANGED
@@ -31,6 +31,11 @@ braces, parentheses, and indentation, not a real parser).
31
31
  | large-file | > ${DEFAULTS.maxFileLines} lines | separation-of-concerns |
32
32
 
33
33
  All thresholds are overridable per call on the \`check_smells\` tool.
34
+
35
+ Duplication skips members of multi-line object and array literals (rows of a
36
+ lookup table are data) and calls or throws whose arguments were all string
37
+ literals (\`lines.push('...')\`, \`throw new Error('...')\`), so message and
38
+ state-name lines do not register.
34
39
  `;
35
40
  /** Render smells as a compact text report. */
36
41
  function renderSmells(smells) {
package/build/smells.js CHANGED
@@ -31,6 +31,26 @@ const TYPE_KEYWORDS = new Set([
31
31
  "class", "interface", "enum", "struct", "namespace", "module", "record",
32
32
  "trait", "object", "protocol", "extension",
33
33
  ]);
34
+ /**
35
+ * From `start`, scan `line` for the next unescaped `delimiter`, treating `\x`
36
+ * as an escape pair (so `\"` or `` \` `` don't close the literal). Returns the
37
+ * delimiter's index with `closed: true` when found, otherwise the line length
38
+ * with `closed: false`. Single left-to-right scan — no backtracking, linear.
39
+ */
40
+ function scanToUnescaped(line, start, delimiter) {
41
+ let scanIndex = start;
42
+ while (scanIndex < line.length) {
43
+ const char = line[scanIndex];
44
+ if (char === "\\") {
45
+ scanIndex += 2;
46
+ continue;
47
+ }
48
+ if (char === delimiter)
49
+ return { end: scanIndex, closed: true };
50
+ scanIndex++;
51
+ }
52
+ return { end: line.length, closed: false };
53
+ }
34
54
  /**
35
55
  * Blank out string literals, line comments, and block comments so structural
36
56
  * counters (braces, commas, numbers) don't trip over their contents. Returns
@@ -65,22 +85,9 @@ export function sanitize(lines) {
65
85
  continue;
66
86
  }
67
87
  if (mode === "template") {
68
- let scanIndex = cursor;
69
- let closed = false;
70
- while (scanIndex < lineLength) {
71
- const char = rawLine[scanIndex];
72
- if (char === "\\") {
73
- scanIndex += 2;
74
- continue;
75
- }
76
- if (char === "`") {
77
- closed = true;
78
- break;
79
- }
80
- scanIndex++;
81
- }
88
+ const { end, closed } = scanToUnescaped(rawLine, cursor, "`");
82
89
  if (closed) {
83
- cursor = scanIndex + 1;
90
+ cursor = end + 1;
84
91
  mode = "code";
85
92
  }
86
93
  else {
@@ -115,41 +122,14 @@ export function sanitize(lines) {
115
122
  continue;
116
123
  }
117
124
  if (char === '"' || char === "'") {
118
- const quote = char;
119
- let scanIndex = cursor + 1;
120
- let closed = false;
121
- while (scanIndex < lineLength) {
122
- const stringChar = rawLine[scanIndex];
123
- if (stringChar === "\\") {
124
- scanIndex += 2;
125
- continue;
126
- }
127
- if (stringChar === quote) {
128
- closed = true;
129
- break;
130
- }
131
- scanIndex++;
132
- }
133
- cursor = closed ? scanIndex + 1 : lineLength; // drop the string content
125
+ const { end, closed } = scanToUnescaped(rawLine, cursor + 1, char);
126
+ cursor = closed ? end + 1 : lineLength; // drop the string content
134
127
  continue;
135
128
  }
136
129
  if (char === "`") {
137
- let scanIndex = cursor + 1;
138
- let closed = false;
139
- while (scanIndex < lineLength) {
140
- const stringChar = rawLine[scanIndex];
141
- if (stringChar === "\\") {
142
- scanIndex += 2;
143
- continue;
144
- }
145
- if (stringChar === "`") {
146
- closed = true;
147
- break;
148
- }
149
- scanIndex++;
150
- }
130
+ const { end, closed } = scanToUnescaped(rawLine, cursor + 1, "`");
151
131
  if (closed) {
152
- cursor = scanIndex + 1;
132
+ cursor = end + 1;
153
133
  }
154
134
  else {
155
135
  mode = "template";
@@ -193,9 +173,14 @@ function leadingIdentifier(trimmedLine) {
193
173
  const match = trimmedLine.match(/^([A-Za-z_$][\w$]*)/);
194
174
  return match ? match[1] : "";
195
175
  }
196
- /** True if the (possibly multi-line-joined) text is a function/method header opening a `{`. */
176
+ /**
177
+ * True if the (possibly multi-line-joined) text is a function/method header
178
+ * opening a `{`. A closing brace that starts the line (`} else if (x) {`,
179
+ * `} catch (error) {`) is dropped first so the control keyword behind it is
180
+ * seen; otherwise such a line has no leading identifier and reads as a header.
181
+ */
197
182
  function isBraceMethodHeader(headerText) {
198
- const trimmed = headerText.trim();
183
+ const trimmed = headerText.trim().replace(/^\}\s*/, "");
199
184
  if (!trimmed.endsWith("{"))
200
185
  return false;
201
186
  if (!trimmed.includes("("))
@@ -466,17 +451,76 @@ function detectLargeClass(sanitized, options) {
466
451
  }
467
452
  return findings;
468
453
  }
454
+ /**
455
+ * Whether a `{` opens a block (function or control body, type declaration)
456
+ * rather than an object literal, judged from the sanitized text before it on
457
+ * its line. A header ends in `)` (optionally followed by a return type) or
458
+ * `=>`, a bare `else`/`try`/`do`/`finally`, or names a type; anything after
459
+ * `=`, `(`, `,`, `:`, `return` and the like is a literal. An empty prefix (a
460
+ * `{` opening the line) counts as a block.
461
+ */
462
+ function opensBlock(prefix) {
463
+ const trimmed = prefix.trim();
464
+ if (trimmed === "")
465
+ return true;
466
+ if (/(=>|\belse|\btry|\bdo|\bfinally)$/.test(trimmed))
467
+ return true;
468
+ // A header keeps its `(...)` and may add a return type after it
469
+ // (`foo(): Promise<T | null> {`, `fn f() -> T {`, `func f() error {`);
470
+ // a literal after a call sits behind `=`, `,`, `(`, `?`, `:` or `||`.
471
+ const lastParen = trimmed.lastIndexOf(")");
472
+ if (lastParen !== -1) {
473
+ const tail = trimmed.slice(lastParen + 1).trim();
474
+ if (tail === "")
475
+ return true;
476
+ if (tail === ":")
477
+ return false; // `ok ? f(a) : {`
478
+ if (/^(:|->)\s*\S/.test(tail))
479
+ return true; // typed return
480
+ return /^[\w$<>\[\]*. ]*$/.test(tail); // `throws X`, `error`, `*T`
481
+ }
482
+ if (/^(case|default)\b/.test(trimmed))
483
+ return true;
484
+ const leadingWord = leadingIdentifier(trimmed);
485
+ if (CONTROL_KEYWORDS.has(leadingWord) || TYPE_KEYWORDS.has(leadingWord))
486
+ return true;
487
+ return /\b(class|interface|enum|struct|trait|impl|namespace|module|match)\s+[\w$<>:, ]*$/.test(trimmed);
488
+ }
489
+ /**
490
+ * A call or throw whose arguments were all string literals (blanked by
491
+ * `sanitize`): `lines.push(, );`, `throw new RangeError();`, `this.status.set();`.
492
+ * Such lines are messages and state names, not repeated logic.
493
+ */
494
+ const TRIVIAL_CALL = /^(?:return\s+|await\s+|throw\s+new\s+)?[\w$.]+\s*\(\s*[,\s]*\)\s*;?$/;
469
495
  /** Detect duplicated non-trivial lines (a DRY proxy). */
470
496
  function detectDuplication(sanitized, options) {
471
497
  const lineCounts = new Map();
498
+ // Innermost open `{`/`[` per line, true when it opened an object/array
499
+ // literal: rows of a lookup table are data, not duplicated logic.
500
+ const bracketStack = [];
472
501
  for (let lineIndex = 0; lineIndex < sanitized.length; lineIndex++) {
473
- const trimmed = sanitized[lineIndex].trim();
502
+ const line = sanitized[lineIndex];
503
+ const insideLiteral = bracketStack.length > 0 && bracketStack[bracketStack.length - 1];
504
+ for (let charIndex = 0; charIndex < line.length; charIndex++) {
505
+ const char = line[charIndex];
506
+ if (char === "{")
507
+ bracketStack.push(!opensBlock(line.slice(0, charIndex)));
508
+ else if (char === "[")
509
+ bracketStack.push(true);
510
+ else if (char === "}" || char === "]")
511
+ bracketStack.pop();
512
+ }
513
+ if (insideLiteral)
514
+ continue;
515
+ const trimmed = line.trim();
474
516
  if (trimmed.length < 15)
475
517
  continue; // too short to be meaningful
476
518
  if (/^[{}()\[\];,]+$/.test(trimmed))
477
519
  continue; // pure punctuation
478
520
  if (/^(import|from|export|package|using|#include|@)/.test(trimmed))
479
521
  continue;
522
+ if (TRIVIAL_CALL.test(trimmed))
523
+ continue;
480
524
  const record = lineCounts.get(trimmed) ?? { count: 0, firstLine: lineIndex + 1 };
481
525
  record.count++;
482
526
  lineCounts.set(trimmed, record);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-software-design",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "MCP server that teaches and applies software-design guidance: SOLID/OOP/DRY principles, the 23 GoF design patterns, pattern scaffolding, and heuristic code-smell detection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,8 @@
33
33
  },
34
34
  "files": [
35
35
  "build",
36
- "README.md"
36
+ "README.md",
37
+ "CHANGELOG.md"
37
38
  ],
38
39
  "engines": {
39
40
  "node": ">=18"
@@ -44,13 +45,17 @@
44
45
  "pretest": "npm run build",
45
46
  "test": "node --test test/*.test.mjs",
46
47
  "prepublishOnly": "npm test",
47
- "test:client": "node test-client.mjs"
48
+ "test:client": "node test-client.mjs",
49
+ "changeset": "changeset",
50
+ "version-packages": "changeset version",
51
+ "release": "npm run build && changeset publish"
48
52
  },
49
53
  "dependencies": {
50
54
  "@modelcontextprotocol/sdk": "^1.30.0",
51
55
  "zod": "^3.25.76"
52
56
  },
53
57
  "devDependencies": {
58
+ "@changesets/cli": "^3.0.2",
54
59
  "@types/node": "^22.10.0",
55
60
  "typescript": "^5.7.2"
56
61
  }