@homenshum/convex-mcp-nodebench 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,41 +1,59 @@
1
1
  # convex-mcp-nodebench
2
2
 
3
- 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.
3
+ Convex-specific MCP server applying NodeBench self-instruct diligence patterns to Convex development. Schema audit, function compliance, HTTP endpoint analysis, deployment gates, persistent gotcha DB, and methodology guidance.
4
4
 
5
5
  **Complements** Context7 (raw library docs) and the official Convex MCP (deployment introspection) with structured verification workflows and persistent Convex knowledge.
6
6
 
7
- ## 11 Tools across 5 Categories
7
+ ## 17 Tools across 8 Categories
8
8
 
9
9
  ### Schema Tools
10
10
  | Tool | Description |
11
11
  |------|-------------|
12
- | `convex_audit_schema` | Scan schema.ts for anti-patterns: deprecated validators, reserved field names, missing indexes, naming violations |
12
+ | `convex_audit_schema` | Scan schema.ts for anti-patterns: deprecated validators (`v.bigint()`), `v.any()` usage, reserved field names, missing indexes, naming conventions |
13
13
  | `convex_suggest_indexes` | Analyze query patterns across all functions and suggest missing indexes |
14
14
  | `convex_check_validator_coverage` | Check all exported functions have args + returns validators |
15
15
 
16
16
  ### Function Tools
17
17
  | Tool | Description |
18
18
  |------|-------------|
19
- | `convex_audit_functions` | Audit function registration, missing validators, public/internal misuse, action anti-patterns |
19
+ | `convex_audit_functions` | Audit function registration, missing validators, public/internal misuse, action anti-patterns, **cross-call violations** (query calling runMutation/runAction) |
20
20
  | `convex_check_function_refs` | Validate api.x.y / internal.x.y references, detect direct function passing |
21
21
 
22
+ ### HTTP Tools
23
+ | Tool | Description |
24
+ |------|-------------|
25
+ | `convex_analyze_http` | Analyze convex/http.ts for duplicate routes, missing CORS headers, missing OPTIONS preflight handlers, missing httpRouter/httpAction imports |
26
+
22
27
  ### Deployment Tools
23
28
  | Tool | Description |
24
29
  |------|-------------|
25
- | `convex_pre_deploy_gate` | Pre-deployment quality gate: schema, auth, validators, recent audit results |
26
- | `convex_check_env_vars` | Check env vars referenced in code exist in .env files |
30
+ | `convex_pre_deploy_gate` | Pre-deployment quality gate: schema, auth, validators, recent audit results (only blocks on truly critical issues) |
31
+ | `convex_check_env_vars` | Check Convex-specific env vars referenced in code exist in .env files (filters out NODE_ENV, PATH, etc.) |
27
32
 
28
33
  ### Learning Tools
29
34
  | Tool | Description |
30
35
  |------|-------------|
31
36
  | `convex_record_gotcha` | Persist a Convex gotcha/edge case for future reference |
32
- | `convex_search_gotchas` | Full-text search across known Convex gotchas |
37
+ | `convex_search_gotchas` | Full-text search across known Convex gotchas (BM25 + FTS5) |
33
38
 
34
39
  ### Methodology Tools
35
40
  | Tool | Description |
36
41
  |------|-------------|
37
42
  | `convex_get_methodology` | Step-by-step guides: schema audit, function compliance, deploy verification, knowledge management |
38
- | `convex_discover_tools` | Search available tools by keyword, category, or task description |
43
+ | `convex_discover_tools` | BM25-scored tool discovery with optional embedding-enhanced semantic search |
44
+
45
+ ### Integration Bridge Tools
46
+ | Tool | Description |
47
+ |------|-------------|
48
+ | `convex_generate_rules_md` | Generate a Convex rules markdown file from gotcha DB, recent audits, and project stats |
49
+ | `convex_snapshot_schema` | Capture schema snapshot for diffing (tracks tables, indexes per table, size). Auto-diffs against previous snapshot |
50
+ | `convex_bootstrap_project` | Comprehensive project health scan: schema, auth, _generated, file count, improvement plan |
51
+
52
+ ### Infrastructure Tools
53
+ | Tool | Description |
54
+ |------|-------------|
55
+ | `convex_check_crons` | Validate crons.ts: duplicate names, public handlers, interval issues |
56
+ | `convex_analyze_components` | Parse convex.config.ts: active/conditional components, unused imports |
39
57
 
40
58
  ## Self-Instruct QuickRefs
41
59
 
@@ -53,20 +71,35 @@ Every tool response includes a `quickRef` block telling the agent what to do nex
53
71
 
54
72
  ## Pre-Seeded Gotcha Database
55
73
 
56
- Ships with 20 gotchas extracted from Convex best practices:
74
+ Ships with 32 gotchas extracted from Convex best practices, auto-upserted on upgrade:
57
75
 
76
+ ### Critical
77
+ - `pagination_cursor_null_first` -- First paginate() call must pass cursor: null
78
+ - `query_no_side_effects` -- Queries CANNOT call runMutation or runAction (runtime error)
79
+ - `use_node_for_external_api` -- Actions calling external APIs need `"use node"` directive
58
80
  - `validator_bigint_deprecated` -- Use v.int64() not v.bigint()
59
- - `undefined_not_valid` -- Use null, never undefined
60
- - `index_field_order` -- Query fields must match index definition order
61
- - `no_map_set_validators` -- Use v.record() instead
62
- - `returns_validator_required` -- Every function needs a returns validator
63
- - `action_from_action` -- Only cross-runtime, otherwise use helpers
64
- - And 14 more...
81
+
82
+ ### Warnings
83
+ - `ctx_auth_returns_null` -- getUserIdentity() returns null when unauthenticated
84
+ - `http_cors_manual` -- CORS headers must be added manually to HTTP endpoints
85
+ - `http_route_no_wildcard` -- HTTP routes use exact path matching, no wildcards
86
+ - `avoid_v_any` -- v.any() defeats the purpose of validators
87
+ - `mutation_transaction_atomicity` -- Each mutation is a separate transaction
88
+ - `db_get_returns_null` -- ctx.db.get() returns null if document missing
89
+ - `storage_get_returns_null` -- ctx.storage.get() returns null if file missing
90
+ - `convex_1mb_document_limit` -- Documents cannot exceed 1MB
91
+ - `scheduled_function_must_be_internal` -- Scheduled functions should be internal
92
+ - And 19 more covering index ordering, undefined handling, field naming, action patterns...
65
93
 
66
94
  ## Quick Start
67
95
 
68
96
  ### Install
69
97
  ```bash
98
+ npm install @homenshum/convex-mcp-nodebench
99
+ ```
100
+
101
+ Or from source:
102
+ ```bash
70
103
  cd packages/convex-mcp-nodebench
71
104
  npm install
72
105
  npm run build
@@ -74,7 +107,7 @@ npm run build
74
107
 
75
108
  ### Run (stdio)
76
109
  ```bash
77
- node dist/index.js
110
+ npx convex-mcp-nodebench
78
111
  ```
79
112
 
80
113
  ### Dev mode
@@ -87,8 +120,8 @@ npx tsx src/index.ts
87
120
  {
88
121
  "mcpServers": {
89
122
  "convex-mcp-nodebench": {
90
- "command": "node",
91
- "args": ["/path/to/packages/convex-mcp-nodebench/dist/index.js"]
123
+ "command": "npx",
124
+ "args": ["@homenshum/convex-mcp-nodebench"]
92
125
  }
93
126
  }
94
127
  }
@@ -96,15 +129,15 @@ npx tsx src/index.ts
96
129
 
97
130
  ### First prompt
98
131
  ```
99
- Search convex gotchas for "validator" then audit the schema at /path/to/my-project
132
+ Search convex gotchas for "pagination" then audit the schema at /path/to/my-project
100
133
  ```
101
134
 
102
135
  ## Data Storage
103
136
 
104
137
  Persistent SQLite database at `~/.convex-mcp-nodebench/convex.db`:
105
138
 
106
- - **convex_gotchas** -- Gotcha knowledge base with FTS5 search
107
- - **schema_snapshots** -- Schema history for diffing
139
+ - **convex_gotchas** -- Gotcha knowledge base with FTS5 full-text search
140
+ - **schema_snapshots** -- Schema history for table + index diffing
108
141
  - **deploy_checks** -- Deployment gate audit trail
109
142
  - **audit_results** -- Per-file analysis cache
110
143
 
@@ -114,24 +147,49 @@ Persistent SQLite database at `~/.convex-mcp-nodebench/convex.db`:
114
147
  npm test
115
148
  ```
116
149
 
117
- All 15 tests verify tools work against the real nodebench-ai codebase (3,138 Convex functions).
150
+ 30 tests verify all 17 tools work against the real nodebench-ai codebase (3,147 Convex functions, 323 tables, 82 crons, 8 HTTP routes).
118
151
 
119
152
  ## Architecture
120
153
 
121
154
  ```
122
155
  packages/convex-mcp-nodebench/
123
156
  src/
124
- index.ts -- MCP server entry, tool assembly
125
- db.ts -- SQLite schema + seed logic
126
- types.ts -- Tool types, QuickRef interface
127
- gotchaSeed.ts -- 20 pre-seeded Convex gotchas
157
+ index.ts -- MCP server entry, tool assembly (17 tools)
158
+ db.ts -- SQLite schema + upsert seed logic
159
+ types.ts -- Tool types, QuickRef interface
160
+ gotchaSeed.ts -- 32 pre-seeded Convex gotchas
128
161
  tools/
129
- schemaTools.ts -- Schema audit, index suggestions, validator coverage
130
- functionTools.ts -- Function audit, reference checking
131
- deploymentTools.ts -- Pre-deploy gate, env var checking
132
- learningTools.ts -- Gotcha recording + search
133
- methodologyTools.ts -- Methodology guides, tool discovery
134
- toolRegistry.ts -- Central catalog with quickRef metadata
162
+ schemaTools.ts -- Schema audit, index suggestions, validator coverage
163
+ functionTools.ts -- Function audit, cross-call detection, reference checking
164
+ httpTools.ts -- HTTP endpoint analysis (routes, CORS, duplicates)
165
+ deploymentTools.ts -- Pre-deploy gate, env var checking (Convex-filtered)
166
+ learningTools.ts -- Gotcha recording + FTS5 search
167
+ methodologyTools.ts -- Methodology guides, BM25 tool discovery
168
+ integrationBridgeTools.ts -- Rules generation, schema snapshots with index diffing, project bootstrap
169
+ cronTools.ts -- Cron job validation
170
+ componentTools.ts -- Component config analysis
171
+ toolRegistry.ts -- Central catalog with quickRef metadata + BM25 scoring
172
+ embeddingProvider.ts -- Optional semantic search (Google/OpenAI embeddings)
135
173
  __tests__/
136
- tools.test.ts -- 15 integration tests
174
+ tools.test.ts -- 30 integration tests
137
175
  ```
176
+
177
+ ## Changelog
178
+
179
+ ### v0.3.0
180
+ - **New tool**: `convex_analyze_http` -- HTTP endpoint analysis (duplicate routes, CORS, OPTIONS handlers)
181
+ - **Cross-call detection**: queries calling `ctx.runMutation`/`ctx.runAction` flagged as critical
182
+ - **12 new gotchas**: pagination, ctx.auth, scheduled functions, HTTP routes, CORS, v.any(), storage, transactions, document limits, "use node"
183
+ - **Schema snapshot diffs** now track index additions/removals per table
184
+ - **env_vars** filtered to Convex-specific patterns (excludes NODE_ENV, PATH, HOME, etc.)
185
+ - **Gotcha seeding** upgraded to upsert -- existing users get new gotchas on upgrade
186
+ - **Bug fixes**: fnv1aHash missing in embeddingProvider, collectTsFilesFlat ESM compat
187
+
188
+ ### v0.2.0
189
+ - Schema audit noise reduced (836 -> 163 issues): index naming aggregated, v.any() detection added
190
+ - Function audit severity corrected: missing returns downgraded from critical to warning
191
+ - Deploy gate threshold: only blocks on >10 criticals
192
+ - npm tarball reduced from 45.9kB to 31.5kB
193
+
194
+ ### v0.1.0
195
+ - Initial release: 16 tools, 20 gotchas, BM25 discovery, embedding-enhanced search
package/dist/db.js CHANGED
@@ -111,13 +111,19 @@ export function genId(prefix) {
111
111
  }
112
112
  export function seedGotchasIfEmpty(gotchas) {
113
113
  const db = getDb();
114
- const count = db.prepare("SELECT COUNT(*) as c FROM convex_gotchas").get().c;
115
- if (count > 0)
116
- return;
117
- const insert = db.prepare("INSERT OR IGNORE INTO convex_gotchas (key, content, category, severity, tags, source) VALUES (?, ?, ?, ?, ?, 'seed')");
114
+ // Upsert: insert new seed gotchas, skip user-created ones (source != 'seed')
115
+ const upsert = db.prepare(`INSERT INTO convex_gotchas (key, content, category, severity, tags, source)
116
+ VALUES (?, ?, ?, ?, ?, 'seed')
117
+ ON CONFLICT(key) DO UPDATE SET
118
+ content = excluded.content,
119
+ category = excluded.category,
120
+ severity = excluded.severity,
121
+ tags = excluded.tags,
122
+ updated_at = datetime('now')
123
+ WHERE source = 'seed'`);
118
124
  const tx = db.transaction(() => {
119
125
  for (const g of gotchas) {
120
- insert.run(g.key, g.content, g.category, g.severity, g.tags);
126
+ upsert.run(g.key, g.content, g.category, g.severity, g.tags);
121
127
  }
122
128
  });
123
129
  tx();
@@ -122,5 +122,77 @@ export declare const CONVEX_GOTCHAS: readonly [{
122
122
  readonly category: "function";
123
123
  readonly severity: "warning";
124
124
  readonly tags: "action,query,mutation,transaction,race,condition";
125
+ }, {
126
+ readonly key: "pagination_cursor_null_first";
127
+ readonly content: "When using .paginate(), the first call must pass cursor: null (not undefined). Subsequent calls use the continueCursor from the previous response. The paginationOpts validator is paginationOptsValidator from 'convex/server'.";
128
+ readonly category: "function";
129
+ readonly severity: "critical";
130
+ readonly tags: "pagination,cursor,null,paginate,paginationOpts";
131
+ }, {
132
+ readonly key: "query_no_side_effects";
133
+ readonly content: "Queries (query/internalQuery) CANNOT call ctx.runMutation or ctx.runAction. They are read-only. Attempting to mutate from a query will throw a runtime error.";
134
+ readonly category: "function";
135
+ readonly severity: "critical";
136
+ readonly tags: "query,mutation,action,side-effect,read-only,runtime";
137
+ }, {
138
+ readonly key: "ctx_auth_returns_null";
139
+ readonly content: "ctx.auth.getUserIdentity() returns null when the user is not authenticated. Always null-check before accessing identity fields. Do NOT assume it returns a value.";
140
+ readonly category: "function";
141
+ readonly severity: "warning";
142
+ readonly tags: "auth,identity,null,getUserIdentity,authentication";
143
+ }, {
144
+ readonly key: "scheduled_function_must_be_internal";
145
+ readonly content: "Functions passed to ctx.scheduler.runAfter or ctx.scheduler.runAt should be internal functions (internalMutation/internalAction). Using public functions exposes them to the API.";
146
+ readonly category: "function";
147
+ readonly severity: "warning";
148
+ readonly tags: "scheduler,scheduled,runAfter,runAt,internal,cron";
149
+ }, {
150
+ readonly key: "http_route_no_wildcard";
151
+ readonly content: "Convex HTTP routes use exact path matching, not prefix/wildcard matching. /api/users does NOT match /api/users/123. Register separate routes for each path.";
152
+ readonly category: "function";
153
+ readonly severity: "warning";
154
+ readonly tags: "http,route,wildcard,path,exact,matching";
155
+ }, {
156
+ readonly key: "http_cors_manual";
157
+ readonly content: "Convex HTTP endpoints do not automatically handle CORS. You must manually add CORS headers (Access-Control-Allow-Origin, etc.) and handle OPTIONS preflight requests.";
158
+ readonly category: "function";
159
+ readonly severity: "warning";
160
+ readonly tags: "http,cors,headers,options,preflight,origin";
161
+ }, {
162
+ readonly key: "avoid_v_any";
163
+ readonly content: "v.any() defeats the purpose of validators — it accepts any value and provides no type safety. Use specific validators (v.string(), v.object({...}), v.union(...)) instead.";
164
+ readonly category: "validator";
165
+ readonly severity: "warning";
166
+ readonly tags: "any,validator,type-safety,schema";
167
+ }, {
168
+ readonly key: "storage_get_returns_null";
169
+ readonly content: "ctx.storage.get(storageId) returns null if the file doesn't exist. Always null-check the result before using it. Also, storage IDs are typed as Id<'_storage'>.";
170
+ readonly category: "function";
171
+ readonly severity: "warning";
172
+ readonly tags: "storage,get,null,file,upload,_storage";
173
+ }, {
174
+ readonly key: "mutation_transaction_atomicity";
175
+ readonly content: "Each mutation runs as a single transaction. If you call ctx.runMutation multiple times from an action, each is a separate transaction — there's no cross-mutation atomicity. Design mutations to be self-contained.";
176
+ readonly category: "function";
177
+ readonly severity: "warning";
178
+ readonly tags: "mutation,transaction,atomicity,action,consistency";
179
+ }, {
180
+ readonly key: "db_get_returns_null";
181
+ readonly content: "ctx.db.get(id) returns null if the document doesn't exist or was deleted. Always null-check before accessing fields. TypeScript won't catch this at compile time.";
182
+ readonly category: "function";
183
+ readonly severity: "warning";
184
+ readonly tags: "db,get,null,document,deleted";
185
+ }, {
186
+ readonly key: "convex_1mb_document_limit";
187
+ readonly content: "Individual Convex documents cannot exceed 1MB. Large text, arrays of objects, or embedded binary data can exceed this. Split large data across multiple documents or use file storage.";
188
+ readonly category: "schema";
189
+ readonly severity: "warning";
190
+ readonly tags: "document,size,limit,1mb,split,storage";
191
+ }, {
192
+ readonly key: "use_node_for_external_api";
193
+ readonly content: "Convex actions that call external APIs (fetch, HTTP clients) must use the Node.js runtime. Add 'use node' at the top of the file. V8 runtime does not support fetch to external URLs.";
194
+ readonly category: "function";
195
+ readonly severity: "critical";
196
+ readonly tags: "node,runtime,fetch,external,api,use-node";
125
197
  }];
126
198
  export type ConvexGotcha = typeof CONVEX_GOTCHAS[number];
@@ -143,5 +143,89 @@ export const CONVEX_GOTCHAS = [
143
143
  severity: "warning",
144
144
  tags: "action,query,mutation,transaction,race,condition",
145
145
  },
146
+ {
147
+ key: "pagination_cursor_null_first",
148
+ content: "When using .paginate(), the first call must pass cursor: null (not undefined). Subsequent calls use the continueCursor from the previous response. The paginationOpts validator is paginationOptsValidator from 'convex/server'.",
149
+ category: "function",
150
+ severity: "critical",
151
+ tags: "pagination,cursor,null,paginate,paginationOpts",
152
+ },
153
+ {
154
+ key: "query_no_side_effects",
155
+ content: "Queries (query/internalQuery) CANNOT call ctx.runMutation or ctx.runAction. They are read-only. Attempting to mutate from a query will throw a runtime error.",
156
+ category: "function",
157
+ severity: "critical",
158
+ tags: "query,mutation,action,side-effect,read-only,runtime",
159
+ },
160
+ {
161
+ key: "ctx_auth_returns_null",
162
+ content: "ctx.auth.getUserIdentity() returns null when the user is not authenticated. Always null-check before accessing identity fields. Do NOT assume it returns a value.",
163
+ category: "function",
164
+ severity: "warning",
165
+ tags: "auth,identity,null,getUserIdentity,authentication",
166
+ },
167
+ {
168
+ key: "scheduled_function_must_be_internal",
169
+ content: "Functions passed to ctx.scheduler.runAfter or ctx.scheduler.runAt should be internal functions (internalMutation/internalAction). Using public functions exposes them to the API.",
170
+ category: "function",
171
+ severity: "warning",
172
+ tags: "scheduler,scheduled,runAfter,runAt,internal,cron",
173
+ },
174
+ {
175
+ key: "http_route_no_wildcard",
176
+ content: "Convex HTTP routes use exact path matching, not prefix/wildcard matching. /api/users does NOT match /api/users/123. Register separate routes for each path.",
177
+ category: "function",
178
+ severity: "warning",
179
+ tags: "http,route,wildcard,path,exact,matching",
180
+ },
181
+ {
182
+ key: "http_cors_manual",
183
+ content: "Convex HTTP endpoints do not automatically handle CORS. You must manually add CORS headers (Access-Control-Allow-Origin, etc.) and handle OPTIONS preflight requests.",
184
+ category: "function",
185
+ severity: "warning",
186
+ tags: "http,cors,headers,options,preflight,origin",
187
+ },
188
+ {
189
+ key: "avoid_v_any",
190
+ content: "v.any() defeats the purpose of validators — it accepts any value and provides no type safety. Use specific validators (v.string(), v.object({...}), v.union(...)) instead.",
191
+ category: "validator",
192
+ severity: "warning",
193
+ tags: "any,validator,type-safety,schema",
194
+ },
195
+ {
196
+ key: "storage_get_returns_null",
197
+ content: "ctx.storage.get(storageId) returns null if the file doesn't exist. Always null-check the result before using it. Also, storage IDs are typed as Id<'_storage'>.",
198
+ category: "function",
199
+ severity: "warning",
200
+ tags: "storage,get,null,file,upload,_storage",
201
+ },
202
+ {
203
+ key: "mutation_transaction_atomicity",
204
+ content: "Each mutation runs as a single transaction. If you call ctx.runMutation multiple times from an action, each is a separate transaction — there's no cross-mutation atomicity. Design mutations to be self-contained.",
205
+ category: "function",
206
+ severity: "warning",
207
+ tags: "mutation,transaction,atomicity,action,consistency",
208
+ },
209
+ {
210
+ key: "db_get_returns_null",
211
+ content: "ctx.db.get(id) returns null if the document doesn't exist or was deleted. Always null-check before accessing fields. TypeScript won't catch this at compile time.",
212
+ category: "function",
213
+ severity: "warning",
214
+ tags: "db,get,null,document,deleted",
215
+ },
216
+ {
217
+ key: "convex_1mb_document_limit",
218
+ content: "Individual Convex documents cannot exceed 1MB. Large text, arrays of objects, or embedded binary data can exceed this. Split large data across multiple documents or use file storage.",
219
+ category: "schema",
220
+ severity: "warning",
221
+ tags: "document,size,limit,1mb,split,storage",
222
+ },
223
+ {
224
+ key: "use_node_for_external_api",
225
+ content: "Convex actions that call external APIs (fetch, HTTP clients) must use the Node.js runtime. Add 'use node' at the top of the file. V8 runtime does not support fetch to external URLs.",
226
+ category: "function",
227
+ severity: "critical",
228
+ tags: "node,runtime,fetch,external,api,use-node",
229
+ },
146
230
  ];
147
231
  //# sourceMappingURL=gotchaSeed.js.map
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { methodologyTools } from "./tools/methodologyTools.js";
24
24
  import { integrationBridgeTools } from "./tools/integrationBridgeTools.js";
25
25
  import { cronTools } from "./tools/cronTools.js";
26
26
  import { componentTools } from "./tools/componentTools.js";
27
+ import { httpTools } from "./tools/httpTools.js";
27
28
  import { CONVEX_GOTCHAS } from "./gotchaSeed.js";
28
29
  import { REGISTRY } from "./tools/toolRegistry.js";
29
30
  import { initEmbeddingIndex } from "./tools/embeddingProvider.js";
@@ -37,6 +38,7 @@ const ALL_TOOLS = [
37
38
  ...integrationBridgeTools,
38
39
  ...cronTools,
39
40
  ...componentTools,
41
+ ...httpTools,
40
42
  ];
41
43
  const toolMap = new Map();
42
44
  for (const tool of ALL_TOOLS) {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { getDb, genId } from "../db.js";
4
4
  import { getQuickRef } from "./toolRegistry.js";
@@ -126,7 +126,13 @@ function checkEnvVars(projectDir) {
126
126
  result.envVarsInEnvFile.push(...vars.map((v) => v.replace("=", "")));
127
127
  }
128
128
  }
129
- // Scan convex/ for process.env references
129
+ // Scan convex/ for process.env references (filter out non-Convex vars)
130
+ const NON_CONVEX_VARS = new Set([
131
+ "NODE_ENV", "HOME", "PATH", "SHELL", "USER", "LANG", "TERM",
132
+ "HOSTNAME", "PWD", "TMPDIR", "TMP", "TEMP", "CI", "DEBUG",
133
+ "LOG_LEVEL", "VERBOSE", "PORT", "HOST", "NODE_PATH",
134
+ "npm_config_registry", "npm_lifecycle_event",
135
+ ]);
130
136
  const convexDir = findConvexDir(projectDir);
131
137
  if (convexDir) {
132
138
  const files = collectTsFilesFlat(convexDir);
@@ -136,7 +142,11 @@ function checkEnvVars(projectDir) {
136
142
  const content = readFileSync(filePath, "utf-8");
137
143
  let m;
138
144
  while ((m = envPattern.exec(content)) !== null) {
139
- codeEnvVars.add(m[1]);
145
+ const varName = m[1];
146
+ // Skip common Node/OS vars that aren't Convex env vars
147
+ if (!NON_CONVEX_VARS.has(varName)) {
148
+ codeEnvVars.add(varName);
149
+ }
140
150
  }
141
151
  }
142
152
  result.envVarsInCode = [...codeEnvVars];
@@ -157,7 +167,6 @@ function collectTsFilesFlat(dir) {
157
167
  const results = [];
158
168
  if (!existsSync(dir))
159
169
  return results;
160
- const { readdirSync } = require("node:fs");
161
170
  const entries = readdirSync(dir, { withFileTypes: true });
162
171
  for (const entry of entries) {
163
172
  const full = join(dir, entry.name);
@@ -14,6 +14,15 @@ let _provider = null;
14
14
  let _providerChecked = false;
15
15
  let _embeddingIndex = null;
16
16
  let _initPromise = null;
17
+ // Simple FNV-1a hash for corpus change detection
18
+ function fnv1aHash(str) {
19
+ let hash = 0x811c9dc5;
20
+ for (let i = 0; i < str.length; i++) {
21
+ hash ^= str.charCodeAt(i);
22
+ hash = (hash * 0x01000193) >>> 0;
23
+ }
24
+ return hash.toString(36);
25
+ }
17
26
  const CACHE_VERSION = 1;
18
27
  const CACHE_DIR = join(homedir(), ".convex-mcp-nodebench");
19
28
  const CACHE_FILE = join(CACHE_DIR, "embedding_cache.json");
@@ -99,12 +108,14 @@ async function _doInit(corpus) {
99
108
  const provider = await getEmbeddingProvider();
100
109
  if (!provider)
101
110
  return;
111
+ const hash = fnv1aHash(corpus.map((c) => c.text).join("\n"));
102
112
  const cached = loadCache();
103
113
  if (cached &&
104
114
  cached.providerName === provider.name &&
105
115
  cached.dimensions === provider.dimensions &&
106
116
  cached.version === CACHE_VERSION &&
107
117
  cached.toolCount === corpus.length &&
118
+ cached.corpusHash === hash &&
108
119
  corpus.every((c) => c.name in cached.entries)) {
109
120
  _embeddingIndex = corpus.map((c) => ({
110
121
  name: c.name,
@@ -124,6 +135,7 @@ async function _doInit(corpus) {
124
135
  dimensions: provider.dimensions,
125
136
  version: CACHE_VERSION,
126
137
  toolCount: corpus.length,
138
+ corpusHash: hash,
127
139
  entries: {},
128
140
  };
129
141
  for (let i = 0; i < corpus.length; i++) {
@@ -146,6 +146,34 @@ function auditFunctions(convexDir) {
146
146
  });
147
147
  }
148
148
  }
149
+ // Check 4: Cross-call violations — queries CANNOT call runMutation or runAction
150
+ for (const fn of functions) {
151
+ if (fn.type !== "query" && fn.type !== "internalQuery")
152
+ continue;
153
+ const content = readFileSync(fn.filePath, "utf-8");
154
+ const lines = content.split("\n");
155
+ // Find the function body (rough: from export line to next export or end)
156
+ const startLine = fn.line - 1;
157
+ const chunk = lines.slice(startLine, Math.min(startLine + 80, lines.length)).join("\n");
158
+ if (/ctx\.runMutation/.test(chunk)) {
159
+ issues.push({
160
+ severity: "critical",
161
+ location: `${fn.relativePath}:${fn.line}`,
162
+ functionName: fn.name,
163
+ message: `${fn.type} "${fn.name}" calls ctx.runMutation — queries cannot mutate data. This will throw at runtime.`,
164
+ fix: "Move the mutation call to a mutation or action function",
165
+ });
166
+ }
167
+ if (/ctx\.runAction/.test(chunk)) {
168
+ issues.push({
169
+ severity: "critical",
170
+ location: `${fn.relativePath}:${fn.line}`,
171
+ functionName: fn.name,
172
+ message: `${fn.type} "${fn.name}" calls ctx.runAction — queries cannot call actions. This will throw at runtime.`,
173
+ fix: "Move the action call to an action function",
174
+ });
175
+ }
176
+ }
149
177
  return issues;
150
178
  }
151
179
  function checkFunctionRefs(convexDir) {
@@ -0,0 +1,2 @@
1
+ import type { McpTool } from "../types.js";
2
+ export declare const httpTools: McpTool[];
@@ -0,0 +1,168 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { getQuickRef } from "./toolRegistry.js";
4
+ // ── Helpers ──────────────────────────────────────────────────────────
5
+ function findConvexDir(projectDir) {
6
+ const candidates = [join(projectDir, "convex"), join(projectDir, "src", "convex")];
7
+ for (const c of candidates) {
8
+ if (existsSync(c))
9
+ return c;
10
+ }
11
+ return null;
12
+ }
13
+ function analyzeHttpEndpoints(convexDir) {
14
+ const httpPath = join(convexDir, "http.ts");
15
+ if (!existsSync(httpPath)) {
16
+ return { hasHttp: false, routes: [], issues: [], hasCors: false, hasOptionsHandler: false };
17
+ }
18
+ const content = readFileSync(httpPath, "utf-8");
19
+ const lines = content.split("\n");
20
+ const routes = [];
21
+ const issues = [];
22
+ // Extract routes: http.route({ path: "...", method: "...", handler: ... })
23
+ const routeBlockPattern = /http\.route\s*\(\s*\{/g;
24
+ let routeMatch;
25
+ while ((routeMatch = routeBlockPattern.exec(content)) !== null) {
26
+ const startIdx = routeMatch.index;
27
+ // Find the closing of this route block (rough — find next })
28
+ let depth = 0;
29
+ let endIdx = startIdx;
30
+ for (let i = startIdx; i < content.length; i++) {
31
+ if (content[i] === "{")
32
+ depth++;
33
+ if (content[i] === "}") {
34
+ depth--;
35
+ if (depth === 0) {
36
+ endIdx = i;
37
+ break;
38
+ }
39
+ }
40
+ }
41
+ const block = content.slice(startIdx, endIdx + 1);
42
+ const pathMatch = block.match(/path\s*:\s*["']([^"']+)["']/);
43
+ const methodMatch = block.match(/method\s*:\s*["']([^"']+)["']/);
44
+ const line = content.slice(0, startIdx).split("\n").length;
45
+ if (pathMatch && methodMatch) {
46
+ routes.push({
47
+ path: pathMatch[1],
48
+ method: methodMatch[1].toUpperCase(),
49
+ line,
50
+ handlerType: /handler\s*:\s*httpAction/.test(block) ? "inline" : "imported",
51
+ });
52
+ }
53
+ }
54
+ // Check for duplicate routes (same path + method)
55
+ const routeKeys = new Map();
56
+ for (const route of routes) {
57
+ const key = `${route.method} ${route.path}`;
58
+ const count = (routeKeys.get(key) || 0) + 1;
59
+ routeKeys.set(key, count);
60
+ if (count === 2) {
61
+ issues.push({
62
+ severity: "critical",
63
+ message: `Duplicate route: ${key} — only the last registration will be used`,
64
+ location: `http.ts:${route.line}`,
65
+ });
66
+ }
67
+ }
68
+ // Check for CORS handling
69
+ const hasCors = /Access-Control-Allow-Origin/i.test(content) ||
70
+ /cors/i.test(content);
71
+ const hasOptionsHandler = routes.some((r) => r.method === "OPTIONS");
72
+ if (!hasCors && routes.length > 0) {
73
+ issues.push({
74
+ severity: "warning",
75
+ message: "No CORS headers detected. Browser requests from different origins will fail. Add Access-Control-Allow-Origin headers.",
76
+ });
77
+ }
78
+ if (hasCors && !hasOptionsHandler) {
79
+ issues.push({
80
+ severity: "warning",
81
+ message: "CORS headers found but no OPTIONS handler registered. Preflight requests will fail. Add http.route({ path: '...', method: 'OPTIONS', handler: ... }).",
82
+ });
83
+ }
84
+ // Check for paths that look like they should be grouped
85
+ const pathPrefixes = new Map();
86
+ for (const route of routes) {
87
+ const parts = route.path.split("/").filter(Boolean);
88
+ if (parts.length >= 2) {
89
+ const prefix = `/${parts[0]}/${parts[1]}`;
90
+ pathPrefixes.set(prefix, (pathPrefixes.get(prefix) || 0) + 1);
91
+ }
92
+ }
93
+ // Check for missing httpRouter import
94
+ if (!/httpRouter/.test(content)) {
95
+ issues.push({
96
+ severity: "critical",
97
+ message: "Missing httpRouter import. HTTP endpoints require: import { httpRouter } from 'convex/server';",
98
+ });
99
+ }
100
+ // Check for export default
101
+ if (!/export\s+default\s+http/.test(content)) {
102
+ issues.push({
103
+ severity: "critical",
104
+ message: "Missing 'export default http'. The httpRouter must be exported as default.",
105
+ });
106
+ }
107
+ // Check for httpAction import
108
+ if (!/httpAction/.test(content) && routes.length > 0) {
109
+ issues.push({
110
+ severity: "warning",
111
+ message: "No httpAction usage found. HTTP route handlers should use httpAction().",
112
+ });
113
+ }
114
+ return { hasHttp: true, routes, issues, hasCors, hasOptionsHandler };
115
+ }
116
+ // ── Tool Definitions ────────────────────────────────────────────────
117
+ export const httpTools = [
118
+ {
119
+ name: "convex_analyze_http",
120
+ description: "Analyze convex/http.ts for HTTP endpoint issues: duplicate routes, missing CORS headers, missing OPTIONS preflight handlers, missing httpRouter/httpAction imports, and missing default export. Returns route inventory with methods and paths.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ projectDir: {
125
+ type: "string",
126
+ description: "Absolute path to the project root containing a convex/ directory",
127
+ },
128
+ },
129
+ required: ["projectDir"],
130
+ },
131
+ handler: async (args) => {
132
+ const projectDir = resolve(args.projectDir);
133
+ const convexDir = findConvexDir(projectDir);
134
+ if (!convexDir) {
135
+ return { error: "No convex/ directory found" };
136
+ }
137
+ const result = analyzeHttpEndpoints(convexDir);
138
+ if (!result.hasHttp) {
139
+ return {
140
+ hasHttp: false,
141
+ message: "No convex/http.ts found — project has no HTTP endpoints",
142
+ quickRef: getQuickRef("convex_analyze_http"),
143
+ };
144
+ }
145
+ // Group routes by path prefix
146
+ const byMethod = {};
147
+ for (const r of result.routes) {
148
+ byMethod[r.method] = (byMethod[r.method] || 0) + 1;
149
+ }
150
+ return {
151
+ hasHttp: true,
152
+ totalRoutes: result.routes.length,
153
+ byMethod,
154
+ hasCors: result.hasCors,
155
+ hasOptionsHandler: result.hasOptionsHandler,
156
+ routes: result.routes,
157
+ issues: {
158
+ total: result.issues.length,
159
+ critical: result.issues.filter((i) => i.severity === "critical").length,
160
+ warnings: result.issues.filter((i) => i.severity === "warning").length,
161
+ details: result.issues,
162
+ },
163
+ quickRef: getQuickRef("convex_analyze_http"),
164
+ };
165
+ },
166
+ },
167
+ ];
168
+ //# sourceMappingURL=httpTools.js.map
@@ -110,8 +110,27 @@ function captureSchemaSnapshot(projectDir) {
110
110
  while ((m = tablePattern.exec(schemaContent)) !== null) {
111
111
  tables.push(m[1]);
112
112
  }
113
+ // Extract indexes per table
114
+ const indexMap = {};
115
+ const indexPattern = /\.index\s*\(\s*["']([^"']+)["']\s*,\s*\[([^\]]*)\]\s*\)/g;
116
+ let currentTable = "";
117
+ const lines = schemaContent.split("\n");
118
+ for (let i = 0; i < lines.length; i++) {
119
+ const tableDef = lines[i].match(/(\w+)\s*[:=]\s*defineTable\s*\(/);
120
+ if (tableDef)
121
+ currentTable = tableDef[1];
122
+ const idxMatch = lines[i].match(/\.index\s*\(\s*["']([^"']+)["']/);
123
+ if (idxMatch && currentTable) {
124
+ if (!indexMap[currentTable])
125
+ indexMap[currentTable] = [];
126
+ indexMap[currentTable].push(idxMatch[1]);
127
+ }
128
+ }
129
+ const totalIndexes = Object.values(indexMap).reduce((sum, arr) => sum + arr.length, 0);
113
130
  const schemaJson = JSON.stringify({
114
131
  tables,
132
+ indexes: indexMap,
133
+ totalIndexes,
115
134
  rawLength: schemaContent.length,
116
135
  capturedAt: new Date().toISOString(),
117
136
  });
@@ -188,10 +207,29 @@ export const integrationBridgeTools = [
188
207
  const curr = JSON.parse(snapshot.schemaJson);
189
208
  const addedTables = curr.tables.filter((t) => !prev.tables.includes(t));
190
209
  const removedTables = prev.tables.filter((t) => !curr.tables.includes(t));
210
+ // Index diff
211
+ const prevIdx = prev.indexes || {};
212
+ const currIdx = curr.indexes || {};
213
+ const addedIndexes = [];
214
+ const removedIndexes = [];
215
+ const allTables = new Set([...Object.keys(prevIdx), ...Object.keys(currIdx)]);
216
+ for (const table of allTables) {
217
+ const pSet = new Set(prevIdx[table] || []);
218
+ const cSet = new Set(currIdx[table] || []);
219
+ for (const idx of cSet)
220
+ if (!pSet.has(idx))
221
+ addedIndexes.push(`${table}.${idx}`);
222
+ for (const idx of pSet)
223
+ if (!cSet.has(idx))
224
+ removedIndexes.push(`${table}.${idx}`);
225
+ }
191
226
  diff = {
192
227
  previousSnapshot: previous.snapshot_at,
193
228
  addedTables,
194
229
  removedTables,
230
+ addedIndexes,
231
+ removedIndexes,
232
+ indexCountChange: (curr.totalIndexes || 0) - (prev.totalIndexes || 0),
195
233
  sizeChange: curr.rawLength - prev.rawLength,
196
234
  };
197
235
  }
@@ -231,6 +231,21 @@ export const REGISTRY = [
231
231
  phase: "audit",
232
232
  complexity: "low",
233
233
  },
234
+ // ── HTTP Tools ────────────
235
+ {
236
+ name: "convex_analyze_http",
237
+ category: "function",
238
+ tags: ["http", "endpoint", "route", "cors", "api", "rest", "options", "preflight"],
239
+ quickRef: {
240
+ nextAction: "Fix duplicate routes and add CORS headers before deploy",
241
+ nextTools: ["convex_pre_deploy_gate", "convex_audit_functions"],
242
+ methodology: "convex_deploy_verification",
243
+ relatedGotchas: ["http_exact_path", "http_route_no_wildcard", "http_cors_manual"],
244
+ confidence: "high",
245
+ },
246
+ phase: "audit",
247
+ complexity: "low",
248
+ },
234
249
  ];
235
250
  export function getQuickRef(toolName) {
236
251
  const entry = REGISTRY.find((e) => e.name === toolName);
@@ -323,15 +338,15 @@ export async function findToolsWithEmbedding(query) {
323
338
  // RRF fusion: combine BM25 rank with embedding rank
324
339
  const fusedScores = new Map();
325
340
  bm25Results.forEach((entry, i) => {
326
- const bm25Rrf = 1000 / (60 + i + 1);
341
+ const bm25Rrf = 1000 / (20 + i + 1);
327
342
  const embRank = vecRanks.get(entry.name);
328
- const embRrf = embRank ? 1000 / (60 + embRank) : 0;
343
+ const embRrf = embRank ? 1000 / (20 + embRank) : 0;
329
344
  fusedScores.set(entry.name, bm25Rrf + embRrf);
330
345
  });
331
346
  // Also include embedding-only hits not in BM25 results
332
347
  for (const [name, rank] of vecRanks) {
333
348
  if (!fusedScores.has(name)) {
334
- const embRrf = 1000 / (60 + rank);
349
+ const embRrf = 1000 / (20 + rank);
335
350
  fusedScores.set(name, embRrf);
336
351
  }
337
352
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homenshum/convex-mcp-nodebench",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {