@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 +91 -33
- package/dist/db.js +11 -5
- package/dist/gotchaSeed.d.ts +72 -0
- package/dist/gotchaSeed.js +84 -0
- package/dist/index.js +2 -0
- package/dist/tools/deploymentTools.js +13 -4
- package/dist/tools/embeddingProvider.js +12 -0
- package/dist/tools/functionTools.js +28 -0
- package/dist/tools/httpTools.d.ts +2 -0
- package/dist/tools/httpTools.js +168 -0
- package/dist/tools/integrationBridgeTools.js +38 -0
- package/dist/tools/toolRegistry.js +18 -3
- package/package.json +1 -1
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
|
-
##
|
|
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
|
|
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` |
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
-
|
|
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
|
-
|
|
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": "
|
|
91
|
-
"args": ["/
|
|
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 "
|
|
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
|
-
|
|
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
|
|
125
|
-
db.ts
|
|
126
|
-
types.ts
|
|
127
|
-
gotchaSeed.ts
|
|
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
|
|
130
|
-
functionTools.ts
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
126
|
+
upsert.run(g.key, g.content, g.category, g.severity, g.tags);
|
|
121
127
|
}
|
|
122
128
|
});
|
|
123
129
|
tx();
|
package/dist/gotchaSeed.d.ts
CHANGED
|
@@ -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];
|
package/dist/gotchaSeed.js
CHANGED
|
@@ -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
|
-
|
|
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,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 / (
|
|
341
|
+
const bm25Rrf = 1000 / (20 + i + 1);
|
|
327
342
|
const embRank = vecRanks.get(entry.name);
|
|
328
|
-
const embRrf = embRank ? 1000 / (
|
|
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 / (
|
|
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.
|
|
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": {
|