@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.
- package/dist/index.js +293 -4
- package/dist/tools/actionAuditTools.d.ts +2 -0
- package/dist/tools/actionAuditTools.js +180 -0
- package/dist/tools/authorizationTools.d.ts +2 -0
- package/dist/tools/authorizationTools.js +201 -0
- package/dist/tools/dataModelingTools.d.ts +2 -0
- package/dist/tools/dataModelingTools.js +168 -0
- package/dist/tools/deploymentTools.js +42 -2
- package/dist/tools/devSetupTools.d.ts +2 -0
- package/dist/tools/devSetupTools.js +170 -0
- package/dist/tools/embeddingProvider.d.ts +6 -0
- package/dist/tools/embeddingProvider.js +3 -0
- package/dist/tools/functionTools.js +24 -1
- package/dist/tools/httpTools.js +128 -48
- package/dist/tools/migrationTools.d.ts +2 -0
- package/dist/tools/migrationTools.js +133 -0
- package/dist/tools/paginationTools.d.ts +2 -0
- package/dist/tools/paginationTools.js +125 -0
- package/dist/tools/qualityGateTools.d.ts +2 -0
- package/dist/tools/qualityGateTools.js +204 -0
- package/dist/tools/queryEfficiencyTools.d.ts +2 -0
- package/dist/tools/queryEfficiencyTools.js +191 -0
- package/dist/tools/reportingTools.d.ts +2 -0
- package/dist/tools/reportingTools.js +240 -0
- package/dist/tools/schedulerTools.d.ts +2 -0
- package/dist/tools/schedulerTools.js +197 -0
- package/dist/tools/schemaTools.js +95 -1
- package/dist/tools/storageAuditTools.d.ts +2 -0
- package/dist/tools/storageAuditTools.js +148 -0
- package/dist/tools/toolRegistry.d.ts +4 -0
- package/dist/tools/toolRegistry.js +274 -11
- package/dist/tools/transactionSafetyTools.d.ts +2 -0
- package/dist/tools/transactionSafetyTools.js +166 -0
- package/dist/tools/typeSafetyTools.d.ts +2 -0
- package/dist/tools/typeSafetyTools.js +146 -0
- package/dist/tools/vectorSearchTools.d.ts +2 -0
- package/dist/tools/vectorSearchTools.js +192 -0
- package/dist/types.d.ts +6 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
16
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
-
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
18
|
import { getDb, seedGotchasIfEmpty } from "./db.js";
|
|
19
19
|
import { schemaTools } from "./tools/schemaTools.js";
|
|
20
20
|
import { functionTools } from "./tools/functionTools.js";
|
|
@@ -26,6 +26,20 @@ import { cronTools } from "./tools/cronTools.js";
|
|
|
26
26
|
import { componentTools } from "./tools/componentTools.js";
|
|
27
27
|
import { httpTools } from "./tools/httpTools.js";
|
|
28
28
|
import { critterTools } from "./tools/critterTools.js";
|
|
29
|
+
import { authorizationTools } from "./tools/authorizationTools.js";
|
|
30
|
+
import { queryEfficiencyTools } from "./tools/queryEfficiencyTools.js";
|
|
31
|
+
import { actionAuditTools } from "./tools/actionAuditTools.js";
|
|
32
|
+
import { typeSafetyTools } from "./tools/typeSafetyTools.js";
|
|
33
|
+
import { transactionSafetyTools } from "./tools/transactionSafetyTools.js";
|
|
34
|
+
import { storageAuditTools } from "./tools/storageAuditTools.js";
|
|
35
|
+
import { paginationTools } from "./tools/paginationTools.js";
|
|
36
|
+
import { dataModelingTools } from "./tools/dataModelingTools.js";
|
|
37
|
+
import { devSetupTools } from "./tools/devSetupTools.js";
|
|
38
|
+
import { migrationTools } from "./tools/migrationTools.js";
|
|
39
|
+
import { reportingTools } from "./tools/reportingTools.js";
|
|
40
|
+
import { vectorSearchTools } from "./tools/vectorSearchTools.js";
|
|
41
|
+
import { schedulerTools } from "./tools/schedulerTools.js";
|
|
42
|
+
import { qualityGateTools } from "./tools/qualityGateTools.js";
|
|
29
43
|
import { CONVEX_GOTCHAS } from "./gotchaSeed.js";
|
|
30
44
|
import { REGISTRY } from "./tools/toolRegistry.js";
|
|
31
45
|
import { initEmbeddingIndex } from "./tools/embeddingProvider.js";
|
|
@@ -41,6 +55,20 @@ const ALL_TOOLS = [
|
|
|
41
55
|
...componentTools,
|
|
42
56
|
...httpTools,
|
|
43
57
|
...critterTools,
|
|
58
|
+
...authorizationTools,
|
|
59
|
+
...queryEfficiencyTools,
|
|
60
|
+
...actionAuditTools,
|
|
61
|
+
...typeSafetyTools,
|
|
62
|
+
...transactionSafetyTools,
|
|
63
|
+
...storageAuditTools,
|
|
64
|
+
...paginationTools,
|
|
65
|
+
...dataModelingTools,
|
|
66
|
+
...devSetupTools,
|
|
67
|
+
...migrationTools,
|
|
68
|
+
...reportingTools,
|
|
69
|
+
...vectorSearchTools,
|
|
70
|
+
...schedulerTools,
|
|
71
|
+
...qualityGateTools,
|
|
44
72
|
];
|
|
45
73
|
const toolMap = new Map();
|
|
46
74
|
for (const tool of ALL_TOOLS) {
|
|
@@ -49,20 +77,52 @@ for (const tool of ALL_TOOLS) {
|
|
|
49
77
|
// ── Server setup ────────────────────────────────────────────────────
|
|
50
78
|
const server = new Server({
|
|
51
79
|
name: "convex-mcp-nodebench",
|
|
52
|
-
version: "0.
|
|
80
|
+
version: "0.8.0",
|
|
53
81
|
}, {
|
|
54
82
|
capabilities: {
|
|
55
83
|
tools: {},
|
|
84
|
+
resources: {},
|
|
85
|
+
prompts: {},
|
|
56
86
|
},
|
|
57
87
|
});
|
|
58
88
|
// ── Initialize DB + seed gotchas ────────────────────────────────────
|
|
59
89
|
getDb();
|
|
60
90
|
seedGotchasIfEmpty(CONVEX_GOTCHAS);
|
|
61
91
|
// ── Background: initialize embedding index for semantic search ───────
|
|
62
|
-
|
|
92
|
+
// Uses Agent-as-a-Graph bipartite corpus: tool nodes + domain nodes for graph-aware retrieval
|
|
93
|
+
const descMap = new Map(ALL_TOOLS.map((t) => [t.name, t.description]));
|
|
94
|
+
// Tool nodes: individual tools with full metadata text
|
|
95
|
+
const toolCorpus = REGISTRY.map((entry) => ({
|
|
63
96
|
name: entry.name,
|
|
64
|
-
text: `${entry.name} ${entry.tags.join(" ")} ${entry.category} ${entry.phase}`,
|
|
97
|
+
text: `${entry.name} ${entry.tags.join(" ")} ${entry.category} ${entry.phase} ${descMap.get(entry.name) ?? ""}`,
|
|
98
|
+
nodeType: "tool",
|
|
65
99
|
}));
|
|
100
|
+
// Domain nodes: aggregate category descriptions for upward traversal
|
|
101
|
+
// When a domain matches, all tools in that domain get a sibling boost
|
|
102
|
+
const categoryTools = new Map();
|
|
103
|
+
for (const entry of REGISTRY) {
|
|
104
|
+
const list = categoryTools.get(entry.category) ?? [];
|
|
105
|
+
list.push(entry.name);
|
|
106
|
+
categoryTools.set(entry.category, list);
|
|
107
|
+
}
|
|
108
|
+
const domainCorpus = [...categoryTools.entries()].map(([category, toolNames]) => {
|
|
109
|
+
const allTags = new Set();
|
|
110
|
+
const descs = [];
|
|
111
|
+
for (const tn of toolNames) {
|
|
112
|
+
const e = REGISTRY.find((r) => r.name === tn);
|
|
113
|
+
if (e)
|
|
114
|
+
e.tags.forEach((t) => allTags.add(t));
|
|
115
|
+
const d = descMap.get(tn);
|
|
116
|
+
if (d)
|
|
117
|
+
descs.push(d);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
name: `domain:${category}`,
|
|
121
|
+
text: `${category} domain: ${toolNames.join(" ")} ${[...allTags].join(" ")} ${descs.map(d => d.slice(0, 80)).join(" ")}`,
|
|
122
|
+
nodeType: "domain",
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const embeddingCorpus = [...toolCorpus, ...domainCorpus];
|
|
66
126
|
initEmbeddingIndex(embeddingCorpus).catch(() => {
|
|
67
127
|
/* Embedding init failed — semantic search stays disabled */
|
|
68
128
|
});
|
|
@@ -119,6 +179,235 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
119
179
|
};
|
|
120
180
|
}
|
|
121
181
|
});
|
|
182
|
+
// ── MCP Resources ───────────────────────────────────────────────────
|
|
183
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
184
|
+
return {
|
|
185
|
+
resources: [
|
|
186
|
+
{
|
|
187
|
+
uri: "convex://project-health",
|
|
188
|
+
name: "Project Health Summary",
|
|
189
|
+
description: "Latest quality gate score, audit coverage, and issue counts across all audit types",
|
|
190
|
+
mimeType: "application/json",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
uri: "convex://recent-audits",
|
|
194
|
+
name: "Recent Audit Results",
|
|
195
|
+
description: "Summary of the 10 most recent audit runs with issue counts and timestamps",
|
|
196
|
+
mimeType: "application/json",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
uri: "convex://gotcha-db",
|
|
200
|
+
name: "Gotcha Knowledge Base",
|
|
201
|
+
description: "All stored Convex gotchas (seeded + user-recorded) with categories and severity",
|
|
202
|
+
mimeType: "application/json",
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
208
|
+
const uri = request.params.uri;
|
|
209
|
+
const db = getDb();
|
|
210
|
+
if (uri === "convex://project-health") {
|
|
211
|
+
// Aggregate across all projects
|
|
212
|
+
const audits = db.prepare("SELECT audit_type, issue_count, audited_at FROM audit_results ORDER BY audited_at DESC LIMIT 50").all();
|
|
213
|
+
const byType = {};
|
|
214
|
+
for (const a of audits) {
|
|
215
|
+
if (!byType[a.audit_type]) {
|
|
216
|
+
byType[a.audit_type] = { count: a.issue_count, latest: a.audited_at };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const totalIssues = Object.values(byType).reduce((s, v) => s + v.count, 0);
|
|
220
|
+
const auditTypes = Object.keys(byType).length;
|
|
221
|
+
const latestGate = db.prepare("SELECT findings FROM deploy_checks WHERE check_type = 'quality_gate' ORDER BY checked_at DESC LIMIT 1").get();
|
|
222
|
+
let gateResult = null;
|
|
223
|
+
if (latestGate?.findings) {
|
|
224
|
+
try {
|
|
225
|
+
gateResult = JSON.parse(latestGate.findings);
|
|
226
|
+
}
|
|
227
|
+
catch { /* skip */ }
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
contents: [{
|
|
231
|
+
uri,
|
|
232
|
+
mimeType: "application/json",
|
|
233
|
+
text: JSON.stringify({
|
|
234
|
+
totalIssues,
|
|
235
|
+
auditTypesRun: auditTypes,
|
|
236
|
+
issuesByType: byType,
|
|
237
|
+
latestQualityGate: gateResult ? { score: gateResult.score, grade: gateResult.grade, passed: gateResult.passed } : null,
|
|
238
|
+
toolCount: ALL_TOOLS.length,
|
|
239
|
+
}, null, 2),
|
|
240
|
+
}],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (uri === "convex://recent-audits") {
|
|
244
|
+
const audits = db.prepare("SELECT id, project_dir, audit_type, issue_count, audited_at FROM audit_results ORDER BY audited_at DESC LIMIT 10").all();
|
|
245
|
+
return {
|
|
246
|
+
contents: [{
|
|
247
|
+
uri,
|
|
248
|
+
mimeType: "application/json",
|
|
249
|
+
text: JSON.stringify({ audits }, null, 2),
|
|
250
|
+
}],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (uri === "convex://gotcha-db") {
|
|
254
|
+
const gotchas = db.prepare("SELECT key, category, severity, tags, source, updated_at FROM convex_gotchas ORDER BY updated_at DESC").all();
|
|
255
|
+
return {
|
|
256
|
+
contents: [{
|
|
257
|
+
uri,
|
|
258
|
+
mimeType: "application/json",
|
|
259
|
+
text: JSON.stringify({
|
|
260
|
+
totalGotchas: gotchas.length,
|
|
261
|
+
bySource: {
|
|
262
|
+
seed: gotchas.filter(g => g.source === "seed").length,
|
|
263
|
+
user: gotchas.filter(g => g.source === "user").length,
|
|
264
|
+
},
|
|
265
|
+
gotchas,
|
|
266
|
+
}, null, 2),
|
|
267
|
+
}],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
contents: [{
|
|
272
|
+
uri,
|
|
273
|
+
mimeType: "text/plain",
|
|
274
|
+
text: `Unknown resource: ${uri}`,
|
|
275
|
+
}],
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
// ── MCP Prompts ─────────────────────────────────────────────────────
|
|
279
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
280
|
+
return {
|
|
281
|
+
prompts: [
|
|
282
|
+
{
|
|
283
|
+
name: "full-audit",
|
|
284
|
+
description: "Run a complete Convex project audit: schema, functions, auth, queries, actions, type safety, transactions, storage, pagination, data modeling, dev setup, vectors, schedulers — then quality gate",
|
|
285
|
+
arguments: [
|
|
286
|
+
{
|
|
287
|
+
name: "projectDir",
|
|
288
|
+
description: "Absolute path to the project root",
|
|
289
|
+
required: true,
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "pre-deploy-checklist",
|
|
295
|
+
description: "Step-by-step pre-deployment verification: audit critical issues, check env vars, review migration plan, run quality gate",
|
|
296
|
+
arguments: [
|
|
297
|
+
{
|
|
298
|
+
name: "projectDir",
|
|
299
|
+
description: "Absolute path to the project root",
|
|
300
|
+
required: true,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "security-review",
|
|
306
|
+
description: "Security-focused audit: authorization coverage, type safety, action safety, storage permissions",
|
|
307
|
+
arguments: [
|
|
308
|
+
{
|
|
309
|
+
name: "projectDir",
|
|
310
|
+
description: "Absolute path to the project root",
|
|
311
|
+
required: true,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
319
|
+
const { name, arguments: promptArgs } = request.params;
|
|
320
|
+
const projectDir = promptArgs?.projectDir ?? ".";
|
|
321
|
+
if (name === "full-audit") {
|
|
322
|
+
return {
|
|
323
|
+
description: "Complete Convex project audit sequence",
|
|
324
|
+
messages: [
|
|
325
|
+
{
|
|
326
|
+
role: "user",
|
|
327
|
+
content: {
|
|
328
|
+
type: "text",
|
|
329
|
+
text: `Run a complete audit of the Convex project at "${projectDir}". Execute these tools in order:
|
|
330
|
+
|
|
331
|
+
1. convex_audit_schema — Check schema.ts for anti-patterns
|
|
332
|
+
2. convex_audit_functions — Audit function registration and compliance
|
|
333
|
+
3. convex_audit_authorization — Check auth coverage on public endpoints
|
|
334
|
+
4. convex_audit_query_efficiency — Find unbounded queries and missing indexes
|
|
335
|
+
5. convex_audit_actions — Validate action safety (no ctx.db, error handling)
|
|
336
|
+
6. convex_check_type_safety — Find as-any casts and type issues
|
|
337
|
+
7. convex_audit_transaction_safety — Detect race conditions
|
|
338
|
+
8. convex_audit_storage_usage — Check file storage patterns
|
|
339
|
+
9. convex_audit_pagination — Validate pagination implementations
|
|
340
|
+
10. convex_audit_data_modeling — Check schema design quality
|
|
341
|
+
11. convex_audit_vector_search — Validate vector search setup
|
|
342
|
+
12. convex_audit_schedulers — Check scheduled function safety
|
|
343
|
+
13. convex_audit_dev_setup — Verify project setup
|
|
344
|
+
14. convex_quality_gate — Run configurable quality gate across all results
|
|
345
|
+
|
|
346
|
+
After running all audits, summarize:
|
|
347
|
+
- Total issues by severity (critical/warning/info)
|
|
348
|
+
- Top 5 most impactful issues to fix first
|
|
349
|
+
- Quality gate score and grade
|
|
350
|
+
- Trend direction if previous audits exist (use convex_audit_diff)`,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (name === "pre-deploy-checklist") {
|
|
357
|
+
return {
|
|
358
|
+
description: "Pre-deployment verification sequence",
|
|
359
|
+
messages: [
|
|
360
|
+
{
|
|
361
|
+
role: "user",
|
|
362
|
+
content: {
|
|
363
|
+
type: "text",
|
|
364
|
+
text: `Run pre-deployment checks for the Convex project at "${projectDir}":
|
|
365
|
+
|
|
366
|
+
1. convex_pre_deploy_gate — Structural checks (schema, auth config, initialization)
|
|
367
|
+
2. convex_check_env_vars — Verify all required env vars are set
|
|
368
|
+
3. convex_audit_authorization — Ensure auth coverage is adequate
|
|
369
|
+
4. convex_audit_actions — No ctx.db access in actions
|
|
370
|
+
5. convex_snapshot_schema — Capture current schema state
|
|
371
|
+
6. convex_schema_migration_plan — Compare against previous snapshot for breaking changes
|
|
372
|
+
7. convex_quality_gate — Final quality check with thresholds
|
|
373
|
+
|
|
374
|
+
Report: DEPLOY or DO NOT DEPLOY with specific blockers to fix.`,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (name === "security-review") {
|
|
381
|
+
return {
|
|
382
|
+
description: "Security-focused audit sequence",
|
|
383
|
+
messages: [
|
|
384
|
+
{
|
|
385
|
+
role: "user",
|
|
386
|
+
content: {
|
|
387
|
+
type: "text",
|
|
388
|
+
text: `Run a security review of the Convex project at "${projectDir}":
|
|
389
|
+
|
|
390
|
+
1. convex_audit_authorization — Auth coverage on all public endpoints
|
|
391
|
+
2. convex_check_type_safety — Type safety bypasses (as any)
|
|
392
|
+
3. convex_audit_actions — Action safety (ctx.db, error handling, "use node")
|
|
393
|
+
4. convex_audit_storage_usage — Storage permission patterns
|
|
394
|
+
5. convex_audit_pagination — Unbounded numItems (DoS risk)
|
|
395
|
+
6. convex_audit_transaction_safety — Race condition risks
|
|
396
|
+
|
|
397
|
+
Focus on: unauthorized data access, unvalidated inputs, missing error boundaries, and potential data corruption vectors.`,
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
description: "Unknown prompt",
|
|
405
|
+
messages: [{
|
|
406
|
+
role: "user",
|
|
407
|
+
content: { type: "text", text: `Unknown prompt: ${name}` },
|
|
408
|
+
}],
|
|
409
|
+
};
|
|
410
|
+
});
|
|
122
411
|
// ── Start server ────────────────────────────────────────────────────
|
|
123
412
|
async function main() {
|
|
124
413
|
const transport = new StdioServerTransport();
|
|
@@ -0,0 +1,180 @@
|
|
|
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 auditActions(convexDir) {
|
|
31
|
+
const files = collectTsFiles(convexDir);
|
|
32
|
+
const issues = [];
|
|
33
|
+
let totalActions = 0;
|
|
34
|
+
let actionsWithDbAccess = 0;
|
|
35
|
+
let actionsWithoutNodeDirective = 0;
|
|
36
|
+
let actionsWithoutErrorHandling = 0;
|
|
37
|
+
let actionCallingAction = 0;
|
|
38
|
+
// Node APIs that require "use node" directive
|
|
39
|
+
const nodeApis = /\b(require|__dirname|__filename|Buffer\.|process\.env|fs\.|path\.|crypto\.|child_process|net\.|http\.|https\.)\b/;
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
const content = readFileSync(filePath, "utf-8");
|
|
42
|
+
const relativePath = filePath.replace(convexDir, "").replace(/^[\\/]/, "");
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
const hasUseNode = /["']use node["']/.test(content);
|
|
45
|
+
const actionPattern = /export\s+(?:const\s+(\w+)\s*=|default)\s+(action|internalAction)\s*\(/g;
|
|
46
|
+
let m;
|
|
47
|
+
while ((m = actionPattern.exec(content)) !== null) {
|
|
48
|
+
const funcName = m[1] || "default";
|
|
49
|
+
const funcType = m[2];
|
|
50
|
+
totalActions++;
|
|
51
|
+
const startLine = content.slice(0, m.index).split("\n").length - 1;
|
|
52
|
+
// Extract body using brace tracking
|
|
53
|
+
let depth = 0;
|
|
54
|
+
let foundOpen = false;
|
|
55
|
+
let endLine = Math.min(startLine + 100, lines.length);
|
|
56
|
+
for (let j = startLine; j < lines.length; j++) {
|
|
57
|
+
for (const ch of lines[j]) {
|
|
58
|
+
if (ch === "{") {
|
|
59
|
+
depth++;
|
|
60
|
+
foundOpen = true;
|
|
61
|
+
}
|
|
62
|
+
if (ch === "}")
|
|
63
|
+
depth--;
|
|
64
|
+
}
|
|
65
|
+
if (foundOpen && depth <= 0) {
|
|
66
|
+
endLine = j + 1;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const body = lines.slice(startLine, endLine).join("\n");
|
|
71
|
+
// Check 1: ctx.db access in action (FATAL — not allowed)
|
|
72
|
+
if (/ctx\.db\.(get|query|insert|patch|replace|delete)\s*\(/.test(body)) {
|
|
73
|
+
actionsWithDbAccess++;
|
|
74
|
+
issues.push({
|
|
75
|
+
severity: "critical",
|
|
76
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
77
|
+
functionName: funcName,
|
|
78
|
+
message: `${funcType} "${funcName}" accesses ctx.db directly. Actions cannot access the database — use ctx.runQuery/ctx.runMutation instead.`,
|
|
79
|
+
fix: "Move DB operations into a query or mutation, then call via ctx.runQuery(internal.file.func, args) or ctx.runMutation(...)",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Check 2: Node API usage without "use node"
|
|
83
|
+
if (!hasUseNode && nodeApis.test(body)) {
|
|
84
|
+
actionsWithoutNodeDirective++;
|
|
85
|
+
issues.push({
|
|
86
|
+
severity: "critical",
|
|
87
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
88
|
+
functionName: funcName,
|
|
89
|
+
message: `${funcType} "${funcName}" uses Node.js APIs but file lacks "use node" directive. Will fail in Convex runtime.`,
|
|
90
|
+
fix: `Add "use node"; at the top of ${relativePath}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Check 3: External API calls without try/catch
|
|
94
|
+
const hasFetch = /\bfetch\s*\(/.test(body);
|
|
95
|
+
const hasAxios = /\baxios\b/.test(body);
|
|
96
|
+
const hasExternalCall = hasFetch || hasAxios;
|
|
97
|
+
const hasTryCatch = /try\s*\{/.test(body);
|
|
98
|
+
if (hasExternalCall && !hasTryCatch) {
|
|
99
|
+
actionsWithoutErrorHandling++;
|
|
100
|
+
issues.push({
|
|
101
|
+
severity: "warning",
|
|
102
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
103
|
+
functionName: funcName,
|
|
104
|
+
message: `${funcType} "${funcName}" makes external API calls without try/catch. Network failures will crash the action.`,
|
|
105
|
+
fix: "Wrap fetch/axios calls in try/catch and handle errors gracefully",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Check 4: Action calling another action
|
|
109
|
+
if (/ctx\.runAction\s*\(/.test(body)) {
|
|
110
|
+
actionCallingAction++;
|
|
111
|
+
issues.push({
|
|
112
|
+
severity: "warning",
|
|
113
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
114
|
+
functionName: funcName,
|
|
115
|
+
message: `${funcType} "${funcName}" calls ctx.runAction(). Only call action from action when crossing runtimes (V8 ↔ Node). Otherwise extract shared logic into a helper function.`,
|
|
116
|
+
fix: "Extract shared logic into an async helper, or use ctx.runMutation/ctx.runQuery as intermediary",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Check 5: Very long action body (likely doing too much)
|
|
120
|
+
const bodyLines = endLine - startLine;
|
|
121
|
+
if (bodyLines > 80) {
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: "info",
|
|
124
|
+
location: `${relativePath}:${startLine + 1}`,
|
|
125
|
+
functionName: funcName,
|
|
126
|
+
message: `${funcType} "${funcName}" is ${bodyLines} lines long. Consider splitting into smaller actions or extracting helpers.`,
|
|
127
|
+
fix: "Break large actions into smaller, focused functions",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
issues,
|
|
134
|
+
stats: {
|
|
135
|
+
totalActions,
|
|
136
|
+
actionsWithDbAccess,
|
|
137
|
+
actionsWithoutNodeDirective,
|
|
138
|
+
actionsWithoutErrorHandling,
|
|
139
|
+
actionCallingAction,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ── Tool Definition ─────────────────────────────────────────────────
|
|
144
|
+
export const actionAuditTools = [
|
|
145
|
+
{
|
|
146
|
+
name: "convex_audit_actions",
|
|
147
|
+
description: 'Audit Convex actions for: ctx.db access (fatal — actions cannot access DB directly), missing "use node" directive for Node APIs, external API calls without error handling, and action-calling-action anti-patterns.',
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
projectDir: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Absolute path to the project root containing a convex/ directory",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
required: ["projectDir"],
|
|
157
|
+
},
|
|
158
|
+
handler: async (args) => {
|
|
159
|
+
const projectDir = resolve(args.projectDir);
|
|
160
|
+
const convexDir = findConvexDir(projectDir);
|
|
161
|
+
if (!convexDir) {
|
|
162
|
+
return { error: "No convex/ directory found" };
|
|
163
|
+
}
|
|
164
|
+
const { issues, stats } = auditActions(convexDir);
|
|
165
|
+
const db = getDb();
|
|
166
|
+
db.prepare("INSERT INTO audit_results (id, project_dir, audit_type, issues_json, issue_count) VALUES (?, ?, ?, ?, ?)").run(genId("audit"), projectDir, "action_audit", JSON.stringify(issues), issues.length);
|
|
167
|
+
return {
|
|
168
|
+
summary: {
|
|
169
|
+
...stats,
|
|
170
|
+
totalIssues: issues.length,
|
|
171
|
+
critical: issues.filter((i) => i.severity === "critical").length,
|
|
172
|
+
warnings: issues.filter((i) => i.severity === "warning").length,
|
|
173
|
+
},
|
|
174
|
+
issues: issues.slice(0, 30),
|
|
175
|
+
quickRef: getQuickRef("convex_audit_actions"),
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
//# sourceMappingURL=actionAuditTools.js.map
|