@homenshum/convex-mcp-nodebench 0.9.4 → 0.9.6

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.4",
82
+ version: "0.9.6",
83
83
  }, {
84
84
  capabilities: {
85
85
  tools: {},
@@ -72,7 +72,10 @@ function auditActions(convexDir) {
72
72
  }
73
73
  const body = lines.slice(startLine, endLine).join("\n");
74
74
  // Check 1: ctx.db access in action (FATAL — not allowed)
75
- if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body)) {
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) {
76
79
  actionsWithDbAccess++;
77
80
  issues.push({
78
81
  severity: "critical",
@@ -81,11 +81,13 @@ function auditAuthorization(convexDir) {
81
81
  const identityAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+ctx\.auth\.getUserIdentity\s*\(\s*\)/);
82
82
  if (identityAssign) {
83
83
  const varName = identityAssign[1];
84
- const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|if\\s*\\(\\s*!${varName}\\s*\\)`).test(body);
84
+ // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
85
+ 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);
85
86
  if (!hasNullCheck) {
86
87
  uncheckedIdentity++;
88
+ // Queries can intentionally return different data for auth/unauth — warning not critical
87
89
  issues.push({
88
- severity: "critical",
90
+ severity: ft === "query" ? "warning" : "critical",
89
91
  location: `${relativePath}:${i + 1}`,
90
92
  functionName: funcName,
91
93
  message: `${ft} "${funcName}" calls getUserIdentity() but doesn't check for null. Unauthenticated users will get undefined identity.`,
@@ -97,11 +99,13 @@ function auditAuthorization(convexDir) {
97
99
  const authUserAssign = body.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+getAuthUserId\s*\(/);
98
100
  if (authUserAssign) {
99
101
  const varName = authUserAssign[1];
100
- const hasNullCheck = new RegExp(`if\\s*\\(\\s*!${varName}\\b|if\\s*\\(\\s*${varName}\\s*===?\\s*null|throw.*!${varName}`).test(body);
102
+ // Recognize: if (!var), if (var === null), if (var), var &&, var ?, throw on !var, comparisons
103
+ 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);
101
104
  if (!hasNullCheck) {
102
105
  uncheckedIdentity++;
106
+ // Queries can intentionally return different data for auth/unauth — warning not critical
103
107
  issues.push({
104
- severity: "critical",
108
+ severity: ft === "query" ? "warning" : "critical",
105
109
  location: `${relativePath}:${i + 1}`,
106
110
  functionName: funcName,
107
111
  message: `${ft} "${funcName}" calls getAuthUserId() but doesn't check for null. Unauthenticated users will get null userId.`,
@@ -76,8 +76,11 @@ 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
+ // Public mutations/actions without args validators are a security concern (unvalidated client input)
80
+ // Queries and internal functions: just a best-practice recommendation
81
+ const isSecurity = !fn.isInternal && (fn.type === "mutation" || fn.type === "action");
79
82
  issues.push({
80
- severity: "critical",
83
+ severity: isSecurity ? "critical" : "warning",
81
84
  location: `${fn.relativePath}:${fn.line}`,
82
85
  functionName: fn.name,
83
86
  message: `${fn.type} "${fn.name}" is missing args validator`,
@@ -94,8 +97,9 @@ function auditFunctions(convexDir) {
94
97
  });
95
98
  }
96
99
  if (!fn.hasHandler) {
100
+ // Old shorthand syntax (query(async (ctx) => {})) works fine — just a style recommendation
97
101
  issues.push({
98
- severity: "critical",
102
+ severity: "warning",
99
103
  location: `${fn.relativePath}:${fn.line}`,
100
104
  functionName: fn.name,
101
105
  message: `${fn.type} "${fn.name}" is missing handler property (may be using old syntax)`,
@@ -284,7 +288,7 @@ export const functionTools = [
284
288
  const functions = extractFunctions(convexDir);
285
289
  // Store audit result
286
290
  const db = getDb();
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
+ 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);
288
292
  const critical = issues.filter((i) => i.severity === "critical");
289
293
  const warnings = issues.filter((i) => i.severity === "warning");
290
294
  // Aggregate issues by category for cleaner output
@@ -77,17 +77,18 @@ function runQualityGate(projectDir, thresholds) {
77
77
  }
78
78
  catch { /* skip */ }
79
79
  }
80
- // Check 4: Type safety (as any casts)
80
+ // Check 4: Type safety (as any casts) — exclude test/eval files
81
81
  const typeSafety = getLatest("type_safety");
82
82
  if (typeSafety) {
83
83
  try {
84
84
  const issues = JSON.parse(typeSafety.issues_json);
85
- const asAnyIssues = Array.isArray(issues)
86
- ? issues.filter((i) => i.message?.includes("as any")).length
87
- : 0;
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;
88
87
  // Each as-any issue represents a FILE, count from message for actual number
89
88
  const actualCasts = Array.isArray(issues)
90
89
  ? issues.reduce((sum, i) => {
90
+ if (testEvalPattern.test(i.location ?? ""))
91
+ return sum; // skip test/eval files
91
92
  const countMatch = i.message?.match(/(\d+)\s+`as any`/);
92
93
  return sum + (countMatch ? parseInt(countMatch[1], 10) : 0);
93
94
  }, 0)
@@ -102,13 +103,13 @@ function runQualityGate(projectDir, thresholds) {
102
103
  }
103
104
  catch { /* skip */ }
104
105
  }
105
- // Check 5: Unbounded collects
106
+ // Check 5: Unbounded collects (only warning-level — indexed collects are downgraded to info)
106
107
  const queryEfficiency = getLatest("query_efficiency");
107
108
  if (queryEfficiency) {
108
109
  try {
109
110
  const issues = JSON.parse(queryEfficiency.issues_json);
110
111
  const unbounded = Array.isArray(issues)
111
- ? issues.filter((i) => i.message?.includes(".collect()")).length
112
+ ? issues.filter((i) => i.severity === "warning" && i.message?.includes(".collect()")).length
112
113
  : 0;
113
114
  checks.push({
114
115
  metric: "unbounded_collects",
@@ -63,13 +63,16 @@ 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
67
66
  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);
68
69
  issues.push({
69
- severity: "warning",
70
+ severity: hasIndex ? "info" : "warning",
70
71
  location: `${relativePath}:${i + 1}`,
71
72
  message: `.collect() without .take() limit${tableMatch ? ` on table "${tableMatch[1]}"` : ""}. Could return entire table.`,
72
- fix: "Add .take(N) before .collect(), or use .paginate() for large result sets",
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",
73
76
  });
74
77
  }
75
78
  }
@@ -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.4",
78
+ version: "0.9.6",
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_usage", 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", 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.4",
3
+ "version": "0.9.6",
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": {