lintbase-mcp 0.1.0
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/dist/core/analysis.utils.d.ts +13 -0
- package/dist/core/analysis.utils.d.ts.map +1 -0
- package/dist/core/analysis.utils.js +22 -0
- package/dist/core/analysis.utils.js.map +1 -0
- package/dist/core/cost.analyzer.d.ts +4 -0
- package/dist/core/cost.analyzer.d.ts.map +1 -0
- package/dist/core/cost.analyzer.js +60 -0
- package/dist/core/cost.analyzer.js.map +1 -0
- package/dist/core/performance.analyzer.d.ts +4 -0
- package/dist/core/performance.analyzer.d.ts.map +1 -0
- package/dist/core/performance.analyzer.js +39 -0
- package/dist/core/performance.analyzer.js.map +1 -0
- package/dist/core/schema-drift.analyzer.d.ts +4 -0
- package/dist/core/schema-drift.analyzer.d.ts.map +1 -0
- package/dist/core/schema-drift.analyzer.js +52 -0
- package/dist/core/schema-drift.analyzer.js.map +1 -0
- package/dist/core/security.analyzer.d.ts +4 -0
- package/dist/core/security.analyzer.d.ts.map +1 -0
- package/dist/core/security.analyzer.js +40 -0
- package/dist/core/security.analyzer.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +31 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/issues.tool.d.ts +3 -0
- package/dist/tools/issues.tool.d.ts.map +1 -0
- package/dist/tools/issues.tool.js +250 -0
- package/dist/tools/issues.tool.js.map +1 -0
- package/dist/tools/scan.tool.d.ts +3 -0
- package/dist/tools/scan.tool.d.ts.map +1 -0
- package/dist/tools/scan.tool.js +207 -0
- package/dist/tools/scan.tool.js.map +1 -0
- package/dist/tools/schema.tool.d.ts +23 -0
- package/dist/tools/schema.tool.d.ts.map +1 -0
- package/dist/tools/schema.tool.js +229 -0
- package/dist/tools/schema.tool.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LintBaseDocument, LintBaseScanResult } from '../types.js';
|
|
2
|
+
export interface CollectionStats {
|
|
3
|
+
docs: LintBaseDocument[];
|
|
4
|
+
count: number;
|
|
5
|
+
totalBytes: number;
|
|
6
|
+
avgBytes: number;
|
|
7
|
+
maxDepth: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function groupByCollection(result: LintBaseScanResult): Map<string, CollectionStats>;
|
|
10
|
+
export interface AnalysisOptions {
|
|
11
|
+
limit: number;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=analysis.utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis.utils.d.ts","sourceRoot":"","sources":["../../src/core/analysis.utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAiB1F;AAED,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.groupByCollection = groupByCollection;
|
|
4
|
+
function groupByCollection(result) {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
for (const col of result.collections) {
|
|
7
|
+
map.set(col, { docs: [], count: 0, totalBytes: 0, avgBytes: 0, maxDepth: 0 });
|
|
8
|
+
}
|
|
9
|
+
for (const doc of result.documents) {
|
|
10
|
+
const existing = map.get(doc.collection) ?? { docs: [], count: 0, totalBytes: 0, avgBytes: 0, maxDepth: 0 };
|
|
11
|
+
existing.docs.push(doc);
|
|
12
|
+
existing.count++;
|
|
13
|
+
existing.totalBytes += doc.sizeBytes;
|
|
14
|
+
existing.maxDepth = Math.max(existing.maxDepth, doc.depth);
|
|
15
|
+
map.set(doc.collection, existing);
|
|
16
|
+
}
|
|
17
|
+
for (const [col, stats] of map.entries()) {
|
|
18
|
+
map.set(col, { ...stats, avgBytes: stats.count > 0 ? Math.round(stats.totalBytes / stats.count) : 0 });
|
|
19
|
+
}
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=analysis.utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analysis.utils.js","sourceRoot":"","sources":["../../src/core/analysis.utils.ts"],"names":[],"mappings":";;AAWA,8CAiBC;AAjBD,SAAgB,iBAAiB,CAAC,MAA0B;IACxD,MAAM,GAAG,GAAG,IAAI,GAAG,EAA2B,CAAC;IAC/C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACnC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QAC5G,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,QAAQ,CAAC,KAAK,EAAE,CAAC;QACjB,QAAQ,CAAC,UAAU,IAAI,GAAG,CAAC,SAAS,CAAC;QACrC,QAAQ,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3D,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACtC,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QACvC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC3G,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost.analyzer.d.ts","sourceRoot":"","sources":["../../src/core/cost.analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAqB,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAMzE,wBAAgB,OAAO,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,eAAe,GAAG,aAAa,EAAE,CAgD7F"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyze = analyze;
|
|
4
|
+
const analysis_utils_js_1 = require("./analysis.utils.js");
|
|
5
|
+
const ERROR_AVG_BYTES = 50 * 1024;
|
|
6
|
+
const WARN_AVG_BYTES = 5 * 1024;
|
|
7
|
+
const LOG_SINK_PATTERNS = [/^console/i, /^log[s]?$/i, /^audit/i, /^event[s]?$/i, /^request(get|post|put|delete|patch)$/i, /payload/i, /^trace[s]?$/i, /^webhook/i, /^analytics/i];
|
|
8
|
+
function analyze(result, options) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
const byCollection = (0, analysis_utils_js_1.groupByCollection)(result);
|
|
11
|
+
for (const [col, stats] of byCollection.entries()) {
|
|
12
|
+
if (stats.count === 0)
|
|
13
|
+
continue;
|
|
14
|
+
if (stats.avgBytes > ERROR_AVG_BYTES) {
|
|
15
|
+
issues.push({ severity: 'error', collection: col, rule: 'cost/large-avg-document', message: `"${col}" has an average document size of ${(stats.avgBytes / 1024).toFixed(1)} KB. At scale this will significantly increase read bandwidth costs.`, suggestion: 'Move large blob fields to Cloud Storage or a separate sub-collection.' });
|
|
16
|
+
}
|
|
17
|
+
else if (stats.avgBytes > WARN_AVG_BYTES) {
|
|
18
|
+
issues.push({ severity: 'warning', collection: col, rule: 'cost/large-avg-document', message: `"${col}" average document size is ${(stats.avgBytes / 1024).toFixed(1)} KB.`, suggestion: 'Consider splitting large documents or using sub-collections for frequently-updated sub-objects.' });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const [col] of byCollection.entries()) {
|
|
22
|
+
if (LOG_SINK_PATTERNS.some((re) => re.test(col))) {
|
|
23
|
+
issues.push({ severity: 'error', collection: col, rule: 'cost/logging-sink', message: `"${col}" appears to be used as a logging or event sink. Firestore charges per document write — unbounded logging will compound costs.`, suggestion: 'Use Cloud Logging, BigQuery, or a dedicated logging service instead.' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const colList = [...byCollection.entries()].filter(([, s]) => s.count > 0);
|
|
27
|
+
const redundancyGroups = [];
|
|
28
|
+
const visited = new Set();
|
|
29
|
+
for (let i = 0; i < colList.length; i++) {
|
|
30
|
+
const [colA, statsA] = colList[i];
|
|
31
|
+
if (visited.has(colA))
|
|
32
|
+
continue;
|
|
33
|
+
const group = [colA];
|
|
34
|
+
for (let j = i + 1; j < colList.length; j++) {
|
|
35
|
+
const [colB, statsB] = colList[j];
|
|
36
|
+
if (visited.has(colB))
|
|
37
|
+
continue;
|
|
38
|
+
const byteRatio = statsA.avgBytes > 0 ? Math.abs(statsA.avgBytes - statsB.avgBytes) / statsA.avgBytes : 1;
|
|
39
|
+
if (byteRatio <= 0.15 && statsA.maxDepth === statsB.maxDepth && statsA.avgBytes > 0) {
|
|
40
|
+
group.push(colB);
|
|
41
|
+
visited.add(colB);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (group.length > 1) {
|
|
45
|
+
redundancyGroups.push(group);
|
|
46
|
+
for (const c of group)
|
|
47
|
+
visited.add(c);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const group of redundancyGroups) {
|
|
51
|
+
issues.push({ severity: 'warning', collection: group[0], rule: 'cost/redundant-collections', message: `Collections [${group.map((c) => `"${c}"`).join(', ')}] have identical average document sizes and nesting depth — they likely store the same schema.`, suggestion: 'Merge redundant collections into one, using a "type" field to differentiate records.' });
|
|
52
|
+
}
|
|
53
|
+
for (const [col, stats] of byCollection.entries()) {
|
|
54
|
+
if (stats.count === options.limit) {
|
|
55
|
+
issues.push({ severity: 'info', collection: col, rule: 'cost/collection-at-limit', message: `"${col}" hit the ${options.limit}-document sampling cap — actual size is unknown.`, suggestion: `If this collection grows unbounded, set up a Cloud Function or cron job to archive or delete old documents.` });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return issues;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=cost.analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost.analyzer.js","sourceRoot":"","sources":["../../src/core/cost.analyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { LintBaseIssue, LintBaseScanResult } from '../types.js';
|
|
2
|
+
import { AnalysisOptions } from './analysis.utils.js';
|
|
3
|
+
export declare function analyze(result: LintBaseScanResult, options: AnalysisOptions): LintBaseIssue[];
|
|
4
|
+
//# sourceMappingURL=performance.analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance.analyzer.d.ts","sourceRoot":"","sources":["../../src/core/performance.analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAqB,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAQzE,wBAAgB,OAAO,CAAC,MAAM,EAAE,kBAAkB,EAAE,OAAO,EAAE,eAAe,GAAG,aAAa,EAAE,CA8B7F"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyze = analyze;
|
|
4
|
+
const analysis_utils_js_1 = require("./analysis.utils.js");
|
|
5
|
+
const MAX_RECOMMENDED_DEPTH = 5;
|
|
6
|
+
const WARN_DEPTH = 3;
|
|
7
|
+
const ERROR_SIZE_BYTES = 500 * 1024;
|
|
8
|
+
const WARN_SIZE_BYTES = 50 * 1024;
|
|
9
|
+
const WARN_AVG_BYTES = 100 * 1024;
|
|
10
|
+
function analyze(result, options) {
|
|
11
|
+
const issues = [];
|
|
12
|
+
const byCollection = (0, analysis_utils_js_1.groupByCollection)(result);
|
|
13
|
+
for (const [col, stats] of byCollection.entries()) {
|
|
14
|
+
if (stats.count === 0)
|
|
15
|
+
continue;
|
|
16
|
+
if (stats.maxDepth > MAX_RECOMMENDED_DEPTH) {
|
|
17
|
+
issues.push({ severity: 'error', collection: col, rule: 'perf/excessive-nesting', message: `"${col}" contains documents nested ${stats.maxDepth} levels deep (recommended max: ${MAX_RECOMMENDED_DEPTH}).`, affectedDocuments: stats.docs.filter((d) => d.depth > MAX_RECOMMENDED_DEPTH).map((d) => d.id).slice(0, 5), suggestion: 'Flatten deeply nested objects into separate sub-collections or top-level fields.' });
|
|
18
|
+
}
|
|
19
|
+
else if (stats.maxDepth > WARN_DEPTH) {
|
|
20
|
+
issues.push({ severity: 'warning', collection: col, rule: 'perf/excessive-nesting', message: `"${col}" documents reach a nesting depth of ${stats.maxDepth}. Consider keeping nesting ≤ ${WARN_DEPTH} for optimal query performance.`, affectedDocuments: stats.docs.filter((d) => d.depth > WARN_DEPTH).map((d) => d.id).slice(0, 5), suggestion: 'Deep nesting makes composite indexes necessary and increases document read cost.' });
|
|
21
|
+
}
|
|
22
|
+
const oversizedDocs = stats.docs.filter((d) => d.sizeBytes > ERROR_SIZE_BYTES);
|
|
23
|
+
const largeDocs = stats.docs.filter((d) => d.sizeBytes > WARN_SIZE_BYTES && d.sizeBytes <= ERROR_SIZE_BYTES);
|
|
24
|
+
if (oversizedDocs.length > 0) {
|
|
25
|
+
issues.push({ severity: 'error', collection: col, rule: 'perf/document-too-large', message: `${oversizedDocs.length} document(s) in "${col}" exceed 500 KB. Largest: ${Math.round(oversizedDocs[0].sizeBytes / 1024)} KB.`, affectedDocuments: oversizedDocs.map((d) => d.id).slice(0, 5), suggestion: 'Split large documents or move blob data to Cloud Storage.' });
|
|
26
|
+
}
|
|
27
|
+
if (largeDocs.length > 0) {
|
|
28
|
+
issues.push({ severity: 'warning', collection: col, rule: 'perf/document-too-large', message: `${largeDocs.length} document(s) in "${col}" are between 50 KB and 500 KB.`, affectedDocuments: largeDocs.map((d) => d.id).slice(0, 5), suggestion: 'Large documents increase read latency and bandwidth cost.' });
|
|
29
|
+
}
|
|
30
|
+
if (stats.avgBytes > WARN_AVG_BYTES) {
|
|
31
|
+
issues.push({ severity: 'warning', collection: col, rule: 'perf/avg-document-large', message: `"${col}" has an average document size of ${Math.round(stats.avgBytes / 1024)} KB.`, suggestion: 'Consider pagination, partial reads, or moving large fields to a sub-collection.' });
|
|
32
|
+
}
|
|
33
|
+
if (stats.count === options.limit) {
|
|
34
|
+
issues.push({ severity: 'info', collection: col, rule: 'perf/sampling-limit-reached', message: `"${col}" returned exactly ${options.limit} documents — the sampling limit. This collection likely has more data.`, suggestion: `Run with a higher sampleSize for a more complete picture of "${col}".` });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return issues;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=performance.analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance.analyzer.js","sourceRoot":"","sources":["../../src/core/performance.analyzer.ts"],"names":[],"mappings":";;AAUA,0BA8BC;AAtCD,2DAAyE;AAEzE,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAChC,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,MAAM,gBAAgB,GAAG,GAAG,GAAG,IAAI,CAAC;AACpC,MAAM,eAAe,GAAG,EAAE,GAAG,IAAI,CAAC;AAClC,MAAM,cAAc,GAAG,GAAG,GAAG,IAAI,CAAC;AAElC,SAAgB,OAAO,CAAC,MAA0B,EAAE,OAAwB;IACxE,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,MAAM,YAAY,GAAG,IAAA,qCAAiB,EAAC,MAAM,CAAC,CAAC;IAE/C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;QAChD,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;YAAE,SAAS;QAEhC,IAAI,KAAK,CAAC,QAAQ,GAAG,qBAAqB,EAAE,CAAC;YACzC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,IAAI,GAAG,+BAA+B,KAAK,CAAC,QAAQ,kCAAkC,qBAAqB,IAAI,EAAE,iBAAiB,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,qBAAqB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,kFAAkF,EAAE,CAAC,CAAC;QAC7Z,CAAC;aAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,UAAU,EAAE,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,IAAI,GAAG,wCAAwC,KAAK,CAAC,QAAQ,gCAAgC,UAAU,iCAAiC,EAAE,iBAAiB,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,kFAAkF,EAAE,CAAC,CAAC;QAC7a,CAAC;QAED,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,gBAAgB,CAAC,CAAC;QAC/E,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,eAAe,IAAI,CAAC,CAAC,SAAS,IAAI,gBAAgB,CAAC,CAAC;QAE7G,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,GAAG,aAAa,CAAC,MAAM,oBAAoB,GAAG,6BAA6B,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAE,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,iBAAiB,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,2DAA2D,EAAE,CAAC,CAAC;QAC3W,CAAC;QACD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC,MAAM,oBAAoB,GAAG,iCAAiC,EAAE,iBAAiB,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,2DAA2D,EAAE,CAAC,CAAC;QACrT,CAAC;QACD,IAAI,KAAK,CAAC,QAAQ,GAAG,cAAc,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,IAAI,GAAG,qCAAqC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,iFAAiF,EAAE,CAAC,CAAC;QACxR,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC;YAChC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,6BAA6B,EAAE,OAAO,EAAE,IAAI,GAAG,sBAAsB,OAAO,CAAC,KAAK,wEAAwE,EAAE,UAAU,EAAE,gEAAgE,GAAG,IAAI,EAAE,CAAC,CAAC;QAC9S,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { LintBaseIssue, LintBaseScanResult } from '../types.js';
|
|
2
|
+
import { AnalysisOptions } from './analysis.utils.js';
|
|
3
|
+
export declare function analyze(result: LintBaseScanResult, _options: AnalysisOptions): LintBaseIssue[];
|
|
4
|
+
//# sourceMappingURL=schema-drift.analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-drift.analyzer.d.ts","sourceRoot":"","sources":["../../src/core/schema-drift.analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAqB,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEzE,wBAAgB,OAAO,CAAC,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,eAAe,GAAG,aAAa,EAAE,CAkD9F"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyze = analyze;
|
|
4
|
+
const analysis_utils_js_1 = require("./analysis.utils.js");
|
|
5
|
+
function analyze(result, _options) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const byCollection = (0, analysis_utils_js_1.groupByCollection)(result);
|
|
8
|
+
for (const [col, stats] of byCollection.entries()) {
|
|
9
|
+
if (stats.count === 0) {
|
|
10
|
+
issues.push({ severity: 'info', collection: col, rule: 'schema/empty-collection', message: `No documents were sampled from "${col}" — schema cannot be analysed.`, suggestion: 'Ensure the collection contains data or check Firestore security rules.' });
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const fieldTypes = new Map();
|
|
14
|
+
const fieldCounts = new Map();
|
|
15
|
+
const fieldCountsPerDoc = [];
|
|
16
|
+
for (const doc of stats.docs) {
|
|
17
|
+
let docFieldCount = 0;
|
|
18
|
+
for (const [field, { type }] of Object.entries(doc.fields)) {
|
|
19
|
+
docFieldCount++;
|
|
20
|
+
if (!fieldTypes.has(field))
|
|
21
|
+
fieldTypes.set(field, new Set());
|
|
22
|
+
fieldTypes.get(field).add(type);
|
|
23
|
+
fieldCounts.set(field, (fieldCounts.get(field) ?? 0) + 1);
|
|
24
|
+
}
|
|
25
|
+
fieldCountsPerDoc.push(docFieldCount);
|
|
26
|
+
}
|
|
27
|
+
for (const [field, types] of fieldTypes.entries()) {
|
|
28
|
+
if (types.size > 1) {
|
|
29
|
+
issues.push({ severity: 'error', collection: col, rule: 'schema/field-type-mismatch', message: `Field "${field}" in "${col}" has ${types.size} different types: [${[...types].join(', ')}].`, suggestion: 'Normalise this field to a single type. Schema drift makes queries unreliable and is hard to fix at scale.' });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const presence60 = stats.count * 0.6;
|
|
33
|
+
const presence80 = stats.count * 0.8;
|
|
34
|
+
for (const [field, count] of fieldCounts.entries()) {
|
|
35
|
+
if (count < presence60) {
|
|
36
|
+
issues.push({ severity: 'warning', collection: col, rule: 'schema/sparse-field', message: `Field "${field}" in "${col}" is present in only ${count}/${stats.count} documents (${Math.round((count / stats.count) * 100)}%).`, suggestion: 'Consider adding a default value or marking it as optional in your application model to prevent runtime null errors.' });
|
|
37
|
+
}
|
|
38
|
+
else if (count < presence80) {
|
|
39
|
+
issues.push({ severity: 'info', collection: col, rule: 'schema/sparse-field', message: `Field "${field}" in "${col}" is present in ${count}/${stats.count} documents (${Math.round((count / stats.count) * 100)}%).`, suggestion: 'Track optional fields explicitly in your data model to avoid unexpected undefined reads.' });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (fieldCountsPerDoc.length > 1) {
|
|
43
|
+
const min = Math.min(...fieldCountsPerDoc);
|
|
44
|
+
const max = Math.max(...fieldCountsPerDoc);
|
|
45
|
+
if (max > 0 && max - min > Math.max(3, min * 0.5)) {
|
|
46
|
+
issues.push({ severity: 'warning', collection: col, rule: 'schema/high-field-variance', message: `Documents in "${col}" have between ${min} and ${max} fields — high structural variance.`, suggestion: 'High field variance is a sign of schema drift over time. Consider a migration or schema validation layer.' });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return issues;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=schema-drift.analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-drift.analyzer.js","sourceRoot":"","sources":["../../src/core/schema-drift.analyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { LintBaseIssue, LintBaseScanResult } from '../types.js';
|
|
2
|
+
import { AnalysisOptions } from './analysis.utils.js';
|
|
3
|
+
export declare function analyze(result: LintBaseScanResult, _options: AnalysisOptions): LintBaseIssue[];
|
|
4
|
+
//# sourceMappingURL=security.analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.analyzer.d.ts","sourceRoot":"","sources":["../../src/core/security.analyzer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAqB,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAOzE,wBAAgB,OAAO,CAAC,MAAM,EAAE,kBAAkB,EAAE,QAAQ,EAAE,eAAe,GAAG,aAAa,EAAE,CAgC9F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyze = analyze;
|
|
4
|
+
const analysis_utils_js_1 = require("./analysis.utils.js");
|
|
5
|
+
const SENSITIVE_PATTERNS = [/^bank/i, /credit/i, /payment/i, /invoice/i, /billing/i, /ssn/i, /passport/i, /secret/i, /password/i, /private/i, /token/i, /api.?key/i, /credential/i, /account.?info/i];
|
|
6
|
+
const DEBUG_PATTERNS = [/^console/i, /^log[s]?$/i, /^debug/i, /^test/i, /^temp/i, /^request(get|post|put|delete|patch)$/i, /payload/i, /webhook\s*log/i, /^dev/i];
|
|
7
|
+
const SENSITIVE_FIELD_PATTERNS = [/\bpassword\b/i, /\bpasswd\b/i, /\bsecret\b/i, /\bapi[_-]?key\b/i, /\bprivate[_-]?key\b/i, /\btoken\b/i, /\bssn\b/i, /\bcredit[_-]?card\b/i, /\bcvv\b/i, /\b(card[_-]?)?pin\b/i];
|
|
8
|
+
const AUTH_COLLECTION_NAMES = /^(users?|accounts?|membres?|members?)$/i;
|
|
9
|
+
function analyze(result, _options) {
|
|
10
|
+
const issues = [];
|
|
11
|
+
const byCollection = (0, analysis_utils_js_1.groupByCollection)(result);
|
|
12
|
+
for (const [col, stats] of byCollection.entries()) {
|
|
13
|
+
const matchedSensitive = SENSITIVE_PATTERNS.find((re) => re.test(col));
|
|
14
|
+
if (matchedSensitive) {
|
|
15
|
+
issues.push({ severity: 'error', collection: col, rule: 'security/sensitive-collection', message: `Collection "${col}" appears to store sensitive data (matched pattern: ${matchedSensitive}).`, suggestion: 'Verify that Firestore Security Rules restrict read access to authenticated users only.' });
|
|
16
|
+
}
|
|
17
|
+
const matchedDebug = DEBUG_PATTERNS.find((re) => re.test(col));
|
|
18
|
+
if (matchedDebug) {
|
|
19
|
+
issues.push({ severity: 'error', collection: col, rule: 'security/debug-data-in-production', message: `Collection "${col}" looks like debug or test data left in production.`, suggestion: 'Delete or archive this collection. Debug data in production is a security and cost risk.' });
|
|
20
|
+
}
|
|
21
|
+
if (AUTH_COLLECTION_NAMES.test(col) && stats.count < 3 && stats.avgBytes < 50) {
|
|
22
|
+
issues.push({ severity: 'warning', collection: col, rule: 'security/stub-auth-collection', message: `"${col}" looks like an auth/user collection but contains very few, tiny documents (${stats.count} docs, avg ${stats.avgBytes} bytes).`, suggestion: 'Confirm whether user data is stored here or in Firebase Auth. Orphaned collections should be removed.' });
|
|
23
|
+
}
|
|
24
|
+
if (stats.count === 0)
|
|
25
|
+
continue;
|
|
26
|
+
const allFieldNames = new Set();
|
|
27
|
+
for (const doc of stats.docs) {
|
|
28
|
+
for (const field of Object.keys(doc.fields))
|
|
29
|
+
allFieldNames.add(field);
|
|
30
|
+
}
|
|
31
|
+
for (const field of allFieldNames) {
|
|
32
|
+
const matchedField = SENSITIVE_FIELD_PATTERNS.find((re) => re.test(field));
|
|
33
|
+
if (matchedField) {
|
|
34
|
+
issues.push({ severity: 'error', collection: col, rule: 'security/field-contains-secret', message: `Field "${field}" in "${col}" has a name that suggests it stores a secret or PII value.`, suggestion: 'Never store raw passwords, tokens, or PII in Firestore. Hash passwords, use Firebase Auth for credentials.' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return issues;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=security.analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.analyzer.js","sourceRoot":"","sources":["../../src/core/security.analyzer.ts"],"names":[],"mappings":";;AASA,0BAgCC;AAvCD,2DAAyE;AAEzE,MAAM,kBAAkB,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAAC;AACtM,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,uCAAuC,EAAE,UAAU,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;AAClK,MAAM,wBAAwB,GAAG,CAAC,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,YAAY,EAAE,UAAU,EAAE,sBAAsB,EAAE,UAAU,EAAE,sBAAsB,CAAC,CAAC;AACnN,MAAM,qBAAqB,GAAG,yCAAyC,CAAC;AAExE,SAAgB,OAAO,CAAC,MAA0B,EAAE,QAAyB;IACzE,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,MAAM,YAAY,GAAG,IAAA,qCAAiB,EAAC,MAAM,CAAC,CAAC;IAE/C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;QAChD,MAAM,gBAAgB,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACvE,IAAI,gBAAgB,EAAE,CAAC;YACnB,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,+BAA+B,EAAE,OAAO,EAAE,eAAe,GAAG,uDAAuD,gBAAgB,IAAI,EAAE,UAAU,EAAE,wFAAwF,EAAE,CAAC,CAAC;QAC7S,CAAC;QAED,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/D,IAAI,YAAY,EAAE,CAAC;YACf,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,mCAAmC,EAAE,OAAO,EAAE,eAAe,GAAG,qDAAqD,EAAE,UAAU,EAAE,0FAA0F,EAAE,CAAC,CAAC;QAC7R,CAAC;QAED,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,GAAG,EAAE,EAAE,CAAC;YAC5E,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,+BAA+B,EAAE,OAAO,EAAE,IAAI,GAAG,+EAA+E,KAAK,CAAC,KAAK,cAAc,KAAK,CAAC,QAAQ,UAAU,EAAE,UAAU,EAAE,uGAAuG,EAAE,CAAC,CAAC;QACxW,CAAC;QAED,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;YAAE,SAAS;QAChC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;QACxC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1E,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;YAChC,MAAM,YAAY,GAAG,wBAAwB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YAC3E,IAAI,YAAY,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,gCAAgC,EAAE,OAAO,EAAE,UAAU,KAAK,SAAS,GAAG,6DAA6D,EAAE,UAAU,EAAE,4GAA4G,EAAE,CAAC,CAAC;YAC7T,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// packages/lintbase-mcp/src/server.ts
|
|
4
|
+
// Entry point for the LintBase MCP server.
|
|
5
|
+
// Run via `npx lintbase-mcp` — the IDE spawns this as a stdio process.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
const scan_tool_js_1 = require("./tools/scan.tool.js");
|
|
10
|
+
const schema_tool_js_1 = require("./tools/schema.tool.js");
|
|
11
|
+
const issues_tool_js_1 = require("./tools/issues.tool.js");
|
|
12
|
+
const server = new mcp_js_1.McpServer({
|
|
13
|
+
name: 'lintbase-mcp',
|
|
14
|
+
version: '0.1.0',
|
|
15
|
+
});
|
|
16
|
+
// ── Register all 3 tools ──────────────────────────────────────────────────────
|
|
17
|
+
(0, scan_tool_js_1.registerScanTool)(server); // Full scan → complete report
|
|
18
|
+
(0, schema_tool_js_1.registerSchemaTool)(server); // Schema introspection → field names + types
|
|
19
|
+
(0, issues_tool_js_1.registerIssuesTool)(server); // Filtered issues → targeted queries
|
|
20
|
+
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
21
|
+
async function main() {
|
|
22
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
// Log to stderr so it doesn't pollute the MCP stdio protocol on stdout
|
|
25
|
+
console.error('LintBase MCP server v0.1.0 running on stdio');
|
|
26
|
+
}
|
|
27
|
+
main().catch((err) => {
|
|
28
|
+
console.error('Fatal error starting LintBase MCP server:', err);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";;AACA,sCAAsC;AACtC,2CAA2C;AAC3C,uEAAuE;;AAEvE,oEAAoE;AACpE,wEAAiF;AAEjF,uDAAwD;AACxD,2DAA4D;AAC5D,2DAA4D;AAE5D,MAAM,MAAM,GAAG,IAAI,kBAAS,CAAC;IACzB,IAAI,EAAE,cAAc;IACpB,OAAO,EAAE,OAAO;CACnB,CAAC,CAAC;AAEH,iFAAiF;AACjF,IAAA,+BAAgB,EAAC,MAAM,CAAC,CAAC,CAAK,8BAA8B;AAC5D,IAAA,mCAAkB,EAAC,MAAM,CAAC,CAAC,CAAG,6CAA6C;AAC3E,IAAA,mCAAkB,EAAC,MAAM,CAAC,CAAC,CAAG,qCAAqC;AAInE,iFAAiF;AACjF,KAAK,UAAU,IAAI;IACf,MAAM,SAAS,GAAG,IAAI,+BAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,uEAAuE;IACvE,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;AACjE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,GAAG,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issues.tool.d.ts","sourceRoot":"","sources":["../../src/tools/issues.tool.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoKpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAyE1D"}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// packages/lintbase-mcp/src/tools/issues.tool.ts
|
|
3
|
+
//
|
|
4
|
+
// MCP Tool: lintbase_get_issues
|
|
5
|
+
//
|
|
6
|
+
// Runs all LintBase analyzers and returns only the filtered issues list —
|
|
7
|
+
// no summaries, no scan metadata, just the actionable problems.
|
|
8
|
+
//
|
|
9
|
+
// Designed for targeted AI queries like:
|
|
10
|
+
// "Any errors in the users collection before I add a field?"
|
|
11
|
+
// "Show me all security issues"
|
|
12
|
+
// "Are there any schema drift problems?"
|
|
13
|
+
//
|
|
14
|
+
// Filters:
|
|
15
|
+
// - severity: 'error' | 'warning' | 'info' (omit = all)
|
|
16
|
+
// - collection: string (omit = all collections)
|
|
17
|
+
// - rule: string prefix, e.g. "schema/" (omit = all rules)
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.registerIssuesTool = registerIssuesTool;
|
|
53
|
+
const zod_1 = require("zod");
|
|
54
|
+
const admin = __importStar(require("firebase-admin"));
|
|
55
|
+
const fs = __importStar(require("fs"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const SchemaDrift = __importStar(require("../core/schema-drift.analyzer.js"));
|
|
58
|
+
const Performance = __importStar(require("../core/performance.analyzer.js"));
|
|
59
|
+
const Security = __importStar(require("../core/security.analyzer.js"));
|
|
60
|
+
const Cost = __importStar(require("../core/cost.analyzer.js"));
|
|
61
|
+
// ── Firestore helpers ─────────────────────────────────────────────────────────
|
|
62
|
+
function inferType(value) {
|
|
63
|
+
if (value === null)
|
|
64
|
+
return 'null';
|
|
65
|
+
if (value === undefined)
|
|
66
|
+
return 'undefined';
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
return 'array';
|
|
69
|
+
if (typeof value === 'object' && value !== null &&
|
|
70
|
+
'toDate' in value && typeof value.toDate === 'function')
|
|
71
|
+
return 'timestamp';
|
|
72
|
+
if (typeof value === 'object' && value !== null &&
|
|
73
|
+
'path' in value && 'id' in value)
|
|
74
|
+
return 'reference';
|
|
75
|
+
if (typeof value === 'object')
|
|
76
|
+
return 'map';
|
|
77
|
+
return typeof value;
|
|
78
|
+
}
|
|
79
|
+
function calculateDepth(obj, current = 1) {
|
|
80
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj))
|
|
81
|
+
return current;
|
|
82
|
+
const values = Object.values(obj);
|
|
83
|
+
if (values.length === 0)
|
|
84
|
+
return current;
|
|
85
|
+
return Math.max(...values.map((v) => calculateDepth(v, current + 1)));
|
|
86
|
+
}
|
|
87
|
+
function mapDocument(doc, collection) {
|
|
88
|
+
const rawData = doc.data();
|
|
89
|
+
const fields = {};
|
|
90
|
+
for (const [key, value] of Object.entries(rawData)) {
|
|
91
|
+
fields[key] = { value, type: inferType(value) };
|
|
92
|
+
}
|
|
93
|
+
const depth = calculateDepth(rawData);
|
|
94
|
+
const sizeBytes = (() => {
|
|
95
|
+
try {
|
|
96
|
+
return Buffer.byteLength(JSON.stringify(rawData), 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
return { id: doc.id, collection, fields, depth, sizeBytes };
|
|
103
|
+
}
|
|
104
|
+
// ── Core function ─────────────────────────────────────────────────────────────
|
|
105
|
+
async function getIssues(keyPath, sampleSize, filters) {
|
|
106
|
+
const resolvedKey = path.resolve(keyPath);
|
|
107
|
+
if (!fs.existsSync(resolvedKey)) {
|
|
108
|
+
throw new Error(`Service account file not found: ${resolvedKey}`);
|
|
109
|
+
}
|
|
110
|
+
const serviceAccount = JSON.parse(fs.readFileSync(resolvedKey, 'utf-8'));
|
|
111
|
+
if (!admin.apps.length) {
|
|
112
|
+
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
|
|
113
|
+
}
|
|
114
|
+
const db = admin.firestore();
|
|
115
|
+
const allCollections = (await db.listCollections()).map((c) => c.id);
|
|
116
|
+
// Apply collection filter
|
|
117
|
+
let targetCollections = allCollections;
|
|
118
|
+
if (filters.collection) {
|
|
119
|
+
targetCollections = allCollections.filter((c) => c === filters.collection);
|
|
120
|
+
if (targetCollections.length === 0) {
|
|
121
|
+
throw new Error(`Collection "${filters.collection}" not found. Available: [${allCollections.join(', ')}]`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (filters.collections && filters.collections.length > 0) {
|
|
125
|
+
const filterSet = new Set(filters.collections);
|
|
126
|
+
targetCollections = allCollections.filter((c) => filterSet.has(c));
|
|
127
|
+
}
|
|
128
|
+
// Sample documents
|
|
129
|
+
const allDocuments = [];
|
|
130
|
+
for (const col of targetCollections) {
|
|
131
|
+
const snapshot = await db.collection(col).limit(sampleSize).get();
|
|
132
|
+
allDocuments.push(...snapshot.docs.map((doc) => mapDocument(doc, col)));
|
|
133
|
+
}
|
|
134
|
+
const scanResult = {
|
|
135
|
+
connector: 'firestore',
|
|
136
|
+
collections: targetCollections,
|
|
137
|
+
documentCount: allDocuments.length,
|
|
138
|
+
documents: allDocuments,
|
|
139
|
+
scannedAt: new Date(),
|
|
140
|
+
};
|
|
141
|
+
const analysisOptions = { limit: sampleSize };
|
|
142
|
+
const [schemaDriftIssues, perfIssues, securityIssues, costIssues] = await Promise.all([
|
|
143
|
+
Promise.resolve(SchemaDrift.analyze(scanResult, analysisOptions)),
|
|
144
|
+
Promise.resolve(Performance.analyze(scanResult, analysisOptions)),
|
|
145
|
+
Promise.resolve(Security.analyze(scanResult, analysisOptions)),
|
|
146
|
+
Promise.resolve(Cost.analyze(scanResult, analysisOptions)),
|
|
147
|
+
]);
|
|
148
|
+
let issues = [
|
|
149
|
+
...schemaDriftIssues,
|
|
150
|
+
...perfIssues,
|
|
151
|
+
...securityIssues,
|
|
152
|
+
...costIssues,
|
|
153
|
+
];
|
|
154
|
+
// Apply filters
|
|
155
|
+
if (filters.severity) {
|
|
156
|
+
issues = issues.filter((i) => i.severity === filters.severity);
|
|
157
|
+
}
|
|
158
|
+
if (filters.rule) {
|
|
159
|
+
issues = issues.filter((i) => i.rule.startsWith(filters.rule));
|
|
160
|
+
}
|
|
161
|
+
// Sort: errors → warnings → infos, then by collection name
|
|
162
|
+
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
163
|
+
issues.sort((a, b) => {
|
|
164
|
+
const sOrder = severityOrder[a.severity] - severityOrder[b.severity];
|
|
165
|
+
if (sOrder !== 0)
|
|
166
|
+
return sOrder;
|
|
167
|
+
return a.collection.localeCompare(b.collection);
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
issues,
|
|
171
|
+
totalScanned: allDocuments.length,
|
|
172
|
+
collectionsScanned: targetCollections,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// ── Input schema ──────────────────────────────────────────────────────────────
|
|
176
|
+
const issuesInputShape = {
|
|
177
|
+
keyPath: zod_1.z.string().describe('Absolute or relative path to the Firebase service account JSON file.'),
|
|
178
|
+
severity: zod_1.z.enum(['error', 'warning', 'info']).optional().describe('Filter by severity. Omit to return all severities.'),
|
|
179
|
+
collection: zod_1.z.string().optional().describe('Filter to a single collection name. Omit to scan all collections.'),
|
|
180
|
+
rule: zod_1.z.string().optional().describe('Filter by rule prefix, e.g. "schema/" returns only schema drift issues, "security/" only security issues.'),
|
|
181
|
+
sampleSize: zod_1.z.number().int().min(1).max(500).optional().describe('Max documents to sample per collection (default: 50).'),
|
|
182
|
+
};
|
|
183
|
+
const IssuesInput = zod_1.z.object({
|
|
184
|
+
keyPath: zod_1.z.string(),
|
|
185
|
+
severity: zod_1.z.enum(['error', 'warning', 'info']).optional(),
|
|
186
|
+
collection: zod_1.z.string().optional(),
|
|
187
|
+
rule: zod_1.z.string().optional(),
|
|
188
|
+
sampleSize: zod_1.z.number().int().min(1).max(500).default(50),
|
|
189
|
+
});
|
|
190
|
+
// ── Register with MCP server ──────────────────────────────────────────────────
|
|
191
|
+
function registerIssuesTool(server) {
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
193
|
+
server.registerTool('lintbase_get_issues', {
|
|
194
|
+
title: 'LintBase Get Issues',
|
|
195
|
+
description: 'Runs all LintBase analyzers and returns a filtered list of issues. ' +
|
|
196
|
+
'Use this for targeted questions like "any errors in users?", "schema issues only?", or "all security problems?". ' +
|
|
197
|
+
'Lighter than lintbase_scan — returns only actionable issues, no summary metadata. ' +
|
|
198
|
+
'Filter by severity (error/warning/info), collection name, or rule prefix (schema/, security/, perf/, cost/).',
|
|
199
|
+
inputSchema: issuesInputShape,
|
|
200
|
+
}, async (rawInput) => {
|
|
201
|
+
try {
|
|
202
|
+
const { keyPath, severity, collection, rule, sampleSize } = IssuesInput.parse(rawInput);
|
|
203
|
+
const { issues, totalScanned, collectionsScanned } = await getIssues(keyPath, sampleSize, { severity, collection, rule });
|
|
204
|
+
if (issues.length === 0) {
|
|
205
|
+
const filterDesc = [
|
|
206
|
+
severity && `severity=${severity}`,
|
|
207
|
+
collection && `collection=${collection}`,
|
|
208
|
+
rule && `rule~=${rule}`,
|
|
209
|
+
].filter(Boolean).join(', ');
|
|
210
|
+
return {
|
|
211
|
+
content: [{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: `✅ No issues found${filterDesc ? ` matching filters [${filterDesc}]` : ''}.\n` +
|
|
214
|
+
`Scanned ${totalScanned} documents across ${collectionsScanned.length} collection(s): [${collectionsScanned.join(', ')}]`,
|
|
215
|
+
}],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// Format as a clean markdown issue list
|
|
219
|
+
const lines = [
|
|
220
|
+
`# LintBase Issues`,
|
|
221
|
+
`Found **${issues.length}** issue(s) across ${collectionsScanned.length} collection(s) (${totalScanned} docs sampled)`,
|
|
222
|
+
``,
|
|
223
|
+
];
|
|
224
|
+
const ICON = { error: '🔴', warning: '🟡', info: '🔵' };
|
|
225
|
+
for (const issue of issues) {
|
|
226
|
+
lines.push(`### ${ICON[issue.severity] ?? '⚪'} [${issue.severity.toUpperCase()}] \`${issue.rule}\``);
|
|
227
|
+
lines.push(`**Collection:** \`${issue.collection}\``);
|
|
228
|
+
lines.push(`**Message:** ${issue.message}`);
|
|
229
|
+
if (issue.suggestion) {
|
|
230
|
+
lines.push(`**Fix:** ${issue.suggestion}`);
|
|
231
|
+
}
|
|
232
|
+
if (issue.affectedDocuments && issue.affectedDocuments.length > 0) {
|
|
233
|
+
lines.push(`**Affected docs:** ${issue.affectedDocuments.slice(0, 3).join(', ')}${issue.affectedDocuments.length > 3 ? ` (+${issue.affectedDocuments.length - 3} more)` : ''}`);
|
|
234
|
+
}
|
|
235
|
+
lines.push('');
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: 'text', text: `lintbase_get_issues failed: ${message}` }],
|
|
245
|
+
isError: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=issues.tool.js.map
|