@homenshum/convex-mcp-nodebench 0.3.0 → 0.4.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 +2 -0
- package/dist/tools/critterTools.d.ts +12 -0
- package/dist/tools/critterTools.js +133 -0
- package/dist/tools/functionTools.js +21 -2
- package/dist/tools/schemaTools.js +14 -8
- package/dist/tools/toolRegistry.js +15 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { integrationBridgeTools } from "./tools/integrationBridgeTools.js";
|
|
|
25
25
|
import { cronTools } from "./tools/cronTools.js";
|
|
26
26
|
import { componentTools } from "./tools/componentTools.js";
|
|
27
27
|
import { httpTools } from "./tools/httpTools.js";
|
|
28
|
+
import { critterTools } from "./tools/critterTools.js";
|
|
28
29
|
import { CONVEX_GOTCHAS } from "./gotchaSeed.js";
|
|
29
30
|
import { REGISTRY } from "./tools/toolRegistry.js";
|
|
30
31
|
import { initEmbeddingIndex } from "./tools/embeddingProvider.js";
|
|
@@ -39,6 +40,7 @@ const ALL_TOOLS = [
|
|
|
39
40
|
...cronTools,
|
|
40
41
|
...componentTools,
|
|
41
42
|
...httpTools,
|
|
43
|
+
...critterTools,
|
|
42
44
|
];
|
|
43
45
|
const toolMap = new Map();
|
|
44
46
|
for (const tool of ALL_TOOLS) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critter Tools — The accountability partner that wants to know everything.
|
|
3
|
+
*
|
|
4
|
+
* Convex-flavored version: "Why are you making this schema change? Who needs this function?"
|
|
5
|
+
* The friction is the feature — slowing down to think prevents Convex-specific pitfalls
|
|
6
|
+
* like unnecessary indexes, over-normalized schemas, and functions nobody calls.
|
|
7
|
+
*
|
|
8
|
+
* 1 tool:
|
|
9
|
+
* - convex_critter_check: Pre-action intentionality check for Convex work
|
|
10
|
+
*/
|
|
11
|
+
import type { McpTool } from "../types.js";
|
|
12
|
+
export declare const critterTools: McpTool[];
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critter Tools — The accountability partner that wants to know everything.
|
|
3
|
+
*
|
|
4
|
+
* Convex-flavored version: "Why are you making this schema change? Who needs this function?"
|
|
5
|
+
* The friction is the feature — slowing down to think prevents Convex-specific pitfalls
|
|
6
|
+
* like unnecessary indexes, over-normalized schemas, and functions nobody calls.
|
|
7
|
+
*
|
|
8
|
+
* 1 tool:
|
|
9
|
+
* - convex_critter_check: Pre-action intentionality check for Convex work
|
|
10
|
+
*/
|
|
11
|
+
import { getDb } from "../db.js";
|
|
12
|
+
// ── DB setup ────────────────────────────────────────────────────────────────
|
|
13
|
+
function ensureCritterTable() {
|
|
14
|
+
const db = getDb();
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS critter_checks (
|
|
17
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
task TEXT NOT NULL,
|
|
19
|
+
why TEXT NOT NULL,
|
|
20
|
+
who TEXT NOT NULL,
|
|
21
|
+
success_looks_like TEXT,
|
|
22
|
+
score INTEGER NOT NULL,
|
|
23
|
+
verdict TEXT NOT NULL,
|
|
24
|
+
feedback TEXT NOT NULL,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
function scoreCritterCheck(input) {
|
|
30
|
+
const feedback = [];
|
|
31
|
+
let score = 100;
|
|
32
|
+
const taskLower = input.task.toLowerCase().trim();
|
|
33
|
+
const whyLower = input.why.toLowerCase().trim();
|
|
34
|
+
const whoLower = input.who.toLowerCase().trim();
|
|
35
|
+
// Check 1: Circular reasoning
|
|
36
|
+
const taskWords = new Set(taskLower.split(/\s+/).filter((w) => w.length > 3));
|
|
37
|
+
const whyWords = whyLower.split(/\s+/).filter((w) => w.length > 3);
|
|
38
|
+
const overlap = whyWords.filter((w) => taskWords.has(w));
|
|
39
|
+
if (whyWords.length > 0 && overlap.length / whyWords.length > 0.7) {
|
|
40
|
+
score -= 30;
|
|
41
|
+
feedback.push("Circular: your 'why' mostly restates the task. What user outcome does this enable?");
|
|
42
|
+
}
|
|
43
|
+
// Check 2: Vague audience
|
|
44
|
+
const vagueAudiences = ["users", "everyone", "people", "the team", "stakeholders", "clients"];
|
|
45
|
+
if (vagueAudiences.includes(whoLower)) {
|
|
46
|
+
score -= 20;
|
|
47
|
+
feedback.push(`"${input.who}" is too broad. Which user role or API consumer specifically?`);
|
|
48
|
+
}
|
|
49
|
+
// Check 3: Too short
|
|
50
|
+
if (whyLower.length < 10) {
|
|
51
|
+
score -= 25;
|
|
52
|
+
feedback.push("The 'why' is too short. What problem does this solve?");
|
|
53
|
+
}
|
|
54
|
+
if (whoLower.length < 3) {
|
|
55
|
+
score -= 25;
|
|
56
|
+
feedback.push("The 'who' is too short. Specify who benefits.");
|
|
57
|
+
}
|
|
58
|
+
// Check 4: Deference over understanding
|
|
59
|
+
const deferPatterns = ["was told", "asked to", "ticket says", "was asked", "jira", "they said"];
|
|
60
|
+
if (deferPatterns.some((p) => whyLower.includes(p))) {
|
|
61
|
+
score -= 15;
|
|
62
|
+
feedback.push("Citing authority instead of understanding purpose. Why does this matter to the product?");
|
|
63
|
+
}
|
|
64
|
+
// Bonus for specificity
|
|
65
|
+
if (input.success_looks_like && input.success_looks_like.length > 20) {
|
|
66
|
+
score += 10;
|
|
67
|
+
feedback.push("Good: success criteria defined — this makes the deploy gate concrete.");
|
|
68
|
+
}
|
|
69
|
+
score = Math.max(0, Math.min(100, score));
|
|
70
|
+
let verdict;
|
|
71
|
+
if (score >= 70) {
|
|
72
|
+
verdict = "proceed";
|
|
73
|
+
if (feedback.length === 0) {
|
|
74
|
+
feedback.push("Clear intent. Proceed with confidence.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (score >= 40) {
|
|
78
|
+
verdict = "reconsider";
|
|
79
|
+
feedback.push("Pause. Sharpen your answers before writing Convex code.");
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
verdict = "stop";
|
|
83
|
+
feedback.push("Stop: purpose unclear. Do not proceed.");
|
|
84
|
+
}
|
|
85
|
+
return { score, verdict, feedback };
|
|
86
|
+
}
|
|
87
|
+
// ── Tool definition ─────────────────────────────────────────────────────────
|
|
88
|
+
export const critterTools = [
|
|
89
|
+
{
|
|
90
|
+
name: "convex_critter_check",
|
|
91
|
+
description: "The accountability partner that wants to know everything — answer 'Why are you doing this? Who is it for?' before starting Convex work. " +
|
|
92
|
+
"Scores for circular reasoning, vague audiences, and deference-over-understanding. " +
|
|
93
|
+
"The friction is the feature: slowing down prevents unnecessary schema changes, unneeded indexes, and functions nobody calls.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
task: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "What you are about to do (e.g. 'Add a new table for user preferences')",
|
|
100
|
+
},
|
|
101
|
+
why: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Why are you doing this? What user problem does it solve?",
|
|
104
|
+
},
|
|
105
|
+
who: {
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "Who is this for? Name a specific role, persona, or API consumer.",
|
|
108
|
+
},
|
|
109
|
+
success_looks_like: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Optional: What does success look like? How will you verify this worked?",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ["task", "why", "who"],
|
|
115
|
+
},
|
|
116
|
+
handler: async (args) => {
|
|
117
|
+
ensureCritterTable();
|
|
118
|
+
const result = scoreCritterCheck(args);
|
|
119
|
+
const db = getDb();
|
|
120
|
+
db.prepare(`INSERT INTO critter_checks (task, why, who, success_looks_like, score, verdict, feedback)
|
|
121
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(args.task, args.why, args.who, args.success_looks_like ?? null, result.score, result.verdict, JSON.stringify(result.feedback));
|
|
122
|
+
return {
|
|
123
|
+
score: result.score,
|
|
124
|
+
verdict: result.verdict,
|
|
125
|
+
feedback: result.feedback,
|
|
126
|
+
tip: result.verdict === "proceed"
|
|
127
|
+
? "Critter check passed. Proceed with clear intent."
|
|
128
|
+
: "Sharpen your answers and re-run convex_critter_check.",
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
//# sourceMappingURL=critterTools.js.map
|
|
@@ -147,14 +147,33 @@ function auditFunctions(convexDir) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
// Check 4: Cross-call violations — queries CANNOT call runMutation or runAction
|
|
150
|
+
// Use brace-depth tracking to find exact function boundaries (avoids false positives)
|
|
150
151
|
for (const fn of functions) {
|
|
151
152
|
if (fn.type !== "query" && fn.type !== "internalQuery")
|
|
152
153
|
continue;
|
|
153
154
|
const content = readFileSync(fn.filePath, "utf-8");
|
|
154
155
|
const lines = content.split("\n");
|
|
155
|
-
// Find the function body (rough: from export line to next export or end)
|
|
156
156
|
const startLine = fn.line - 1;
|
|
157
|
-
|
|
157
|
+
// Find the function body by tracking brace depth from the opening ({
|
|
158
|
+
// The pattern is: export const X = query({ ... });
|
|
159
|
+
let depth = 0;
|
|
160
|
+
let foundOpen = false;
|
|
161
|
+
let endLine = Math.min(startLine + 80, lines.length);
|
|
162
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
163
|
+
for (const ch of lines[i]) {
|
|
164
|
+
if (ch === "{") {
|
|
165
|
+
depth++;
|
|
166
|
+
foundOpen = true;
|
|
167
|
+
}
|
|
168
|
+
if (ch === "}")
|
|
169
|
+
depth--;
|
|
170
|
+
}
|
|
171
|
+
if (foundOpen && depth <= 0) {
|
|
172
|
+
endLine = i + 1;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const chunk = lines.slice(startLine, endLine).join("\n");
|
|
158
177
|
if (/ctx\.runMutation/.test(chunk)) {
|
|
159
178
|
issues.push({
|
|
160
179
|
severity: "critical",
|
|
@@ -120,20 +120,26 @@ function analyzeSchema(schemaContent, filePath) {
|
|
|
120
120
|
gotchaKey: "index_name_include_fields",
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
// Check: v.any() usage (defeats validator purpose)
|
|
123
|
+
// Check: v.any() usage (defeats validator purpose) — aggregate
|
|
124
|
+
const vAnyLines = [];
|
|
124
125
|
lines.forEach((line, i) => {
|
|
125
126
|
if (line.trim().startsWith("//") || line.trim().startsWith("*"))
|
|
126
127
|
return;
|
|
127
128
|
if (/v\.any\s*\(\s*\)/.test(line)) {
|
|
128
|
-
|
|
129
|
-
severity: "warning",
|
|
130
|
-
location: `${filePath}:${i + 1}`,
|
|
131
|
-
message: "v.any() defeats the purpose of validators. Use a specific validator type.",
|
|
132
|
-
fix: "Replace v.any() with the appropriate validator (v.string(), v.object({...}), etc.)",
|
|
133
|
-
gotchaKey: "avoid_v_any",
|
|
134
|
-
});
|
|
129
|
+
vAnyLines.push(i + 1);
|
|
135
130
|
}
|
|
136
131
|
});
|
|
132
|
+
if (vAnyLines.length > 0) {
|
|
133
|
+
const examples = vAnyLines.slice(0, 5).map((l) => `line ${l}`).join(", ");
|
|
134
|
+
const more = vAnyLines.length > 5 ? ` (+${vAnyLines.length - 5} more)` : "";
|
|
135
|
+
issues.push({
|
|
136
|
+
severity: "warning",
|
|
137
|
+
location: filePath,
|
|
138
|
+
message: `${vAnyLines.length} uses of v.any() defeat the purpose of validators. Locations: ${examples}${more}`,
|
|
139
|
+
fix: "Replace v.any() with specific validators (v.string(), v.object({...}), v.union(...), etc.)",
|
|
140
|
+
gotchaKey: "avoid_v_any",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
137
143
|
// Check: _creationTime or _id in schema definition (system fields)
|
|
138
144
|
lines.forEach((line, i) => {
|
|
139
145
|
if (line.trim().startsWith("//") || line.trim().startsWith("*"))
|
|
@@ -246,6 +246,21 @@ export const REGISTRY = [
|
|
|
246
246
|
phase: "audit",
|
|
247
247
|
complexity: "low",
|
|
248
248
|
},
|
|
249
|
+
// ── Critter Tools ──────────────────────────
|
|
250
|
+
{
|
|
251
|
+
name: "convex_critter_check",
|
|
252
|
+
category: "methodology",
|
|
253
|
+
tags: ["intentionality", "why", "who", "purpose", "audience", "reflection", "pre-action", "critter"],
|
|
254
|
+
quickRef: {
|
|
255
|
+
nextAction: "Critter check done. If verdict is 'proceed', start your Convex work. If 'reconsider', sharpen answers.",
|
|
256
|
+
nextTools: ["convex_audit_schema", "convex_audit_functions", "convex_search_gotchas"],
|
|
257
|
+
methodology: "convex_intentionality",
|
|
258
|
+
relatedGotchas: [],
|
|
259
|
+
confidence: "high",
|
|
260
|
+
},
|
|
261
|
+
phase: "meta",
|
|
262
|
+
complexity: "low",
|
|
263
|
+
},
|
|
249
264
|
];
|
|
250
265
|
export function getQuickRef(toolName) {
|
|
251
266
|
const entry = REGISTRY.find((e) => e.name === toolName);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homenshum/convex-mcp-nodebench",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": {
|