@parasraju/plumber-mcp 1.0.0 → 1.0.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.
package/README.md CHANGED
@@ -107,3 +107,13 @@ Tool inputs are deliberately small so they cost few tokens:
107
107
 
108
108
  - CLI: [plumber-linter](https://www.npmjs.com/package/plumber-linter)
109
109
  - Repository: [github.com/parasraju/plumber](https://github.com/parasraju/plumber)
110
+ - Model Context Protocol: [modelcontextprotocol.io](https://modelcontextprotocol.io)
111
+ - MCP TypeScript SDK: [github.com/modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk)
112
+
113
+ ## Changelog
114
+
115
+ ## 1.0.1
116
+ - Fix false-positive test path matching that skipped secrets in files containing "test" or "spec" substrings (e.g. `latest.js`, `inspector.js`).
117
+ - Fix directory scan skipping target path when root directory name matches skip list or begins with a dot (e.g. `dist`, `.hidden`).
118
+ - Fix misleading 100/A score when target contains zero scannable files by reporting unverified status.
119
+ - Fix MCP initialize handshake reporting static version by reading dynamically from `package.json`.
@@ -21,9 +21,11 @@ const {
21
21
  ErrorCode,
22
22
  McpError
23
23
  } = require('@modelcontextprotocol/sdk/types.js');
24
- const { RULES, scanLocalDirectory, scanText } = require('../src/engine.js');
24
+ const { RULES, SCANNABLE_EXTS, scanLocalDirectory, scanText } = require('../src/engine.js');
25
25
 
26
- const SERVER_INFO = { name: 'plumber-mcp', version: '1.0.0' };
26
+ // v1.0.1 fix: read the version from package.json so serverInfo never drifts.
27
+ // Repro (v1.0.0): initialize -> serverInfo.version was hardcoded to "1.0.0".
28
+ const SERVER_INFO = { name: 'plumber-mcp', version: require('../package.json').version };
27
29
 
28
30
  const TOOLS = [
29
31
  {
@@ -64,10 +66,17 @@ const TOOLS = [
64
66
  function formatReport(report) {
65
67
  const header = [
66
68
  `Plumber Health Report for ${report.repo_name}`,
67
- `Score: ${report.score}/100 (Grade ${report.grade})`,
69
+ report.total_files === 0 ? 'Score: n/a' : `Score: ${report.score}/100 (Grade ${report.grade})`,
68
70
  `Files: ${report.total_files} | Issues: ${report.issues.length}`
69
71
  ].join('\n');
70
72
 
73
+ // v1.0.1 fix: do not report "No issues found." when nothing was scanned.
74
+ // Repro (v1.0.0): plumber_scan { target: ".github/workflows/ci.yml" } (or a
75
+ // Dockerfile, or an empty dir) -> "Score: 100/100 (Grade A) ... No issues found."
76
+ if (report.total_files === 0) {
77
+ return `${header}\n\nNo scannable files found, so nothing was checked. Supported: ${SCANNABLE_EXTS.join(' ')} and .env* files.`;
78
+ }
79
+
71
80
  if (report.issues.length === 0) {
72
81
  return `${header}\n\nNo issues found.`;
73
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parasraju/plumber-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Plumber as an MCP server — let your AI coding agent scan repos for leaked secrets, dead code, and AI slop, with fixes in plain English.",
5
5
  "main": "src/engine.js",
6
6
  "bin": {
package/src/engine.js CHANGED
@@ -53,8 +53,10 @@ function scoreFor(issueCount) {
53
53
  return Math.max(0, 100 - (issueCount * 4.2));
54
54
  }
55
55
 
56
+ const SCANNABLE_EXTS = ['.py', '.js', '.jsx', '.ts', '.tsx', '.html', '.htm', '.css', '.scss', '.env'];
57
+
56
58
  function scanLocalDirectory(dirPath) {
57
- const scannableExts = new Set(['.py', '.js', '.jsx', '.ts', '.tsx', '.html', '.htm', '.css', '.scss', '.env']);
59
+ const scannableExts = new Set(SCANNABLE_EXTS);
58
60
  const skipDirs = new Set(['.git', 'node_modules', 'dist', 'build', '.next', '.nuxt', 'vendor', '.venv', 'venv', '__pycache__']);
59
61
 
60
62
  const issues = [];
@@ -89,7 +91,11 @@ function scanLocalDirectory(dirPath) {
89
91
 
90
92
  if (stat.isDirectory()) {
91
93
  const base = path.basename(current);
92
- if (skipDirs.has(base) || (base.startsWith('.') && base !== '.')) return;
94
+ // v1.0.1 fix: the skip list applies to subdirectories only, never to the
95
+ // directory the user asked to scan.
96
+ // Repro (v1.0.0): plumber_scan { target: "proj/dist" } or { target: "~/.dotfiles" }
97
+ // with an AKIA key inside -> "Files: 0 | Issues: 0 ... No issues found."
98
+ if (current !== dirPath && (skipDirs.has(base) || base.startsWith('.'))) return;
93
99
  const entries = fs.readdirSync(current);
94
100
  for (const entry of entries) {
95
101
  walk(path.join(current, entry));
@@ -117,7 +123,7 @@ function scanText(text, filename) {
117
123
  if (typeof text !== 'string') {
118
124
  throw new TypeError('text must be a string');
119
125
  }
120
- const name = filename && typeof filename === 'string' ? path.basename(filename) : 'snippet.js';
126
+ const name = filename && typeof filename === 'string' ? filename.replace(/\\/g, '/') : 'snippet.js';
121
127
  const ext = path.extname(name).toLowerCase();
122
128
  const content = text.replace(/\r\n?/g, '\n');
123
129
  const lines = content.split('\n');
@@ -135,11 +141,23 @@ function scanText(text, filename) {
135
141
  };
136
142
  }
137
143
 
144
+ // v1.0.1 fix: match "test"/"spec" as a path segment or filename part, not as a
145
+ // substring. The old substring check treated latest.js, inspector.js, special.js,
146
+ // contest/, respect.py ... as test files and skipped secret detection in them.
147
+ // Repro (v1.0.0): plumber_scan_text { text: 'const k = "AKIAIOSFODNN7EXAMPLE";',
148
+ // filename: "src/latest.js" } -> "No issues found."
149
+ function isTestPath(relPath) {
150
+ const base = path.basename(relPath);
151
+ return /(?:^|[\/._-])(?:__tests__|tests?|specs?|testing)(?:[\/._-]|$)/i.test(relPath)
152
+ || /[a-z0-9](?:Tests?|Specs?)\.\w+$/.test(base)
153
+ || base.toLowerCase() === 'conftest.py';
154
+ }
155
+
138
156
  function inspectFile(relPath, content, lines, ext) {
139
157
  const issues = [];
140
158
  const lower = content.toLowerCase();
141
159
  const base = path.basename(relPath);
142
- const isTest = relPath.toLowerCase().includes('test') || relPath.toLowerCase().includes('spec');
160
+ const isTest = isTestPath(relPath);
143
161
 
144
162
  // Environment files must never be committed
145
163
  if (base.startsWith('.env') && !ENV_TEMPLATE_RE.test(base)) {
@@ -299,6 +317,7 @@ function findUnreachable(lines, start, indent, skip, isPy) {
299
317
  module.exports = {
300
318
  RULES,
301
319
  SEV_WEIGHT,
320
+ SCANNABLE_EXTS,
302
321
  scanLocalDirectory,
303
322
  scanText
304
323
  };