@homenshum/convex-mcp-nodebench 0.4.1 → 0.8.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.
Files changed (39) hide show
  1. package/dist/index.js +293 -4
  2. package/dist/tools/actionAuditTools.d.ts +2 -0
  3. package/dist/tools/actionAuditTools.js +180 -0
  4. package/dist/tools/authorizationTools.d.ts +2 -0
  5. package/dist/tools/authorizationTools.js +201 -0
  6. package/dist/tools/dataModelingTools.d.ts +2 -0
  7. package/dist/tools/dataModelingTools.js +168 -0
  8. package/dist/tools/deploymentTools.js +42 -2
  9. package/dist/tools/devSetupTools.d.ts +2 -0
  10. package/dist/tools/devSetupTools.js +170 -0
  11. package/dist/tools/embeddingProvider.d.ts +6 -0
  12. package/dist/tools/embeddingProvider.js +3 -0
  13. package/dist/tools/functionTools.js +24 -1
  14. package/dist/tools/httpTools.js +128 -48
  15. package/dist/tools/migrationTools.d.ts +2 -0
  16. package/dist/tools/migrationTools.js +133 -0
  17. package/dist/tools/paginationTools.d.ts +2 -0
  18. package/dist/tools/paginationTools.js +125 -0
  19. package/dist/tools/qualityGateTools.d.ts +2 -0
  20. package/dist/tools/qualityGateTools.js +204 -0
  21. package/dist/tools/queryEfficiencyTools.d.ts +2 -0
  22. package/dist/tools/queryEfficiencyTools.js +191 -0
  23. package/dist/tools/reportingTools.d.ts +2 -0
  24. package/dist/tools/reportingTools.js +240 -0
  25. package/dist/tools/schedulerTools.d.ts +2 -0
  26. package/dist/tools/schedulerTools.js +197 -0
  27. package/dist/tools/schemaTools.js +95 -1
  28. package/dist/tools/storageAuditTools.d.ts +2 -0
  29. package/dist/tools/storageAuditTools.js +148 -0
  30. package/dist/tools/toolRegistry.d.ts +4 -0
  31. package/dist/tools/toolRegistry.js +274 -11
  32. package/dist/tools/transactionSafetyTools.d.ts +2 -0
  33. package/dist/tools/transactionSafetyTools.js +166 -0
  34. package/dist/tools/typeSafetyTools.d.ts +2 -0
  35. package/dist/tools/typeSafetyTools.js +146 -0
  36. package/dist/tools/vectorSearchTools.d.ts +2 -0
  37. package/dist/tools/vectorSearchTools.js +192 -0
  38. package/dist/types.d.ts +6 -1
  39. package/package.json +1 -1
@@ -261,6 +261,230 @@ export const REGISTRY = [
261
261
  phase: "meta",
262
262
  complexity: "low",
263
263
  },
264
+ // ── Authorization Tools ──────────────────
265
+ {
266
+ name: "convex_audit_authorization",
267
+ category: "function",
268
+ tags: ["auth", "authorization", "security", "getUserIdentity", "public", "mutation", "permission"],
269
+ quickRef: {
270
+ nextAction: "Fix critical auth issues: add getUserIdentity() checks to public mutations that write data",
271
+ nextTools: ["convex_audit_functions", "convex_audit_actions"],
272
+ methodology: "convex_function_compliance",
273
+ relatedGotchas: ["internal_for_private"],
274
+ confidence: "high",
275
+ },
276
+ phase: "audit",
277
+ complexity: "high",
278
+ },
279
+ // ── Query Efficiency Tools ────────────────
280
+ {
281
+ name: "convex_audit_query_efficiency",
282
+ category: "schema",
283
+ tags: ["query", "performance", "collect", "filter", "index", "pagination", "efficiency", "table-scan"],
284
+ quickRef: {
285
+ nextAction: "Add .take() limits to unbounded .collect() calls and indexes for .filter() patterns",
286
+ nextTools: ["convex_suggest_indexes", "convex_audit_pagination"],
287
+ methodology: "convex_index_optimization",
288
+ relatedGotchas: ["index_field_order"],
289
+ confidence: "high",
290
+ },
291
+ phase: "audit",
292
+ complexity: "medium",
293
+ },
294
+ // ── Action Audit Tools ────────────────────
295
+ {
296
+ name: "convex_audit_actions",
297
+ category: "function",
298
+ tags: ["action", "ctx.db", "use-node", "fetch", "error-handling", "external-api", "runtime"],
299
+ quickRef: {
300
+ nextAction: "Fix critical action issues: remove ctx.db access, add 'use node' directives, wrap external calls in try/catch",
301
+ nextTools: ["convex_audit_functions", "convex_audit_transaction_safety"],
302
+ methodology: "convex_function_compliance",
303
+ relatedGotchas: ["action_from_action"],
304
+ confidence: "high",
305
+ },
306
+ phase: "audit",
307
+ complexity: "medium",
308
+ },
309
+ // ── Type Safety Tools ─────────────────────
310
+ {
311
+ name: "convex_check_type_safety",
312
+ category: "function",
313
+ tags: ["type", "safety", "as-any", "undefined", "null", "id", "generated", "typescript"],
314
+ quickRef: {
315
+ nextAction: "Replace `as any` casts with proper types and use v.id() instead of v.string() for ID fields",
316
+ nextTools: ["convex_check_validator_coverage", "convex_audit_functions"],
317
+ methodology: "convex_function_compliance",
318
+ relatedGotchas: [],
319
+ confidence: "high",
320
+ },
321
+ phase: "audit",
322
+ complexity: "medium",
323
+ },
324
+ // ── Transaction Safety Tools ──────────────
325
+ {
326
+ name: "convex_audit_transaction_safety",
327
+ category: "function",
328
+ tags: ["transaction", "atomicity", "race-condition", "TOCTOU", "runMutation", "consistency"],
329
+ quickRef: {
330
+ nextAction: "Combine multiple runMutation calls into single atomic mutation to avoid partial failures",
331
+ nextTools: ["convex_audit_actions", "convex_audit_functions"],
332
+ methodology: "convex_function_compliance",
333
+ relatedGotchas: [],
334
+ confidence: "medium",
335
+ },
336
+ phase: "audit",
337
+ complexity: "high",
338
+ },
339
+ // ── Storage Audit Tools ───────────────────
340
+ {
341
+ name: "convex_audit_storage_usage",
342
+ category: "function",
343
+ tags: ["storage", "file", "upload", "blob", "getUrl", "orphan", "null-check"],
344
+ quickRef: {
345
+ nextAction: "Add null checks for storage.get()/getUrl() and implement file cleanup on record deletion",
346
+ nextTools: ["convex_audit_functions", "convex_check_type_safety"],
347
+ methodology: "convex_function_compliance",
348
+ relatedGotchas: [],
349
+ confidence: "high",
350
+ },
351
+ phase: "audit",
352
+ complexity: "low",
353
+ },
354
+ // ── Pagination Tools ──────────────────────
355
+ {
356
+ name: "convex_audit_pagination",
357
+ category: "schema",
358
+ tags: ["pagination", "paginate", "cursor", "numItems", "paginationOptsValidator", "limit"],
359
+ quickRef: {
360
+ nextAction: "Add paginationOptsValidator to paginated queries and bound numItems",
361
+ nextTools: ["convex_audit_query_efficiency", "convex_suggest_indexes"],
362
+ methodology: "convex_index_optimization",
363
+ relatedGotchas: [],
364
+ confidence: "high",
365
+ },
366
+ phase: "audit",
367
+ complexity: "low",
368
+ },
369
+ // ── Data Modeling Tools ───────────────────
370
+ {
371
+ name: "convex_audit_data_modeling",
372
+ category: "schema",
373
+ tags: ["modeling", "schema", "nesting", "array", "v.id", "referential-integrity", "normalization"],
374
+ quickRef: {
375
+ nextAction: "Fix dangling v.id() references and consider normalizing deeply nested tables",
376
+ nextTools: ["convex_audit_schema", "convex_suggest_indexes"],
377
+ methodology: "convex_schema_audit",
378
+ relatedGotchas: ["system_fields_auto"],
379
+ confidence: "medium",
380
+ },
381
+ phase: "audit",
382
+ complexity: "medium",
383
+ },
384
+ // ── Dev Setup Tools ───────────────────────
385
+ {
386
+ name: "convex_audit_dev_setup",
387
+ category: "deployment",
388
+ tags: ["setup", "gitignore", "env", "tsconfig", "initialize", "onboarding", "convex-json"],
389
+ quickRef: {
390
+ nextAction: "Fix setup issues: update .gitignore, create .env.example, run npx convex dev",
391
+ nextTools: ["convex_bootstrap_project", "convex_pre_deploy_gate"],
392
+ methodology: "convex_deploy_verification",
393
+ relatedGotchas: [],
394
+ confidence: "high",
395
+ },
396
+ phase: "deploy",
397
+ complexity: "low",
398
+ },
399
+ // ── Migration Tools ───────────────────────
400
+ {
401
+ name: "convex_schema_migration_plan",
402
+ category: "schema",
403
+ tags: ["migration", "diff", "snapshot", "deploy", "risk", "breaking-change", "backup"],
404
+ quickRef: {
405
+ nextAction: "Review migration steps and back up data before deploying if risk is high",
406
+ nextTools: ["convex_snapshot_schema", "convex_pre_deploy_gate"],
407
+ methodology: "convex_deploy_verification",
408
+ relatedGotchas: [],
409
+ confidence: "high",
410
+ },
411
+ phase: "deploy",
412
+ complexity: "medium",
413
+ },
414
+ // ── Reporting Tools ─────────────────────
415
+ {
416
+ name: "convex_export_sarif",
417
+ category: "integration",
418
+ tags: ["sarif", "export", "report", "github", "code-scanning", "ci", "static-analysis"],
419
+ quickRef: {
420
+ nextAction: "Upload the SARIF file to GitHub Code Scanning or open in VS Code SARIF Viewer",
421
+ nextTools: ["convex_audit_diff", "convex_quality_gate"],
422
+ methodology: "convex_deploy_verification",
423
+ relatedGotchas: [],
424
+ confidence: "high",
425
+ },
426
+ phase: "deploy",
427
+ complexity: "low",
428
+ },
429
+ {
430
+ name: "convex_audit_diff",
431
+ category: "deployment",
432
+ tags: ["diff", "baseline", "trend", "new-issues", "fixed", "improving", "degrading", "comparison"],
433
+ quickRef: {
434
+ nextAction: "Focus on fixing new issues first, then tackle existing ones",
435
+ nextTools: ["convex_export_sarif", "convex_quality_gate"],
436
+ methodology: "convex_deploy_verification",
437
+ relatedGotchas: [],
438
+ confidence: "high",
439
+ },
440
+ phase: "deploy",
441
+ complexity: "medium",
442
+ },
443
+ // ── Vector Search Tools ─────────────────
444
+ {
445
+ name: "convex_audit_vector_search",
446
+ category: "schema",
447
+ tags: ["vector", "search", "embedding", "dimension", "similarity", "vectorIndex", "float64", "AI", "RAG"],
448
+ quickRef: {
449
+ nextAction: "Fix dimension mismatches and add filterFields to vector indexes for better performance",
450
+ nextTools: ["convex_audit_schema", "convex_suggest_indexes"],
451
+ methodology: "convex_schema_audit",
452
+ relatedGotchas: [],
453
+ confidence: "high",
454
+ },
455
+ phase: "audit",
456
+ complexity: "medium",
457
+ },
458
+ // ── Scheduler Tools ─────────────────────
459
+ {
460
+ name: "convex_audit_schedulers",
461
+ category: "function",
462
+ tags: ["scheduler", "runAfter", "runAt", "schedule", "cron", "infinite-loop", "backoff", "retry", "delayed"],
463
+ quickRef: {
464
+ nextAction: "Fix self-scheduling loops (add termination conditions) and implement exponential backoff",
465
+ nextTools: ["convex_check_crons", "convex_audit_actions"],
466
+ methodology: "convex_function_compliance",
467
+ relatedGotchas: [],
468
+ confidence: "high",
469
+ },
470
+ phase: "audit",
471
+ complexity: "medium",
472
+ },
473
+ // ── Quality Gate Tools ──────────────────
474
+ {
475
+ name: "convex_quality_gate",
476
+ category: "deployment",
477
+ tags: ["quality", "gate", "score", "grade", "threshold", "sonarqube", "metrics", "pass-fail", "A-F"],
478
+ quickRef: {
479
+ nextAction: "Fix blockers to raise your grade, then run again to verify improvement",
480
+ nextTools: ["convex_audit_diff", "convex_export_sarif", "convex_pre_deploy_gate"],
481
+ methodology: "convex_deploy_verification",
482
+ relatedGotchas: [],
483
+ confidence: "high",
484
+ },
485
+ phase: "deploy",
486
+ complexity: "high",
487
+ },
264
488
  ];
265
489
  export function getQuickRef(toolName) {
266
490
  const entry = REGISTRY.find((e) => e.name === toolName);
@@ -338,6 +562,10 @@ export function findTools(query) {
338
562
  /**
339
563
  * Async wrapper around findTools that fuses BM25 results with embedding RRF
340
564
  * when a neural embedding provider is available. Falls back to plain findTools otherwise.
565
+ *
566
+ * Uses Agent-as-a-Graph bipartite RRF (arxiv:2511.18194):
567
+ * - Tool nodes get direct wRRF with α_T = 1.0
568
+ * - Domain nodes get stronger wRRF with α_D = 1.5 (paper-optimal, lifts sibling tools in that category)
341
569
  */
342
570
  export async function findToolsWithEmbedding(query) {
343
571
  const bm25Results = findTools(query);
@@ -346,22 +574,57 @@ export async function findToolsWithEmbedding(query) {
346
574
  const queryVec = await embedQuery(query);
347
575
  if (!queryVec)
348
576
  return bm25Results;
349
- const vecResults = embeddingSearch(queryVec, 16);
350
- const vecRanks = new Map();
351
- vecResults.forEach((r, i) => vecRanks.set(r.name, i + 1));
352
- // RRF fusion: combine BM25 rank with embedding rank
577
+ const vecResults = embeddingSearch(queryVec, 30);
578
+ // Split embedding results by node type
579
+ const toolRanks = new Map();
580
+ const domainRanks = new Map();
581
+ let toolIdx = 0, domainIdx = 0;
582
+ for (const r of vecResults) {
583
+ if (r.nodeType === "domain") {
584
+ domainIdx++;
585
+ domainRanks.set(r.name.replace("domain:", ""), domainIdx);
586
+ }
587
+ else {
588
+ toolIdx++;
589
+ toolRanks.set(r.name, toolIdx);
590
+ }
591
+ }
592
+ // Type-specific wRRF: α_T for direct tool matches, α_D for domain matches
593
+ // Paper-optimal (arxiv:2511.18194): α_A=1.5, α_T=1.0, K=60
594
+ // Validated via 6-config ablation grid in mcp-local tools.test.ts
595
+ const ALPHA_T = 1.0; // tool weight
596
+ const ALPHA_D = 1.5; // domain weight (paper-optimal — upward traversal boost)
597
+ const K = 60; // RRF k parameter (paper-optimal)
598
+ // RRF fusion: combine BM25 rank with type-specific embedding ranks
353
599
  const fusedScores = new Map();
354
600
  bm25Results.forEach((entry, i) => {
355
- const bm25Rrf = 1000 / (20 + i + 1);
356
- const embRank = vecRanks.get(entry.name);
357
- const embRrf = embRank ? 1000 / (20 + embRank) : 0;
358
- fusedScores.set(entry.name, bm25Rrf + embRrf);
601
+ const bm25Rrf = 1000 / (K + i + 1);
602
+ // Direct tool embedding match
603
+ const tRank = toolRanks.get(entry.name);
604
+ const toolRrf = tRank ? ALPHA_T * 1000 / (K + tRank) : 0;
605
+ // Domain-level embedding match (upward traversal: tool → category → domain node)
606
+ const dRank = domainRanks.get(entry.category);
607
+ const domainRrf = dRank ? ALPHA_D * 1000 / (K + dRank) : 0;
608
+ fusedScores.set(entry.name, bm25Rrf + toolRrf + domainRrf);
359
609
  });
360
610
  // Also include embedding-only hits not in BM25 results
361
- for (const [name, rank] of vecRanks) {
611
+ for (const [name, rank] of toolRanks) {
362
612
  if (!fusedScores.has(name)) {
363
- const embRrf = 1000 / (20 + rank);
364
- fusedScores.set(name, embRrf);
613
+ const toolRrf = ALPHA_T * 1000 / (K + rank);
614
+ // Look up domain boost for this tool
615
+ const entry = REGISTRY.find((e) => e.name === name);
616
+ const dRank = entry ? domainRanks.get(entry.category) : undefined;
617
+ const domainRrf = dRank ? ALPHA_D * 1000 / (K + dRank) : 0;
618
+ fusedScores.set(name, toolRrf + domainRrf);
619
+ }
620
+ }
621
+ // Domain-only hits: boost all tools in a matched domain even without direct tool hit
622
+ for (const [category, dRank] of domainRanks) {
623
+ const domainRrf = ALPHA_D * 1000 / (K + dRank);
624
+ for (const entry of REGISTRY) {
625
+ if (entry.category === category && !fusedScores.has(entry.name)) {
626
+ fusedScores.set(entry.name, domainRrf);
627
+ }
365
628
  }
366
629
  }
367
630
  // Re-sort by fused score
@@ -0,0 +1,2 @@
1
+ import type { McpTool } from "../types.js";
2
+ export declare const transactionSafetyTools: McpTool[];
@@ -0,0 +1,166 @@
1
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { getDb, genId } from "../db.js";
4
+ import { getQuickRef } from "./toolRegistry.js";
5
+ // ── Helpers ──────────────────────────────────────────────────────────
6
+ function findConvexDir(projectDir) {
7
+ const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
8
+ for (const c of candidates) {
9
+ if (existsSync(c))
10
+ return c;
11
+ }
12
+ return null;
13
+ }
14
+ function collectTsFiles(dir) {
15
+ const results = [];
16
+ if (!existsSync(dir))
17
+ return results;
18
+ const entries = readdirSync(dir, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ const full = join(dir, entry.name);
21
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
22
+ results.push(...collectTsFiles(full));
23
+ }
24
+ else if (entry.isFile() && entry.name.endsWith(".ts")) {
25
+ results.push(full);
26
+ }
27
+ }
28
+ return results;
29
+ }
30
+ function auditTransactionSafety(convexDir) {
31
+ const files = collectTsFiles(convexDir);
32
+ const issues = [];
33
+ let totalMutations = 0;
34
+ let readModifyWrite = 0;
35
+ let multipleRunMutation = 0;
36
+ let checkThenAct = 0;
37
+ for (const filePath of files) {
38
+ const content = readFileSync(filePath, "utf-8");
39
+ const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
40
+ const lines = content.split("\n");
41
+ // Find action/mutation bodies
42
+ const funcPattern = /export\s+(?:const\s+(\w+)\s*=|default)\s+(action|internalAction|mutation|internalMutation)\s*\(/g;
43
+ let m;
44
+ while ((m = funcPattern.exec(content)) !== null) {
45
+ const funcName = m[1] || "default";
46
+ const funcType = m[2];
47
+ if (funcType === "mutation" || funcType === "internalMutation")
48
+ totalMutations++;
49
+ const startLine = content.slice(0, m.index).split("\n").length - 1;
50
+ // Extract body
51
+ let depth = 0;
52
+ let foundOpen = false;
53
+ let endLine = Math.min(startLine + 100, lines.length);
54
+ for (let j = startLine; j < lines.length; j++) {
55
+ for (const ch of lines[j]) {
56
+ if (ch === "{") {
57
+ depth++;
58
+ foundOpen = true;
59
+ }
60
+ if (ch === "}")
61
+ depth--;
62
+ }
63
+ if (foundOpen && depth <= 0) {
64
+ endLine = j + 1;
65
+ break;
66
+ }
67
+ }
68
+ const body = lines.slice(startLine, endLine).join("\n");
69
+ // Check 1: Multiple ctx.runMutation calls in an action (separate transactions)
70
+ if (funcType === "action" || funcType === "internalAction") {
71
+ const runMutationMatches = body.match(/ctx\.runMutation\s*\(/g);
72
+ if (runMutationMatches && runMutationMatches.length >= 2) {
73
+ multipleRunMutation++;
74
+ issues.push({
75
+ severity: "warning",
76
+ location: `${relativePath}:${startLine + 1}`,
77
+ functionName: funcName,
78
+ message: `${funcType} "${funcName}" calls ctx.runMutation ${runMutationMatches.length} times. Each is a separate transaction — if the second fails, the first is NOT rolled back.`,
79
+ fix: "Combine into a single mutation that does both operations atomically, or add idempotency handling",
80
+ });
81
+ }
82
+ }
83
+ // Check 2: Read-modify-write in action (TOCTOU race)
84
+ if (funcType === "action" || funcType === "internalAction") {
85
+ const hasRunQuery = /ctx\.runQuery\s*\(/.test(body);
86
+ const hasRunMutation = /ctx\.runMutation\s*\(/.test(body);
87
+ if (hasRunQuery && hasRunMutation) {
88
+ // Potential read-then-write pattern — check if they reference similar data
89
+ readModifyWrite++;
90
+ issues.push({
91
+ severity: "warning",
92
+ location: `${relativePath}:${startLine + 1}`,
93
+ functionName: funcName,
94
+ message: `${funcType} "${funcName}" reads via runQuery then writes via runMutation. Between these calls, another client may have changed the data (TOCTOU race).`,
95
+ fix: "Move the read-modify-write into a single mutation for atomicity, or add optimistic concurrency checks",
96
+ });
97
+ }
98
+ }
99
+ // Check 3: Check-then-act in mutations (get → check → modify)
100
+ if (funcType === "mutation" || funcType === "internalMutation") {
101
+ // Pattern: const x = await ctx.db.get(...); if (!x) throw; ctx.db.patch(x._id, ...)
102
+ const hasGet = /ctx\.db\.get\s*\(/.test(body);
103
+ const hasPatch = /ctx\.db\.(patch|replace|delete)\s*\(/.test(body);
104
+ const hasConditional = /if\s*\(\s*!?\w+/.test(body);
105
+ if (hasGet && hasPatch && hasConditional) {
106
+ // This is actually safe in Convex mutations (they're serializable), but flag for review
107
+ // Only flag if there are multiple get/patch patterns suggesting complexity
108
+ const getCount = (body.match(/ctx\.db\.get\s*\(/g) || []).length;
109
+ const patchCount = (body.match(/ctx\.db\.(patch|replace|delete)\s*\(/g) || []).length;
110
+ if (getCount >= 2 && patchCount >= 2) {
111
+ checkThenAct++;
112
+ issues.push({
113
+ severity: "info",
114
+ location: `${relativePath}:${startLine + 1}`,
115
+ functionName: funcName,
116
+ message: `${funcType} "${funcName}" has complex read-check-modify pattern (${getCount} reads, ${patchCount} writes). Convex mutations are serializable so this is safe, but consider simplifying.`,
117
+ fix: "Consider breaking into smaller, focused mutations if the logic is complex",
118
+ });
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ return {
125
+ issues,
126
+ stats: { totalMutations, readModifyWrite, multipleRunMutation, checkThenAct },
127
+ };
128
+ }
129
+ // ── Tool Definition ─────────────────────────────────────────────────
130
+ export const transactionSafetyTools = [
131
+ {
132
+ name: "convex_audit_transaction_safety",
133
+ description: "Audit Convex functions for transaction safety: multiple ctx.runMutation calls in actions (separate transactions — partial failure risk), read-then-write patterns across query/mutation boundaries (TOCTOU races), and complex check-then-act mutations.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ projectDir: {
138
+ type: "string",
139
+ description: "Absolute path to the project root containing a convex/ directory",
140
+ },
141
+ },
142
+ required: ["projectDir"],
143
+ },
144
+ handler: async (args) => {
145
+ const projectDir = resolve(args.projectDir);
146
+ const convexDir = findConvexDir(projectDir);
147
+ if (!convexDir) {
148
+ return { error: "No convex/ directory found" };
149
+ }
150
+ const { issues, stats } = auditTransactionSafety(convexDir);
151
+ const db = getDb();
152
+ db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "transaction_safety", JSON.stringify(issues), issues.length);
153
+ return {
154
+ summary: {
155
+ ...stats,
156
+ totalIssues: issues.length,
157
+ critical: issues.filter((i) => i.severity === "critical").length,
158
+ warnings: issues.filter((i) => i.severity === "warning").length,
159
+ },
160
+ issues: issues.slice(0, 30),
161
+ quickRef: getQuickRef("convex_audit_transaction_safety"),
162
+ };
163
+ },
164
+ },
165
+ ];
166
+ //# sourceMappingURL=transactionSafetyTools.js.map
@@ -0,0 +1,2 @@
1
+ import type { McpTool } from "../types.js";
2
+ export declare const typeSafetyTools: McpTool[];
@@ -0,0 +1,146 @@
1
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { getDb, genId } from "../db.js";
4
+ import { getQuickRef } from "./toolRegistry.js";
5
+ // ── Helpers ──────────────────────────────────────────────────────────
6
+ function findConvexDir(projectDir) {
7
+ const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
8
+ for (const c of candidates) {
9
+ if (existsSync(c))
10
+ return c;
11
+ }
12
+ return null;
13
+ }
14
+ function collectTsFiles(dir) {
15
+ const results = [];
16
+ if (!existsSync(dir))
17
+ return results;
18
+ const entries = readdirSync(dir, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ const full = join(dir, entry.name);
21
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
22
+ results.push(...collectTsFiles(full));
23
+ }
24
+ else if (entry.isFile() && entry.name.endsWith(".ts")) {
25
+ results.push(full);
26
+ }
27
+ }
28
+ return results;
29
+ }
30
+ function auditTypeSafety(convexDir) {
31
+ const files = collectTsFiles(convexDir);
32
+ const issues = [];
33
+ let filesWithAsAny = 0;
34
+ let asAnyCastCount = 0;
35
+ let undefinedReturns = 0;
36
+ let looseIdTypes = 0;
37
+ for (const filePath of files) {
38
+ const content = readFileSync(filePath, "utf-8");
39
+ const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
40
+ const lines = content.split("\n");
41
+ let fileHasAsAny = false;
42
+ for (let i = 0; i < lines.length; i++) {
43
+ const line = lines[i];
44
+ if (line.trim().startsWith("//") || line.trim().startsWith("*"))
45
+ continue;
46
+ // Check 1: `as any` casts
47
+ const asAnyMatches = line.match(/as\s+any\b/g);
48
+ if (asAnyMatches) {
49
+ asAnyCastCount += asAnyMatches.length;
50
+ if (!fileHasAsAny) {
51
+ fileHasAsAny = true;
52
+ filesWithAsAny++;
53
+ }
54
+ }
55
+ // Check 2: return undefined (should be null in Convex)
56
+ if (/return\s+undefined\b/.test(line)) {
57
+ undefinedReturns++;
58
+ issues.push({
59
+ severity: "warning",
60
+ location: `${relativePath}:${i + 1}`,
61
+ message: "Returning undefined — Convex serializes undefined differently from null. Use null explicitly.",
62
+ fix: "Replace `return undefined` with `return null`",
63
+ });
64
+ }
65
+ // Check 3: Using string type where Id<'table'> should be
66
+ // Heuristic: args with names like *Id that use v.string() instead of v.id("table")
67
+ const idArgMatch = line.match(/(\w+Id)\s*:\s*v\.string\s*\(\s*\)/);
68
+ if (idArgMatch) {
69
+ looseIdTypes++;
70
+ issues.push({
71
+ severity: "warning",
72
+ location: `${relativePath}:${i + 1}`,
73
+ message: `Arg "${idArgMatch[1]}" uses v.string() but looks like an ID field. Use v.id("tableName") for type-safe ID references.`,
74
+ fix: `Change ${idArgMatch[1]}: v.string() to ${idArgMatch[1]}: v.id("tableName")`,
75
+ });
76
+ }
77
+ // Check 4: Manual interface/type definitions that shadow generated types
78
+ if (/(?:interface|type)\s+(Doc|Id|DataModel|FunctionReference)\b/.test(line)) {
79
+ issues.push({
80
+ severity: "critical",
81
+ location: `${relativePath}:${i + 1}`,
82
+ message: "Manual type definition shadows Convex generated type. Import from _generated/dataModel instead.",
83
+ fix: 'Import from "_generated/dataModel" or "_generated/server" instead of defining manually',
84
+ });
85
+ }
86
+ }
87
+ // Aggregate `as any` per file
88
+ if (fileHasAsAny) {
89
+ const count = (content.match(/as\s+any\b/g) || []).length;
90
+ issues.push({
91
+ severity: "warning",
92
+ location: relativePath,
93
+ message: `${count} \`as any\` cast(s) in this file. Each cast bypasses Convex's automatic type safety.`,
94
+ fix: "Replace `as any` with proper typing or use Convex generated types",
95
+ });
96
+ }
97
+ }
98
+ return {
99
+ issues,
100
+ stats: {
101
+ totalFiles: files.length,
102
+ filesWithAsAny,
103
+ asAnyCastCount,
104
+ undefinedReturns,
105
+ looseIdTypes,
106
+ },
107
+ };
108
+ }
109
+ // ── Tool Definition ─────────────────────────────────────────────────
110
+ export const typeSafetyTools = [
111
+ {
112
+ name: "convex_check_type_safety",
113
+ description: "Check Convex code for type safety issues: `as any` casts (bypass generated types), returning undefined instead of null, v.string() where v.id() should be used, and manual type definitions shadowing Convex generated types.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ projectDir: {
118
+ type: "string",
119
+ description: "Absolute path to the project root containing a convex/ directory",
120
+ },
121
+ },
122
+ required: ["projectDir"],
123
+ },
124
+ handler: async (args) => {
125
+ const projectDir = resolve(args.projectDir);
126
+ const convexDir = findConvexDir(projectDir);
127
+ if (!convexDir) {
128
+ return { error: "No convex/ directory found" };
129
+ }
130
+ const { issues, stats } = auditTypeSafety(convexDir);
131
+ const db = getDb();
132
+ db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "type_safety", JSON.stringify(issues), issues.length);
133
+ return {
134
+ summary: {
135
+ ...stats,
136
+ totalIssues: issues.length,
137
+ critical: issues.filter((i) => i.severity === "critical").length,
138
+ warnings: issues.filter((i) => i.severity === "warning").length,
139
+ },
140
+ issues: issues.slice(0, 30),
141
+ quickRef: getQuickRef("convex_check_type_safety"),
142
+ };
143
+ },
144
+ },
145
+ ];
146
+ //# sourceMappingURL=typeSafetyTools.js.map
@@ -0,0 +1,2 @@
1
+ import type { McpTool } from "../types.js";
2
+ export declare const vectorSearchTools: McpTool[];