@homenshum/convex-mcp-nodebench 0.3.0 → 0.4.1

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
@@ -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,204 @@
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 (threshold 0.5)
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.5) {
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", "customers", "developers"];
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: Empty or too short
50
+ if (whyLower.length === 0) {
51
+ score -= 40;
52
+ feedback.push("Empty 'why': you haven't stated any purpose at all.");
53
+ }
54
+ else if (whyLower.length < 10) {
55
+ score -= 25;
56
+ feedback.push("The 'why' is too short. What problem does this solve?");
57
+ }
58
+ if (whoLower.length < 3) {
59
+ score -= 25;
60
+ feedback.push("The 'who' is too short. Specify who benefits.");
61
+ }
62
+ // Check 4: Deference over understanding
63
+ const deferPatterns = ["was told", "asked to", "ticket says", "was asked", "jira", "they said"];
64
+ if (deferPatterns.some((p) => whyLower.includes(p))) {
65
+ score -= 15;
66
+ feedback.push("Citing authority instead of understanding purpose. Why does this matter to the product?");
67
+ }
68
+ // Check 5: Non-answer patterns (count matches, -20 each, cap -40)
69
+ const nonAnswerPatterns = [
70
+ "just because", "don't know", "not sure", "why not", "might need it",
71
+ "no reason", "no idea", "whatever", "idk", "tbd",
72
+ ];
73
+ const nonAnswerHits = nonAnswerPatterns.filter((p) => whyLower.includes(p)).length;
74
+ if (nonAnswerHits > 0) {
75
+ const nonAnswerPenalty = Math.min(nonAnswerHits * 20, 40);
76
+ score -= nonAnswerPenalty;
77
+ feedback.push("Non-answer: your 'why' signals unclear purpose. What specific problem does this solve?");
78
+ }
79
+ // Check 6: Repetitive padding (why + who)
80
+ const whyAllWords = whyLower.split(/\s+/).filter((w) => w.length > 2);
81
+ if (whyAllWords.length >= 5) {
82
+ const whyUniqueWords = new Set(whyAllWords);
83
+ if (whyUniqueWords.size / whyAllWords.length < 0.4) {
84
+ score -= 25;
85
+ feedback.push("Repetitive: your 'why' repeats the same words. Articulate distinct reasoning.");
86
+ }
87
+ }
88
+ const whoAllWords = whoLower.split(/\s+/).filter((w) => w.length > 2);
89
+ if (whoAllWords.length >= 5) {
90
+ const whoUniqueWords = new Set(whoAllWords);
91
+ if (whoUniqueWords.size / whoAllWords.length < 0.4) {
92
+ score -= 25;
93
+ feedback.push("Repetitive: your 'who' repeats the same words. Name a real audience.");
94
+ }
95
+ }
96
+ // Check 7: Buzzword-heavy corporate-speak (scans why + who)
97
+ const buzzwords = [
98
+ "leverage", "synergies", "synergy", "paradigm", "holistic", "alignment",
99
+ "transformation", "innovative", "disruptive", "best practices",
100
+ "streamline", "ecosystem", "actionable", "circle back",
101
+ ];
102
+ const allText = `${whyLower} ${whoLower}`;
103
+ const buzzCount = buzzwords.filter((b) => allText.includes(b)).length;
104
+ if (buzzCount >= 4) {
105
+ score -= 35;
106
+ feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
107
+ }
108
+ else if (buzzCount >= 3) {
109
+ score -= 30;
110
+ feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
111
+ }
112
+ else if (buzzCount >= 2) {
113
+ score -= 20;
114
+ feedback.push("Buzzword-heavy: corporate-speak without concrete meaning. What specific problem does this solve?");
115
+ }
116
+ // Check 8: Hedging language
117
+ const hedgeWords = ["could", "potentially", "maybe", "possibly", "might", "perhaps", "hopefully"];
118
+ const hedgeCount = hedgeWords.filter((h) => {
119
+ const regex = new RegExp(`\\b${h}\\b`, "i");
120
+ return regex.test(whyLower);
121
+ }).length;
122
+ if (hedgeCount >= 2) {
123
+ score -= 15;
124
+ feedback.push("Hedging: too many 'could/maybe/potentially' signals uncertain value. Be definitive.");
125
+ }
126
+ // Check 9: Task-word echo — same word from task repeated 3+ times in why
127
+ for (const tw of taskWords) {
128
+ const twCount = whyWords.filter((w) => w === tw).length;
129
+ if (twCount >= 3) {
130
+ score -= 20;
131
+ feedback.push(`Echo: "${tw}" appears ${twCount} times in your 'why' — this is filler, not reasoning.`);
132
+ break;
133
+ }
134
+ }
135
+ // Bonus for specificity
136
+ if (input.success_looks_like && input.success_looks_like.length > 20) {
137
+ score += 10;
138
+ feedback.push("Good: success criteria defined — this makes the deploy gate concrete.");
139
+ }
140
+ score = Math.max(0, Math.min(100, score));
141
+ let verdict;
142
+ if (score >= 70) {
143
+ verdict = "proceed";
144
+ if (feedback.length === 0) {
145
+ feedback.push("Clear intent. Proceed with confidence.");
146
+ }
147
+ }
148
+ else if (score >= 40) {
149
+ verdict = "reconsider";
150
+ feedback.push("Pause. Sharpen your answers before writing Convex code.");
151
+ }
152
+ else {
153
+ verdict = "stop";
154
+ feedback.push("Stop: purpose unclear. Do not proceed.");
155
+ }
156
+ return { score, verdict, feedback };
157
+ }
158
+ // ── Tool definition ─────────────────────────────────────────────────────────
159
+ export const critterTools = [
160
+ {
161
+ name: "convex_critter_check",
162
+ description: "The accountability partner that wants to know everything — answer 'Why are you doing this? Who is it for?' before starting Convex work. " +
163
+ "Scores for circular reasoning, vague audiences, and deference-over-understanding. " +
164
+ "The friction is the feature: slowing down prevents unnecessary schema changes, unneeded indexes, and functions nobody calls.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ task: {
169
+ type: "string",
170
+ description: "What you are about to do (e.g. 'Add a new table for user preferences')",
171
+ },
172
+ why: {
173
+ type: "string",
174
+ description: "Why are you doing this? What user problem does it solve?",
175
+ },
176
+ who: {
177
+ type: "string",
178
+ description: "Who is this for? Name a specific role, persona, or API consumer.",
179
+ },
180
+ success_looks_like: {
181
+ type: "string",
182
+ description: "Optional: What does success look like? How will you verify this worked?",
183
+ },
184
+ },
185
+ required: ["task", "why", "who"],
186
+ },
187
+ handler: async (args) => {
188
+ ensureCritterTable();
189
+ const result = scoreCritterCheck(args);
190
+ const db = getDb();
191
+ db.prepare(`INSERT INTO critter_checks (task, why, who, success_looks_like, score, verdict, feedback)
192
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(args.task, args.why, args.who, args.success_looks_like ?? null, result.score, result.verdict, JSON.stringify(result.feedback));
193
+ return {
194
+ score: result.score,
195
+ verdict: result.verdict,
196
+ feedback: result.feedback,
197
+ tip: result.verdict === "proceed"
198
+ ? "Critter check passed. Proceed with clear intent."
199
+ : "Sharpen your answers and re-run convex_critter_check.",
200
+ };
201
+ },
202
+ },
203
+ ];
204
+ //# 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
- const chunk = lines.slice(startLine, Math.min(startLine + 80, lines.length)).join("\n");
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",
@@ -235,9 +235,13 @@ export const integrationBridgeTools = [
235
235
  }
236
236
  catch { /* ignore */ }
237
237
  }
238
+ const parsed = JSON.parse(snapshot.schemaJson);
238
239
  return {
239
240
  snapshotId: id,
240
241
  tableCount: snapshot.tableCount,
242
+ tables: parsed.tables,
243
+ totalIndexes: parsed.totalIndexes,
244
+ indexes: parsed.indexes,
241
245
  diff,
242
246
  quickRef: getQuickRef("convex_audit_schema"),
243
247
  };
@@ -108,7 +108,13 @@ export const methodologyTools = [
108
108
  required: ["topic"],
109
109
  },
110
110
  handler: async (args) => {
111
- const content = METHODOLOGY_CONTENT[args.topic];
111
+ // Default to overview when topic is missing
112
+ let topic = args.topic || "overview";
113
+ // Accept short names: "schema_audit" -> "convex_schema_audit"
114
+ if (!METHODOLOGY_CONTENT[topic] && METHODOLOGY_CONTENT[`convex_${topic}`]) {
115
+ topic = `convex_${topic}`;
116
+ }
117
+ const content = METHODOLOGY_CONTENT[topic];
112
118
  if (!content) {
113
119
  return {
114
120
  error: `Unknown topic: ${args.topic}`,
@@ -149,6 +155,7 @@ export const methodologyTools = [
149
155
  matchingTools: results.length,
150
156
  tools: results.map((r) => ({
151
157
  name: r.name,
158
+ score: Math.round(r._score * 100) / 100,
152
159
  category: r.category,
153
160
  phase: r.phase,
154
161
  complexity: r.complexity,
@@ -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
- issues.push({
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("*"))
@@ -2,9 +2,12 @@ import type { ConvexQuickRef, ToolRegistryEntry } from "../types.js";
2
2
  export declare const REGISTRY: ToolRegistryEntry[];
3
3
  export declare function getQuickRef(toolName: string): ConvexQuickRef | null;
4
4
  export declare function getToolsByCategory(category: string): ToolRegistryEntry[];
5
- export declare function findTools(query: string): ToolRegistryEntry[];
5
+ export interface ScoredToolEntry extends ToolRegistryEntry {
6
+ _score: number;
7
+ }
8
+ export declare function findTools(query: string): ScoredToolEntry[];
6
9
  /**
7
10
  * Async wrapper around findTools that fuses BM25 results with embedding RRF
8
11
  * when a neural embedding provider is available. Falls back to plain findTools otherwise.
9
12
  */
10
- export declare function findToolsWithEmbedding(query: string): Promise<ToolRegistryEntry[]>;
13
+ export declare function findToolsWithEmbedding(query: string): Promise<ScoredToolEntry[]>;
@@ -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);
@@ -314,12 +329,11 @@ export function findTools(query) {
314
329
  const termIdf = index.idf.get(qt) ?? 0;
315
330
  score += termIdf * (termTf * (k1 + 1)) / (termTf + k1 * (1 - b + b * (dl / index.avgDl)));
316
331
  }
317
- return { entry, score };
332
+ return { ...entry, _score: score };
318
333
  });
319
334
  return scored
320
- .filter((s) => s.score > 0)
321
- .sort((a, b) => b.score - a.score)
322
- .map((s) => s.entry);
335
+ .filter((s) => s._score > 0)
336
+ .sort((a, b) => b._score - a._score);
323
337
  }
324
338
  /**
325
339
  * Async wrapper around findTools that fuses BM25 results with embedding RRF
@@ -360,6 +374,7 @@ export async function findToolsWithEmbedding(query) {
360
374
  }
361
375
  return [...allEntries.values()]
362
376
  .filter((e) => fusedScores.has(e.name))
363
- .sort((a, b) => (fusedScores.get(b.name) ?? 0) - (fusedScores.get(a.name) ?? 0));
377
+ .sort((a, b) => (fusedScores.get(b.name) ?? 0) - (fusedScores.get(a.name) ?? 0))
378
+ .map((e) => ({ ...e, _score: fusedScores.get(e.name) ?? 0 }));
364
379
  }
365
380
  //# sourceMappingURL=toolRegistry.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homenshum/convex-mcp-nodebench",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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": {