@homenshum/convex-mcp-nodebench 0.9.8 → 0.10.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/index.js CHANGED
@@ -79,7 +79,7 @@ for (const tool of ALL_TOOLS) {
79
79
  // ── Server setup ────────────────────────────────────────────────────
80
80
  const server = new Server({
81
81
  name: "convex-mcp-nodebench",
82
- version: "0.9.8",
82
+ version: "0.9.4",
83
83
  }, {
84
84
  capabilities: {
85
85
  tools: {},
@@ -349,7 +349,9 @@ After running all audits, summarize:
349
349
  - Total issues by severity (critical/warning/info)
350
350
  - Top 5 most impactful issues to fix first
351
351
  - Quality gate score and grade
352
- - Trend direction if previous audits exist (use convex_audit_diff)`,
352
+ - Trend direction if previous audits exist (use convex_audit_diff)
353
+
354
+ IMPORTANT — Analyst diagnostic: For every issue found, trace the ROOT CAUSE — don't just list what's wrong, explain WHY it happened. Ask "why" 5 times. Record root causes with convex_record_gotcha so they don't recur.`,
353
355
  },
354
356
  },
355
357
  ],
@@ -373,6 +375,8 @@ After running all audits, summarize:
373
375
  6. convex_schema_migration_plan — Compare against previous snapshot for breaking changes
374
376
  7. convex_quality_gate — Final quality check with thresholds
375
377
 
378
+ For each blocker: diagnose the ROOT CAUSE, not just the symptom. Explain WHY the issue exists and how to prevent it from recurring.
379
+
376
380
  Report: DEPLOY or DO NOT DEPLOY with specific blockers to fix.`,
377
381
  },
378
382
  },
@@ -396,7 +400,9 @@ Report: DEPLOY or DO NOT DEPLOY with specific blockers to fix.`,
396
400
  5. convex_audit_pagination — Unbounded numItems (DoS risk)
397
401
  6. convex_audit_transaction_safety — Race condition risks
398
402
 
399
- Focus on: unauthorized data access, unvalidated inputs, missing error boundaries, and potential data corruption vectors.`,
403
+ Focus on: unauthorized data access, unvalidated inputs, missing error boundaries, and potential data corruption vectors.
404
+
405
+ Analyst diagnostic: For each security finding, trace upstream to the ROOT CAUSE. Don't just flag the symptom — explain what system condition allowed the vulnerability to exist. Record findings with convex_record_gotcha.`,
400
406
  },
401
407
  },
402
408
  ],
@@ -71,16 +71,11 @@ function auditActions(convexDir) {
71
71
  }
72
72
  }
73
73
  const body = lines.slice(startLine, endLine).join("\n");
74
- // Check 1: ctx.db access in action (not allowed will throw at runtime)
75
- // BUT: skip if ctx.db is inside an inline ctx.runMutation/ctx.runQuery callback
76
- // e.g. ctx.runMutation(async (ctx) => { ctx.db.patch(...) }) — the inner ctx is a mutation context
77
- const hasInlineCallback = /ctx\.run(Mutation|Query)\s*\(\s*async\s*\(/.test(body);
78
- if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body) && !hasInlineCallback) {
74
+ // Check 1: ctx.db access in action (FATALnot allowed)
75
+ if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body)) {
79
76
  actionsWithDbAccess++;
80
- // internalAction ctx.db is a warning (not client-callable, likely called from controlled contexts)
81
- // public action ctx.db is a warning too (runtime error but caught during development/testing)
82
77
  issues.push({
83
- severity: "warning",
78
+ severity: "critical",
84
79
  location: `${relativePath}:${startLine + 1}`,
85
80
  functionName: funcName,
86
81
  message: `${funcType} "${funcName}" accesses ctx.db directly. Actions cannot access the database — use ctx.runQuery/ctx.runMutation instead.`,
@@ -90,9 +85,8 @@ function auditActions(convexDir) {
90
85
  // Check 2: Node API usage without "use node"
91
86
  if (!hasUseNode && (nodeApis.test(body) || nodeCryptoApis.test(body))) {
92
87
  actionsWithoutNodeDirective++;
93
- // Warning: missing directive is a deployment concern caught during development
94
88
  issues.push({
95
- severity: "warning",
89
+ severity: "critical",
96
90
  location: `${relativePath}:${startLine + 1}`,
97
91
  functionName: funcName,
98
92
  message: `${funcType} "${funcName}" uses Node.js APIs but file lacks "use node" directive. Will fail in Convex runtime.`,
@@ -41,27 +41,6 @@ function auditAuthorization(convexDir) {
41
41
  const content = readFileSync(filePath, "utf-8");
42
42
  const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
43
43
  const lines = content.split("\n");
44
- // Pre-scan: detect local helper functions that wrap getAuthUserId/getUserIdentity
45
- // Pattern: function getSafeUserId(ctx) { ... getAuthUserId(ctx) ... }
46
- const authHelperNames = [];
47
- const helperFuncPattern = /(?:async\s+)?function\s+(\w+)\s*\(/g;
48
- let hm;
49
- while ((hm = helperFuncPattern.exec(content)) !== null) {
50
- const hStart = content.slice(0, hm.index).split("\n").length - 1;
51
- const hBody = lines.slice(hStart, Math.min(hStart + 30, lines.length)).join("\n");
52
- if (/getAuthUserId|getUserIdentity|getAuthSessionId/.test(hBody)) {
53
- authHelperNames.push(hm[1]);
54
- }
55
- }
56
- // Also check arrow function helpers: const getUserId = async (ctx) => { ... }
57
- const arrowHelperPattern = /(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/g;
58
- while ((hm = arrowHelperPattern.exec(content)) !== null) {
59
- const hStart = content.slice(0, hm.index).split("\n").length - 1;
60
- const hBody = lines.slice(hStart, Math.min(hStart + 20, lines.length)).join("\n");
61
- if (/getAuthUserId|getUserIdentity|getAuthSessionId/.test(hBody)) {
62
- authHelperNames.push(hm[1]);
63
- }
64
- }
65
44
  for (let i = 0; i < lines.length; i++) {
66
45
  const line = lines[i];
67
46
  for (const ft of funcTypes) {
@@ -90,14 +69,10 @@ function auditAuthorization(convexDir) {
90
69
  }
91
70
  }
92
71
  const body = lines.slice(i, endLine).join("\n");
93
- // Check for direct auth calls OR calls to local auth helper wrappers
94
- const hasDirectAuth = /ctx\.auth\.getUserIdentity\s*\(\s*\)/.test(body) ||
72
+ const hasAuthCheck = /ctx\.auth\.getUserIdentity\s*\(\s*\)/.test(body) ||
95
73
  /getUserIdentity/.test(body) ||
96
74
  /getAuthUserId/.test(body) ||
97
75
  /getAuthSessionId/.test(body);
98
- const callsAuthHelper = authHelperNames.length > 0 &&
99
- authHelperNames.some(h => new RegExp(`\\b${h}\\s*\\(`).test(body));
100
- const hasAuthCheck = hasDirectAuth || callsAuthHelper;
101
76
  const hasDbWrite = dbWriteOps.test(body);
102
77
  const isSensitiveName = writeSensitive.test(funcName);
103
78
  if (hasAuthCheck) {
@@ -106,13 +81,11 @@ function auditAuthorization(convexDir) {
106
81
  const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
107
82
  if (identityAssign) {
108
83
  const varName = identityAssign[1];
109
- // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
110
- const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*${varName}\\s*\\)|${varName}\\s*&&|${varName}\\s*\\?|throw.*!${varName}|${varName}\\s*!==?\\s*null|===\\s*${varName}|!==\\s*${varName}`).test(body);
84
+ const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*!${varName}\\s*\\)`).test(body);
111
85
  if (!hasNullCheck) {
112
86
  uncheckedIdentity++;
113
- // Queries can intentionally return different data for auth/unauth — warning not critical
114
87
  issues.push({
115
- severity: "warning",
88
+ severity: "critical",
116
89
  location: `${relativePath}:${i + 1}`,
117
90
  functionName: funcName,
118
91
  message: `${ft} "${funcName}" calls getUserIdentity() but doesn't check for null. Unauthenticated users will get undefined identity.`,
@@ -124,13 +97,11 @@ function auditAuthorization(convexDir) {
124
97
  const authUserAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+getAuthUserId\s*\(/);
125
98
  if (authUserAssign) {
126
99
  const varName = authUserAssign[1];
127
- // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
128
- const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*${varName}\\s*\\)|${varName}\\s*&&|${varName}\\s*\\?|throw.*!${varName}|${varName}\\s*!==?\\s*null|===\\s*${varName}|!==\\s*${varName}`).test(body);
100
+ const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|throw.*!${varName}`).test(body);
129
101
  if (!hasNullCheck) {
130
102
  uncheckedIdentity++;
131
- // Queries can intentionally return different data for auth/unauth — warning not critical
132
103
  issues.push({
133
- severity: "warning",
104
+ severity: "critical",
134
105
  location: `${relativePath}:${i + 1}`,
135
106
  functionName: funcName,
136
107
  message: `${ft} "${funcName}" calls getAuthUserId() but doesn't check for null. Unauthenticated users will get null userId.`,
@@ -141,13 +112,11 @@ function auditAuthorization(convexDir) {
141
112
  }
142
113
  else {
143
114
  withoutAuth++;
144
- // Warning: public mutation/action with DB writes but no auth
145
- // Downgraded from critical — missing auth is a security posture issue, not a runtime failure.
146
- // Many monorepo mutations are system-level (called by actions/schedulers), not client-facing.
115
+ // Critical: public mutation/action with DB writes but no auth
147
116
  if ((ft === "mutation" || ft === "action") && hasDbWrite) {
148
117
  const sensitiveHint = isSensitiveName ? ` Name "${funcName}" suggests a destructive operation.` : "";
149
118
  issues.push({
150
- severity: "warning",
119
+ severity: "critical",
151
120
  location: `${relativePath}:${i + 1}`,
152
121
  functionName: funcName,
153
122
  message: `Public ${ft} "${funcName}" writes to DB without auth check. Any client can call this.${sensitiveHint}`,
@@ -155,8 +124,9 @@ function auditAuthorization(convexDir) {
155
124
  });
156
125
  }
157
126
  else if (isSensitiveName) {
127
+ // Only flag sensitive name separately if not already caught by DB-write check
158
128
  issues.push({
159
- severity: "warning",
129
+ severity: "critical",
160
130
  location: `${relativePath}:${i + 1}`,
161
131
  functionName: funcName,
162
132
  message: `Public ${ft} "${funcName}" has a sensitive name but no auth check. Consider making it internal or adding auth.`,
@@ -137,6 +137,20 @@ function scoreCritterCheck(input) {
137
137
  score += 10;
138
138
  feedback.push("Good: success criteria defined — this makes the deploy gate concrete.");
139
139
  }
140
+ // ── Check 10: Bandaid detection — symptom fixes without root-cause reasoning ──
141
+ const bandaidPatterns = [
142
+ "add try.?catch", "wrap in try", "catch the error", "suppress the error",
143
+ "add optional chaining", "add \\?\\.", "silence the warning",
144
+ "add as any", "cast to any", "ignore the type",
145
+ "add a timeout", "increase the timeout", "add a delay",
146
+ "delete the test", "skip the test", "disable the test",
147
+ "hide the error", "remove the warning",
148
+ ];
149
+ const bandaidRegex = new RegExp(bandaidPatterns.join("|"), "i");
150
+ if (bandaidRegex.test(taskLower) && !whyLower.includes("root cause") && !whyLower.includes("because") && whyLower.length < 50) {
151
+ score -= 20;
152
+ feedback.push("Bandaid alert: this looks like a symptom fix. What's the root cause? Diagnose like an analyst, not a junior dev.");
153
+ }
140
154
  score = Math.max(0, Math.min(100, score));
141
155
  let verdict;
142
156
  if (score >= 70) {
@@ -57,7 +57,7 @@ function extractFunctions(convexDir) {
57
57
  filePath,
58
58
  relativePath,
59
59
  line: i + 1,
60
- hasArgs: /args\s*:\s*[\{\v]/.test(chunk) || /args\s*:\s*v\./.test(chunk) || /args\s*:\s*\w/.test(chunk),
60
+ hasArgs: /args\s*:\s*[\{\v]/.test(chunk) || /args\s*:\s*v\./.test(chunk),
61
61
  hasReturns: /returns\s*:\s*v\./.test(chunk),
62
62
  hasHandler: /handler\s*:/.test(chunk),
63
63
  });
@@ -76,10 +76,8 @@ function auditFunctions(convexDir) {
76
76
  if (fn.type === "httpAction")
77
77
  continue; // httpActions don't have args/returns validators
78
78
  if (!fn.hasArgs) {
79
- // Missing args validator is a best practice recommendation, not a runtime failure.
80
- // Functions without args simply accept no arguments — no unvalidated input risk.
81
79
  issues.push({
82
- severity: "warning",
80
+ severity: "critical",
83
81
  location: `${fn.relativePath}:${fn.line}`,
84
82
  functionName: fn.name,
85
83
  message: `${fn.type} "${fn.name}" is missing args validator`,
@@ -96,9 +94,8 @@ function auditFunctions(convexDir) {
96
94
  });
97
95
  }
98
96
  if (!fn.hasHandler) {
99
- // Old shorthand syntax (query(async (ctx) => {})) works fine — just a style recommendation
100
97
  issues.push({
101
- severity: "warning",
98
+ severity: "critical",
102
99
  location: `${fn.relativePath}:${fn.line}`,
103
100
  functionName: fn.name,
104
101
  message: `${fn.type} "${fn.name}" is missing handler property (may be using old syntax)`,
@@ -287,7 +284,7 @@ export const functionTools = [
287
284
  const functions = extractFunctions(convexDir);
288
285
  // Store audit result
289
286
  const db = getDb();
290
- db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "functions", JSON.stringify(issues), issues.length);
287
+ db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "function_audit", JSON.stringify(issues), issues.length);
291
288
  const critical = issues.filter((i) => i.severity === "critical");
292
289
  const warnings = issues.filter((i) => i.severity === "warning");
293
290
  // Aggregate issues by category for cleaner output
@@ -83,6 +83,19 @@ const METHODOLOGY_CONTENT = {
83
83
  ],
84
84
  tools: ["convex_suggest_indexes", "convex_audit_schema"],
85
85
  },
86
+ analyst_diagnostic: {
87
+ title: "Analyst Diagnostic — Root Cause Over Bandaids",
88
+ description: "Guide yourself like an analyst diagnosing the root cause, NOT a junior dev slapping on a bandaid. Mandatory for all bug work.",
89
+ steps: [
90
+ "1. REPRODUCE: Confirm the exact failure mode before touching any code",
91
+ "2. TRACE UPSTREAM: Walk from symptom → intermediate state → root cause. Don't stop at the first error you see",
92
+ "3. ASK 'WHY' 5 TIMES: Each answer should go one level deeper into the system",
93
+ "4. FIX THE CAUSE: The right fix makes the symptom impossible, not just invisible",
94
+ "5. VERIFY NO SIDEWAYS SHIFT: Bandaids move bugs, they don't fix them — check adjacent behavior",
95
+ "6. RECORD: Use convex_record_gotcha with the root cause so the next person doesn't re-discover it",
96
+ ],
97
+ tools: ["convex_search_gotchas", "convex_record_gotcha", "convex_critter_check"],
98
+ },
86
99
  };
87
100
  // ── Tool Definitions ────────────────────────────────────────────────
88
101
  export const methodologyTools = [
@@ -101,6 +114,7 @@ export const methodologyTools = [
101
114
  "convex_deploy_verification",
102
115
  "convex_knowledge_management",
103
116
  "convex_index_optimization",
117
+ "analyst_diagnostic",
104
118
  ],
105
119
  description: "Which methodology to explain",
106
120
  },
@@ -77,18 +77,17 @@ function runQualityGate(projectDir, thresholds) {
77
77
  }
78
78
  catch { /* skip */ }
79
79
  }
80
- // Check 4: Type safety (as any casts) — exclude test/eval files
80
+ // Check 4: Type safety (as any casts)
81
81
  const typeSafety = getLatest("type_safety");
82
82
  if (typeSafety) {
83
83
  try {
84
84
  const issues = JSON.parse(typeSafety.issues_json);
85
- // Exclude test, eval, benchmark, and fixture files from production quality gate
86
- const testEvalPattern = /test|__tests__|spec|\.test\.|\.spec\.|fixtures|mocks|evaluation|liveEval|liveApiSmoke|benchmark|calibration|harness|quickTest|comprehensiveTest|comprehensiveEval/i;
85
+ const asAnyIssues = Array.isArray(issues)
86
+ ? issues.filter((i) => i.message?.includes("as any")).length
87
+ : 0;
87
88
  // Each as-any issue represents a FILE, count from message for actual number
88
89
  const actualCasts = Array.isArray(issues)
89
90
  ? issues.reduce((sum, i) => {
90
- if (testEvalPattern.test(i.location ?? ""))
91
- return sum; // skip test/eval files
92
91
  const countMatch = i.message?.match(/(\d+)\s+`as any`/);
93
92
  return sum + (countMatch ? parseInt(countMatch[1], 10) : 0);
94
93
  }, 0)
@@ -103,13 +102,13 @@ function runQualityGate(projectDir, thresholds) {
103
102
  }
104
103
  catch { /* skip */ }
105
104
  }
106
- // Check 5: Unbounded collects (only warning-level — indexed collects are downgraded to info)
105
+ // Check 5: Unbounded collects
107
106
  const queryEfficiency = getLatest("query_efficiency");
108
107
  if (queryEfficiency) {
109
108
  try {
110
109
  const issues = JSON.parse(queryEfficiency.issues_json);
111
110
  const unbounded = Array.isArray(issues)
112
- ? issues.filter((i) => i.severity === "warning" && i.message?.includes(".collect()")).length
111
+ ? issues.filter((i) => i.message?.includes(".collect()")).length
113
112
  : 0;
114
113
  checks.push({
115
114
  metric: "unbounded_collects",
@@ -63,16 +63,13 @@ function auditQueryEfficiency(convexDir) {
63
63
  const chain = lines.slice(chainStart, i + 1).join("\n");
64
64
  if (!/\.take\s*\(/.test(chain) && !/\.paginate\s*\(/.test(chain)) {
65
65
  collectWithoutLimit++;
66
+ // Check if it's a bounded table or could be large
66
67
  const tableMatch = chain.match(/\.query\s*\(\s*["'](\w+)["']\s*\)/);
67
- // Indexed queries are bounded by the index range — lower severity
68
- const hasIndex = /\.withIndex\s*\(/.test(chain);
69
68
  issues.push({
70
- severity: hasIndex ? "info" : "warning",
69
+ severity: "warning",
71
70
  location: `${relativePath}:${i + 1}`,
72
71
  message: `.collect() without .take() limit${tableMatch ? ` on table "${tableMatch[1]}"` : ""}. Could return entire table.`,
73
- fix: hasIndex
74
- ? "Consider adding .take(N) for predictable result sizes, even with index filtering"
75
- : "Add .take(N) before .collect(), or use .paginate() for large result sets",
72
+ fix: "Add .take(N) before .collect(), or use .paginate() for large result sets",
76
73
  });
77
74
  }
78
75
  }
@@ -75,7 +75,7 @@ function buildSarif(projectDir, auditTypes, limit) {
75
75
  tool: {
76
76
  driver: {
77
77
  name: "convex-mcp-nodebench",
78
- version: "0.9.8",
78
+ version: "0.9.4",
79
79
  informationUri: "https://www.npmjs.com/package/@homenshum/convex-mcp-nodebench",
80
80
  rules: [...rulesMap.values()],
81
81
  },
@@ -133,7 +133,7 @@ export const storageAuditTools = [
133
133
  }
134
134
  const { issues, stats } = auditStorageUsage(convexDir);
135
135
  const db = getDb();
136
- db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "storage", JSON.stringify(issues), issues.length);
136
+ db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "storage_usage", JSON.stringify(issues), issues.length);
137
137
  return {
138
138
  summary: {
139
139
  ...stats,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homenshum/convex-mcp-nodebench",
3
- "version": "0.9.8",
3
+ "version": "0.10.0",
4
4
  "description": "Convex-specific MCP server applying NodeBench self-instruct diligence patterns to Convex development. Schema audit, function compliance, deployment gates, persistent gotcha DB, and methodology guidance. Complements Context7 (raw docs) and official Convex MCP (deployment introspection) with structured verification workflows.",
5
5
  "type": "module",
6
6
  "bin": {