@homenshum/convex-mcp-nodebench 0.8.0 → 0.9.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
@@ -40,6 +40,7 @@ import { reportingTools } from "./tools/reportingTools.js";
40
40
  import { vectorSearchTools } from "./tools/vectorSearchTools.js";
41
41
  import { schedulerTools } from "./tools/schedulerTools.js";
42
42
  import { qualityGateTools } from "./tools/qualityGateTools.js";
43
+ import { architectTools } from "./tools/architectTools.js";
43
44
  import { CONVEX_GOTCHAS } from "./gotchaSeed.js";
44
45
  import { REGISTRY } from "./tools/toolRegistry.js";
45
46
  import { initEmbeddingIndex } from "./tools/embeddingProvider.js";
@@ -69,6 +70,7 @@ const ALL_TOOLS = [
69
70
  ...vectorSearchTools,
70
71
  ...schedulerTools,
71
72
  ...qualityGateTools,
73
+ ...architectTools,
72
74
  ];
73
75
  const toolMap = new Map();
74
76
  for (const tool of ALL_TOOLS) {
@@ -77,7 +79,7 @@ for (const tool of ALL_TOOLS) {
77
79
  // ── Server setup ────────────────────────────────────────────────────
78
80
  const server = new Server({
79
81
  name: "convex-mcp-nodebench",
80
- version: "0.8.0",
82
+ version: "0.9.0",
81
83
  }, {
82
84
  capabilities: {
83
85
  tools: {},
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Architect Tools — Structural code analysis for Convex projects.
3
+ *
4
+ * Convex-specific variant of the mcp-local architect tools. Instead of
5
+ * generic React/Express patterns, these scan for Convex function types,
6
+ * schema constructs, data access patterns, auth guards, storage usage,
7
+ * scheduler calls, and client-side hooks.
8
+ *
9
+ * 3 tools:
10
+ * - convex_scan_capabilities: Analyze a file for Convex structural patterns
11
+ * - convex_verify_concept: Check if a file has required code signatures
12
+ * - convex_generate_plan: Build a plan for missing signatures
13
+ */
14
+ import type { McpTool } from "../types.js";
15
+ export declare const architectTools: McpTool[];
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Architect Tools — Structural code analysis for Convex projects.
3
+ *
4
+ * Convex-specific variant of the mcp-local architect tools. Instead of
5
+ * generic React/Express patterns, these scan for Convex function types,
6
+ * schema constructs, data access patterns, auth guards, storage usage,
7
+ * scheduler calls, and client-side hooks.
8
+ *
9
+ * 3 tools:
10
+ * - convex_scan_capabilities: Analyze a file for Convex structural patterns
11
+ * - convex_verify_concept: Check if a file has required code signatures
12
+ * - convex_generate_plan: Build a plan for missing signatures
13
+ */
14
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+ import { getDb, genId } from "../db.js";
17
+ import { getQuickRef } from "./toolRegistry.js";
18
+ // ── DB setup ────────────────────────────────────────────────────────────────
19
+ function ensureConceptTable() {
20
+ const db = getDb();
21
+ db.exec(`
22
+ CREATE TABLE IF NOT EXISTS concept_verifications (
23
+ id TEXT PRIMARY KEY,
24
+ concept_name TEXT NOT NULL,
25
+ file_path TEXT NOT NULL,
26
+ status TEXT NOT NULL,
27
+ match_score REAL NOT NULL,
28
+ signatures_total INTEGER NOT NULL,
29
+ signatures_found INTEGER NOT NULL,
30
+ gap_list TEXT,
31
+ verified_at TEXT NOT NULL DEFAULT (datetime('now'))
32
+ )
33
+ `);
34
+ }
35
+ const FUNCTION_TYPE_PATTERNS = {
36
+ queries: { pattern: /(?:export\s+(?:const|default)\s+\w+\s*=\s*)?query\s*\(/g, count: true },
37
+ internal_queries: { pattern: /internalQuery\s*\(/g, count: true },
38
+ mutations: { pattern: /(?:export\s+(?:const|default)\s+\w+\s*=\s*)?mutation\s*\(/g, count: true },
39
+ internal_mutations: { pattern: /internalMutation\s*\(/g, count: true },
40
+ actions: { pattern: /(?:export\s+(?:const|default)\s+\w+\s*=\s*)?action\s*\(/g, count: true },
41
+ internal_actions: { pattern: /internalAction\s*\(/g, count: true },
42
+ http_actions: { pattern: /httpAction\s*\(/g, count: true },
43
+ cron_handlers: { pattern: /cronJobs\.|crons\./g, count: true },
44
+ };
45
+ const SCHEMA_PATTERNS = {
46
+ define_schema: { pattern: /defineSchema\s*\(/ },
47
+ define_table: { pattern: /defineTable\s*\(/g, count: true },
48
+ indexes: { pattern: /\.index\s*\(/g, count: true },
49
+ search_indexes: { pattern: /\.searchIndex\s*\(/g, count: true },
50
+ vector_indexes: { pattern: /\.vectorIndex\s*\(/g, count: true },
51
+ validators: { pattern: /v\.\w+\s*\(/g, count: true },
52
+ v_id_refs: { pattern: /v\.id\s*\(\s*["']/g, count: true },
53
+ v_optional: { pattern: /v\.optional\s*\(/g, count: true },
54
+ v_union: { pattern: /v\.union\s*\(/g, count: true },
55
+ };
56
+ const DATA_ACCESS_PATTERNS = {
57
+ db_query: { pattern: /ctx\.db\.query\s*\(/g, count: true },
58
+ db_get: { pattern: /ctx\.db\.get\s*\(/g, count: true },
59
+ db_insert: { pattern: /ctx\.db\.insert\s*\(/g, count: true },
60
+ db_patch: { pattern: /ctx\.db\.patch\s*\(/g, count: true },
61
+ db_replace: { pattern: /ctx\.db\.replace\s*\(/g, count: true },
62
+ db_delete: { pattern: /ctx\.db\.delete\s*\(/g, count: true },
63
+ collect: { pattern: /\.collect\s*\(/g, count: true },
64
+ first: { pattern: /\.first\s*\(/g, count: true },
65
+ unique: { pattern: /\.unique\s*\(/g, count: true },
66
+ paginate: { pattern: /\.paginate\s*\(/g, count: true },
67
+ with_index: { pattern: /\.withIndex\s*\(/g, count: true },
68
+ with_search_index: { pattern: /\.withSearchIndex\s*\(/g, count: true },
69
+ filter: { pattern: /\.filter\s*\(/g, count: true },
70
+ order: { pattern: /\.order\s*\(/g, count: true },
71
+ };
72
+ const AUTH_AND_CONTEXT_PATTERNS = {
73
+ get_user_identity: { pattern: /ctx\.auth\.getUserIdentity\s*\(/g, count: true },
74
+ identity_check: { pattern: /if\s*\(\s*!?\s*identity|if\s*\(\s*!?\s*user/g, count: true },
75
+ run_mutation: { pattern: /ctx\.runMutation\s*\(/g, count: true },
76
+ run_query: { pattern: /ctx\.runQuery\s*\(/g, count: true },
77
+ run_action: { pattern: /ctx\.runAction\s*\(/g, count: true },
78
+ scheduler_run_after: { pattern: /ctx\.scheduler\.runAfter\s*\(/g, count: true },
79
+ scheduler_run_at: { pattern: /ctx\.scheduler\.runAt\s*\(/g, count: true },
80
+ };
81
+ const STORAGE_PATTERNS = {
82
+ storage_store: { pattern: /ctx\.storage\.store\s*\(/g, count: true },
83
+ storage_get: { pattern: /ctx\.storage\.get\s*\(/g, count: true },
84
+ storage_get_url: { pattern: /ctx\.storage\.getUrl\s*\(/g, count: true },
85
+ storage_delete: { pattern: /ctx\.storage\.delete\s*\(/g, count: true },
86
+ generate_upload_url: { pattern: /ctx\.storage\.generateUploadUrl\s*\(/g, count: true },
87
+ };
88
+ const CLIENT_PATTERNS = {
89
+ use_query: { pattern: /useQuery\s*\(/g, count: true },
90
+ use_mutation: { pattern: /useMutation\s*\(/g, count: true },
91
+ use_action: { pattern: /useAction\s*\(/g, count: true },
92
+ use_convex: { pattern: /useConvex\s*\(/ },
93
+ convex_provider: { pattern: /ConvexProvider|ConvexReactClient/ },
94
+ convex_react_import: { pattern: /from\s+["']convex\/react["']/ },
95
+ api_import: { pattern: /from\s+["']\.\.?\/_generated\/api["']/ },
96
+ };
97
+ function analyzePatterns(content, patterns) {
98
+ const result = {};
99
+ for (const [key, { pattern, count }] of Object.entries(patterns)) {
100
+ // Reset lastIndex for global regexes
101
+ pattern.lastIndex = 0;
102
+ if (count) {
103
+ const matches = content.match(pattern);
104
+ result[key] = matches ? matches.length : 0;
105
+ }
106
+ else {
107
+ result[key] = pattern.test(content);
108
+ pattern.lastIndex = 0; // Reset after test
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+ // ── Helpers ──────────────────────────────────────────────────────────
114
+ function findConvexDir(projectDir) {
115
+ const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
116
+ for (const c of candidates) {
117
+ if (existsSync(c))
118
+ return c;
119
+ }
120
+ return null;
121
+ }
122
+ function collectTsFiles(dir) {
123
+ const results = [];
124
+ if (!existsSync(dir))
125
+ return results;
126
+ const entries = readdirSync(dir, { withFileTypes: true });
127
+ for (const entry of entries) {
128
+ const full = join(dir, entry.name);
129
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "_generated") {
130
+ results.push(...collectTsFiles(full));
131
+ }
132
+ else if (entry.isFile() && entry.name.endsWith(".ts")) {
133
+ results.push(full);
134
+ }
135
+ }
136
+ return results;
137
+ }
138
+ // ── Tools ──────────────────────────────────────────────────────────────────
139
+ export const architectTools = [
140
+ {
141
+ name: "convex_scan_capabilities",
142
+ description: "Analyze Convex source files for structural patterns. Scans a single file or entire convex/ directory and returns a capability report: function types (queries, mutations, actions, crons, http), schema constructs (tables, indexes, validators), data access (query, get, insert, patch, delete, collect, paginate), auth/context (getUserIdentity, runMutation, scheduler), storage (store, getUrl, delete), and client-side hooks (useQuery, useMutation, useAction). Pure regex — no LLM needed.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ projectDir: {
147
+ type: "string",
148
+ description: "Absolute path to the project root containing a convex/ directory. If provided, scans all .ts files in convex/.",
149
+ },
150
+ filePath: {
151
+ type: "string",
152
+ description: "Absolute path to a single file to analyze. Takes priority over projectDir.",
153
+ },
154
+ },
155
+ required: [],
156
+ },
157
+ handler: async (args) => {
158
+ if (args.filePath) {
159
+ // Single file mode
160
+ if (!existsSync(args.filePath)) {
161
+ return { error: `File not found: ${args.filePath}` };
162
+ }
163
+ const content = readFileSync(args.filePath, "utf-8");
164
+ const lines = content.split("\n").length;
165
+ return {
166
+ mode: "single_file",
167
+ file: {
168
+ path: args.filePath,
169
+ lines,
170
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
171
+ },
172
+ function_types: analyzePatterns(content, FUNCTION_TYPE_PATTERNS),
173
+ schema_constructs: analyzePatterns(content, SCHEMA_PATTERNS),
174
+ data_access: analyzePatterns(content, DATA_ACCESS_PATTERNS),
175
+ auth_and_context: analyzePatterns(content, AUTH_AND_CONTEXT_PATTERNS),
176
+ storage: analyzePatterns(content, STORAGE_PATTERNS),
177
+ client_side: analyzePatterns(content, CLIENT_PATTERNS),
178
+ imports: {
179
+ count: (content.match(/^import /gm) || []).length,
180
+ has_convex_server: /from\s+["']convex\/server["']/.test(content),
181
+ has_convex_values: /from\s+["']convex\/values["']/.test(content),
182
+ has_convex_react: /from\s+["']convex\/react["']/.test(content),
183
+ has_generated_api: /from\s+["']\.\.?\/_generated\/api["']/.test(content),
184
+ has_generated_server: /from\s+["']\.\.?\/_generated\/server["']/.test(content),
185
+ },
186
+ exports: {
187
+ default_export: /export default/.test(content),
188
+ named_exports: (content.match(/^export (?:const|function|class|type|interface)/gm) || []).length,
189
+ },
190
+ quickRef: getQuickRef("convex_scan_capabilities"),
191
+ };
192
+ }
193
+ // Directory mode — scan all convex/ files
194
+ if (!args.projectDir) {
195
+ return { error: "Provide either projectDir or filePath" };
196
+ }
197
+ const projectDir = resolve(args.projectDir);
198
+ const convexDir = findConvexDir(projectDir);
199
+ if (!convexDir) {
200
+ return { error: "No convex/ directory found" };
201
+ }
202
+ const files = collectTsFiles(convexDir);
203
+ const aggregate = {
204
+ function_types: {},
205
+ schema_constructs: {},
206
+ data_access: {},
207
+ auth_and_context: {},
208
+ storage: {},
209
+ };
210
+ const fileCapabilities = [];
211
+ for (const filePath of files) {
212
+ const content = readFileSync(filePath, "utf-8");
213
+ const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
214
+ const lines = content.split("\n").length;
215
+ const ft = analyzePatterns(content, FUNCTION_TYPE_PATTERNS);
216
+ const da = analyzePatterns(content, DATA_ACCESS_PATTERNS);
217
+ // Aggregate counts
218
+ for (const [category, patterns] of [
219
+ ["function_types", FUNCTION_TYPE_PATTERNS],
220
+ ["schema_constructs", SCHEMA_PATTERNS],
221
+ ["data_access", DATA_ACCESS_PATTERNS],
222
+ ["auth_and_context", AUTH_AND_CONTEXT_PATTERNS],
223
+ ["storage", STORAGE_PATTERNS],
224
+ ]) {
225
+ const results = analyzePatterns(content, patterns);
226
+ for (const [key, val] of Object.entries(results)) {
227
+ if (typeof val === "number") {
228
+ aggregate[category][key] = (aggregate[category][key] ?? 0) + val;
229
+ }
230
+ else if (val === true) {
231
+ aggregate[category][key] = (aggregate[category][key] ?? 0) + 1;
232
+ }
233
+ }
234
+ }
235
+ const ftTotal = Object.values(ft).reduce((s, v) => s + (typeof v === "number" ? v : v ? 1 : 0), 0);
236
+ const daTotal = Object.values(da).reduce((s, v) => s + (typeof v === "number" ? v : v ? 1 : 0), 0);
237
+ if (ftTotal > 0 || daTotal > 0) {
238
+ fileCapabilities.push({
239
+ file: relativePath,
240
+ lines,
241
+ functionTypes: ftTotal,
242
+ dataAccess: daTotal,
243
+ });
244
+ }
245
+ }
246
+ // Sort by most active files
247
+ fileCapabilities.sort((a, b) => (b.functionTypes + b.dataAccess) - (a.functionTypes + a.dataAccess));
248
+ return {
249
+ mode: "directory",
250
+ convexDir,
251
+ totalFiles: files.length,
252
+ activeFiles: fileCapabilities.length,
253
+ aggregate,
254
+ topFiles: fileCapabilities.slice(0, 20),
255
+ quickRef: getQuickRef("convex_scan_capabilities"),
256
+ };
257
+ },
258
+ },
259
+ {
260
+ name: "convex_verify_concept",
261
+ description: "Check if Convex source files contain all required code signatures for a concept. Provide a concept name (e.g., 'Real-time Subscriptions', 'Vector Search RAG', 'File Upload Pipeline') and regex patterns that MUST exist. Returns match score (0-100%), status, evidence, and gap analysis. Persisted to SQLite for tracking progress. Works on a single file or scans the entire convex/ directory.",
262
+ inputSchema: {
263
+ type: "object",
264
+ properties: {
265
+ projectDir: {
266
+ type: "string",
267
+ description: "Absolute path to the project root. Scans all convex/ files.",
268
+ },
269
+ filePath: {
270
+ type: "string",
271
+ description: "Path to a single file to verify. Takes priority over projectDir.",
272
+ },
273
+ concept_name: {
274
+ type: "string",
275
+ description: 'The Convex feature/concept to verify (e.g., "Vector Search RAG", "Real-time Pagination", "Scheduled Retry Queue")',
276
+ },
277
+ required_signatures: {
278
+ type: "array",
279
+ items: { type: "string" },
280
+ description: 'Regex patterns that MUST exist for the concept. E.g., ["vectorIndex", "ctx\\.db\\.query.*withSearchIndex", "v\\.array.*v\\.float64"]',
281
+ },
282
+ },
283
+ required: ["concept_name", "required_signatures"],
284
+ },
285
+ handler: async (args) => {
286
+ ensureConceptTable();
287
+ // Gather content
288
+ let content;
289
+ let sourcePath;
290
+ if (args.filePath) {
291
+ if (!existsSync(args.filePath)) {
292
+ return { error: `File not found: ${args.filePath}` };
293
+ }
294
+ content = readFileSync(args.filePath, "utf-8");
295
+ sourcePath = args.filePath;
296
+ }
297
+ else if (args.projectDir) {
298
+ const projectDir = resolve(args.projectDir);
299
+ const convexDir = findConvexDir(projectDir);
300
+ if (!convexDir) {
301
+ return { error: "No convex/ directory found" };
302
+ }
303
+ const files = collectTsFiles(convexDir);
304
+ content = files.map(f => readFileSync(f, "utf-8")).join("\n// ── file boundary ──\n");
305
+ sourcePath = convexDir;
306
+ }
307
+ else {
308
+ return { error: "Provide either projectDir or filePath" };
309
+ }
310
+ const found = [];
311
+ const missing = [];
312
+ for (const sig of args.required_signatures) {
313
+ try {
314
+ const regex = new RegExp(sig, "i");
315
+ const match = content.match(regex);
316
+ if (match) {
317
+ // Extract surrounding context for evidence
318
+ const idx = content.indexOf(match[0]);
319
+ const lineNum = content.slice(0, idx).split("\n").length;
320
+ const line = content.split("\n")[lineNum - 1]?.trim() || match[0];
321
+ found.push({
322
+ signature: sig,
323
+ evidence: `Line ${lineNum}: ${line.slice(0, 120)}`,
324
+ });
325
+ }
326
+ else {
327
+ missing.push(sig);
328
+ }
329
+ }
330
+ catch {
331
+ // Invalid regex — try literal search
332
+ if (content.toLowerCase().includes(sig.toLowerCase())) {
333
+ found.push({ signature: sig, evidence: "(literal match)" });
334
+ }
335
+ else {
336
+ missing.push(sig);
337
+ }
338
+ }
339
+ }
340
+ const score = args.required_signatures.length > 0
341
+ ? Math.round((found.length / args.required_signatures.length) * 100)
342
+ : 0;
343
+ const status = score === 100
344
+ ? "Fully Implemented"
345
+ : score > 50
346
+ ? "Partially Implemented"
347
+ : "Not Implemented";
348
+ // Persist
349
+ const id = genId("cv");
350
+ const db = getDb();
351
+ db.prepare(`INSERT INTO concept_verifications (id, concept_name, file_path, status, match_score, signatures_total, signatures_found, gap_list)
352
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, args.concept_name, sourcePath, status, score, args.required_signatures.length, found.length, JSON.stringify(missing));
353
+ return {
354
+ id,
355
+ concept: args.concept_name,
356
+ source: sourcePath,
357
+ status,
358
+ match_score: `${score}%`,
359
+ signatures_total: args.required_signatures.length,
360
+ evidence_found: found,
361
+ gap_analysis: missing,
362
+ recommendation: missing.length === 0
363
+ ? "All required signatures found. Concept is fully implemented in your Convex project."
364
+ : missing.length <= 2
365
+ ? `Nearly there — ${missing.length} signature(s) missing: ${missing.join(", ")}`
366
+ : `${missing.length} of ${args.required_signatures.length} signatures missing. Major implementation work needed.`,
367
+ quickRef: getQuickRef("convex_verify_concept"),
368
+ };
369
+ },
370
+ },
371
+ {
372
+ name: "convex_generate_plan",
373
+ description: "Generate a Convex-specific implementation plan for missing code signatures. Takes the gap analysis from convex_verify_concept and produces step-by-step instructions with Convex-aware injection strategies (schema changes, function registration, auth guards, index creation, etc.).",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ concept_name: {
378
+ type: "string",
379
+ description: "The Convex concept being implemented",
380
+ },
381
+ missing_signatures: {
382
+ type: "array",
383
+ items: { type: "string" },
384
+ description: "List of missing regex patterns from convex_verify_concept gap_analysis",
385
+ },
386
+ current_context: {
387
+ type: "string",
388
+ description: "Brief description or JSON of current capabilities (from convex_scan_capabilities). Helps avoid conflicts.",
389
+ },
390
+ target_file: {
391
+ type: "string",
392
+ description: "Path to the target file for changes (optional)",
393
+ },
394
+ },
395
+ required: ["concept_name", "missing_signatures"],
396
+ },
397
+ handler: async (args) => {
398
+ const steps = args.missing_signatures.map((sig, i) => ({
399
+ step: i + 1,
400
+ requirement: sig,
401
+ description: `Inject pattern matching: ${sig}`,
402
+ strategy: inferConvexStrategy(sig),
403
+ convex_file_hint: inferTargetFile(sig),
404
+ conflicts: args.current_context
405
+ ? `Review current context for overlap with: ${sig}`
406
+ : "No context provided — run convex_scan_capabilities first for conflict detection",
407
+ }));
408
+ return {
409
+ concept: args.concept_name,
410
+ target_file: args.target_file || "(not specified)",
411
+ total_steps: steps.length,
412
+ estimated_complexity: steps.length <= 2 ? "low" : steps.length <= 5 ? "medium" : "high",
413
+ context_provided: !!args.current_context,
414
+ steps,
415
+ workflow: [
416
+ "1. Run convex_scan_capabilities on the project (if not already done)",
417
+ "2. Review each step below and implement in order",
418
+ "3. After implementation, run convex_verify_concept to track progress",
419
+ "4. When all signatures match, run the relevant audit tools to validate quality",
420
+ "5. Run convex_quality_gate before deploying",
421
+ ],
422
+ quickRef: getQuickRef("convex_generate_plan"),
423
+ };
424
+ },
425
+ },
426
+ ];
427
+ // ── Convex-specific strategy inference ──────────────────────────────────
428
+ function inferConvexStrategy(signature) {
429
+ const lower = signature.toLowerCase();
430
+ // Schema patterns
431
+ if (/definetable|defineschema/i.test(lower))
432
+ return "Add to schema.ts — define table with defineTable() inside defineSchema()";
433
+ if (/vectorindex/i.test(lower))
434
+ return "Add .vectorIndex() to table definition in schema.ts — specify dimensions (must match embedding model) and filterFields";
435
+ if (/searchindex/i.test(lower))
436
+ return "Add .searchIndex() to table definition in schema.ts — specify searchField and filterFields";
437
+ if (/\.index\b/i.test(lower))
438
+ return "Add .index() to table definition in schema.ts — field order matters for query efficiency (equality fields first, then range)";
439
+ if (/v\.id\b/i.test(lower))
440
+ return "Use v.id('tableName') for foreign key references — ensures referential type safety";
441
+ if (/v\.float64|v\.array.*float64/i.test(lower))
442
+ return "Use v.array(v.float64()) for vector embedding fields — NOT v.array(v.number())";
443
+ if (/v\.optional/i.test(lower))
444
+ return "Wrap optional fields with v.optional() — avoids runtime errors on missing fields";
445
+ if (/v\.union/i.test(lower))
446
+ return "Use v.union() for discriminated types — add a 'type' field as discriminator for type safety";
447
+ // Function types
448
+ if (/internalmutation/i.test(lower))
449
+ return "Create internalMutation — only callable by other server functions, not from client. Import from _generated/server";
450
+ if (/internalaction/i.test(lower))
451
+ return "Create internalAction — for server-side-only work like API calls. Cannot access ctx.db directly, use ctx.runMutation/ctx.runQuery";
452
+ if (/internalquery/i.test(lower))
453
+ return "Create internalQuery — server-side-only reads. Not subscribable from client";
454
+ if (/httpaction/i.test(lower))
455
+ return "Create httpAction in http.ts — register with httpRouter.route({ method, path, handler })";
456
+ if (/mutation\b/i.test(lower))
457
+ return "Create public mutation — add args validator, return validator, and auth check (ctx.auth.getUserIdentity)";
458
+ if (/action\b/i.test(lower))
459
+ return "Create public action — for external API calls. Wrap in try/catch, use ctx.runMutation for DB writes";
460
+ if (/query\b/i.test(lower))
461
+ return "Create public query — reactive subscription from client. Must be deterministic, no side effects";
462
+ // Data access
463
+ if (/ctx\.db\.query/i.test(lower))
464
+ return "Add ctx.db.query('table') — chain .withIndex() for indexed lookups (avoid full-table .filter())";
465
+ if (/withindex/i.test(lower))
466
+ return "Use .withIndex('indexName', q => q.eq('field', value)) — ensure index exists in schema.ts first";
467
+ if (/withsearchindex/i.test(lower))
468
+ return "Use .withSearchIndex('indexName', q => q.search('field', searchText)) — ensure .searchIndex() defined";
469
+ if (/\.collect\b/i.test(lower))
470
+ return "Add .collect() to get all results — WARNING: add .take(limit) for bounded queries to avoid scanning entire table";
471
+ if (/\.paginate\b/i.test(lower))
472
+ return "Use .paginate(opts) with paginationOptsValidator in args — return { page, isDone, continueCursor }";
473
+ if (/ctx\.db\.insert/i.test(lower))
474
+ return "Use ctx.db.insert('table', { ...fields }) — within a mutation for transactional guarantee";
475
+ if (/ctx\.db\.patch/i.test(lower))
476
+ return "Use ctx.db.patch(id, { ...fields }) — partial update, only specified fields change";
477
+ if (/ctx\.db\.delete/i.test(lower))
478
+ return "Use ctx.db.delete(id) — remember to clean up related records and storage files";
479
+ // Auth
480
+ if (/getuseridentity/i.test(lower))
481
+ return "Add `const identity = await ctx.auth.getUserIdentity()` — throw if null for protected endpoints";
482
+ if (/identity.*check|if.*identity/i.test(lower))
483
+ return "Add identity null check after getUserIdentity() — return 401/throw for unauthenticated users";
484
+ // Scheduler
485
+ if (/scheduler.*runafter/i.test(lower))
486
+ return "Add ctx.scheduler.runAfter(delayMs, api.module.functionName, args) — use at least 1s delay for retries, add termination condition for loops";
487
+ if (/scheduler.*runat/i.test(lower))
488
+ return "Add ctx.scheduler.runAt(timestamp, api.module.functionName, args) — use for one-time future execution";
489
+ // Storage
490
+ if (/storage.*store/i.test(lower))
491
+ return "Use ctx.storage.store(blob) — returns storageId, save it to a document for later retrieval";
492
+ if (/storage.*geturl/i.test(lower))
493
+ return "Use ctx.storage.getUrl(storageId) — returns URL or null, always null-check the result";
494
+ if (/storage.*generateuploadurl/i.test(lower))
495
+ return "Use ctx.storage.generateUploadUrl() in a mutation — return URL to client for direct upload";
496
+ // Client-side
497
+ if (/usequery/i.test(lower))
498
+ return "Add useQuery(api.module.queryName, args) — reactive subscription, re-renders on data change";
499
+ if (/usemutation/i.test(lower))
500
+ return "Add useMutation(api.module.mutationName) — returns async function to call mutation";
501
+ if (/useaction/i.test(lower))
502
+ return "Add useAction(api.module.actionName) — returns async function, use for external API calls";
503
+ if (/convexprovider|convexreactclient/i.test(lower))
504
+ return "Wrap app with <ConvexProvider client={convex}> — create client with new ConvexReactClient(url)";
505
+ // Cross-function calls
506
+ if (/runmutation|runquery|runaction/i.test(lower))
507
+ return "Use ctx.runMutation/ctx.runQuery/ctx.runAction for cross-function calls — prefer internal functions for server-to-server";
508
+ return "Inject this pattern into the appropriate Convex file — check existing patterns with convex_scan_capabilities first";
509
+ }
510
+ function inferTargetFile(signature) {
511
+ const lower = signature.toLowerCase();
512
+ if (/definetable|defineschema|vectorindex|searchindex|\.index\b|v\.\w+/i.test(lower))
513
+ return "convex/schema.ts";
514
+ if (/httpaction|httprouter/i.test(lower))
515
+ return "convex/http.ts";
516
+ if (/cronjobs|crons\./i.test(lower))
517
+ return "convex/crons.ts";
518
+ if (/usequery|usemutation|useaction|convexprovider/i.test(lower))
519
+ return "src/ (React component file)";
520
+ if (/ctx\.auth|getuseridentity/i.test(lower))
521
+ return "convex/ (mutation or action file)";
522
+ if (/ctx\.storage/i.test(lower))
523
+ return "convex/ (mutation or action handling file uploads)";
524
+ return "convex/ (appropriate module file)";
525
+ }
526
+ //# sourceMappingURL=architectTools.js.map
@@ -75,7 +75,7 @@ function buildSarif(projectDir, auditTypes, limit) {
75
75
  tool: {
76
76
  driver: {
77
77
  name: "convex-mcp-nodebench",
78
- version: "0.8.0",
78
+ version: "0.9.0",
79
79
  informationUri: "https://www.npmjs.com/package/@homenshum/convex-mcp-nodebench",
80
80
  rules: [...rulesMap.values()],
81
81
  },
@@ -485,6 +485,49 @@ export const REGISTRY = [
485
485
  phase: "deploy",
486
486
  complexity: "high",
487
487
  },
488
+ // ── Architect Tools ──────────────────────
489
+ {
490
+ name: "convex_scan_capabilities",
491
+ category: "architect",
492
+ tags: ["scan", "capabilities", "structure", "patterns", "analysis", "functions", "schema", "data-access", "regex"],
493
+ quickRef: {
494
+ nextAction: "Use the capability report to identify what patterns exist before implementing new features",
495
+ nextTools: ["convex_verify_concept", "convex_generate_plan"],
496
+ methodology: "convex_schema_audit",
497
+ relatedGotchas: [],
498
+ confidence: "high",
499
+ },
500
+ phase: "audit",
501
+ complexity: "low",
502
+ },
503
+ {
504
+ name: "convex_verify_concept",
505
+ category: "architect",
506
+ tags: ["verify", "concept", "signatures", "gap-analysis", "implementation", "check", "progress", "regex"],
507
+ quickRef: {
508
+ nextAction: "Pass missing signatures to convex_generate_plan to get Convex-specific implementation steps",
509
+ nextTools: ["convex_generate_plan", "convex_scan_capabilities"],
510
+ methodology: "convex_schema_audit",
511
+ relatedGotchas: [],
512
+ confidence: "high",
513
+ },
514
+ phase: "audit",
515
+ complexity: "low",
516
+ },
517
+ {
518
+ name: "convex_generate_plan",
519
+ category: "architect",
520
+ tags: ["plan", "implementation", "strategy", "missing", "signatures", "steps", "inject", "convex-specific"],
521
+ quickRef: {
522
+ nextAction: "Implement each step in order, then re-verify with convex_verify_concept to track progress",
523
+ nextTools: ["convex_verify_concept", "convex_quality_gate"],
524
+ methodology: "convex_function_compliance",
525
+ relatedGotchas: [],
526
+ confidence: "high",
527
+ },
528
+ phase: "implement",
529
+ complexity: "low",
530
+ },
488
531
  ];
489
532
  export function getQuickRef(toolName) {
490
533
  const entry = REGISTRY.find((e) => e.name === toolName);
package/dist/types.d.ts CHANGED
@@ -13,7 +13,7 @@ export interface ConvexQuickRef {
13
13
  }
14
14
  export interface ToolRegistryEntry {
15
15
  name: string;
16
- category: "schema" | "function" | "deployment" | "learning" | "methodology" | "integration";
16
+ category: "schema" | "function" | "deployment" | "learning" | "methodology" | "integration" | "architect";
17
17
  tags: string[];
18
18
  quickRef: ConvexQuickRef;
19
19
  phase: "audit" | "implement" | "test" | "deploy" | "learn" | "meta";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homenshum/convex-mcp-nodebench",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": {