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 +74 -0
- package/build/index.js +5 -0
- package/build/smells.js +93 -49
- package/package.json +8 -3
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
|
-
|
|
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 =
|
|
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
|
|
119
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
|
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
|
+
"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
|
}
|