@everystack/mcp 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adding-database.md +36 -23
- package/dist/cli.md +44 -3
- package/dist/core.md +19 -1
- package/dist/database-operations.md +236 -0
- package/dist/derived-objects.md +225 -0
- package/dist/gates/detectors/embedded-data-bundle.d.ts +13 -0
- package/dist/gates/detectors/embedded-data-bundle.d.ts.map +1 -0
- package/dist/gates/detectors/embedded-data-bundle.js +56 -0
- package/dist/gates/detectors/hand-written-migration.d.ts +24 -0
- package/dist/gates/detectors/hand-written-migration.d.ts.map +1 -0
- package/dist/gates/detectors/hand-written-migration.js +54 -0
- package/dist/gates/detectors/secret-in-public-env.d.ts +12 -0
- package/dist/gates/detectors/secret-in-public-env.d.ts.map +1 -0
- package/dist/gates/detectors/secret-in-public-env.js +38 -0
- package/dist/gates/engine.d.ts +28 -0
- package/dist/gates/engine.d.ts.map +1 -0
- package/dist/gates/engine.js +74 -0
- package/dist/gates/registry.d.ts +14 -0
- package/dist/gates/registry.d.ts.map +1 -0
- package/dist/gates/registry.js +21 -0
- package/dist/gates/telemetry.d.ts +47 -0
- package/dist/gates/telemetry.d.ts.map +1 -0
- package/dist/gates/telemetry.js +121 -0
- package/dist/gates/types.d.ts +67 -0
- package/dist/gates/types.d.ts.map +1 -0
- package/dist/gates/types.js +14 -0
- package/dist/governance/cli.d.ts +57 -0
- package/dist/governance/cli.d.ts.map +1 -0
- package/dist/governance/cli.js +169 -0
- package/dist/governance/grounding.d.ts +78 -0
- package/dist/governance/grounding.d.ts.map +1 -0
- package/dist/governance/grounding.js +299 -0
- package/dist/index.cjs +119 -107
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/project-claude-md.md +35 -18
- package/dist/prompts/add-feature.d.ts +3 -0
- package/dist/prompts/add-feature.d.ts.map +1 -0
- package/dist/prompts/add-feature.js +154 -0
- package/dist/prompts/claude-md.d.ts +12 -0
- package/dist/prompts/claude-md.d.ts.map +1 -0
- package/dist/prompts/claude-md.js +87 -0
- package/dist/prompts/debug.d.ts +3 -0
- package/dist/prompts/debug.d.ts.map +1 -0
- package/dist/prompts/debug.js +129 -0
- package/dist/prompts/deploy.d.ts +3 -0
- package/dist/prompts/deploy.d.ts.map +1 -0
- package/dist/prompts/deploy.js +118 -0
- package/dist/prompts/design-schema.d.ts +3 -0
- package/dist/prompts/design-schema.d.ts.map +1 -0
- package/dist/prompts/design-schema.js +97 -0
- package/dist/prompts/governance-setup.d.ts +9 -0
- package/dist/prompts/governance-setup.d.ts.map +1 -0
- package/dist/prompts/governance-setup.js +76 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +20 -0
- package/dist/prompts/new-app.d.ts +3 -0
- package/dist/prompts/new-app.d.ts.map +1 -0
- package/dist/prompts/new-app.js +203 -0
- package/dist/prompts/runbook.d.ts +12 -0
- package/dist/prompts/runbook.d.ts.map +1 -0
- package/dist/prompts/runbook.js +70 -0
- package/dist/prompts/secure.d.ts +3 -0
- package/dist/prompts/secure.d.ts.map +1 -0
- package/dist/prompts/secure.js +219 -0
- package/dist/resources/index.d.ts +12 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +182 -0
- package/dist/schema-patterns.md +92 -106
- package/dist/tools/check-environment.d.ts +25 -0
- package/dist/tools/check-environment.d.ts.map +1 -0
- package/dist/tools/check-environment.js +281 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/project-status.d.ts +28 -0
- package/dist/tools/project-status.d.ts.map +1 -0
- package/dist/tools/project-status.js +138 -0
- package/dist/tools/project-validate.d.ts +19 -0
- package/dist/tools/project-validate.d.ts.map +1 -0
- package/dist/tools/project-validate.js +323 -0
- package/dist/tools/schema-analyze.d.ts +46 -0
- package/dist/tools/schema-analyze.d.ts.map +1 -0
- package/dist/tools/schema-analyze.js +336 -0
- package/package.json +3 -3
- package/src/gates/detectors/hand-written-migration.ts +29 -13
- package/src/index.ts +1 -1
- package/src/prompts/add-feature.ts +4 -4
- package/src/prompts/claude-md.ts +4 -2
- package/src/prompts/debug.ts +3 -4
- package/src/prompts/deploy.ts +17 -8
- package/src/prompts/design-schema.ts +45 -59
- package/src/prompts/new-app.ts +21 -19
- package/src/prompts/secure.ts +3 -3
- package/src/resources/adding-database.md +36 -23
- package/src/resources/cli.md +44 -3
- package/src/resources/core.md +19 -1
- package/src/resources/database-operations.md +236 -0
- package/src/resources/derived-objects.md +225 -0
- package/src/resources/index.ts +15 -1
- package/src/resources/project-claude-md.md +35 -18
- package/src/resources/schema-patterns.md +92 -106
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Extract a brace-balanced block starting from an opening brace.
|
|
5
|
+
*/
|
|
6
|
+
function extractBraceBlock(source, openIndex) {
|
|
7
|
+
let depth = 0;
|
|
8
|
+
for (let i = openIndex; i < source.length; i++) {
|
|
9
|
+
if (source[i] === '{')
|
|
10
|
+
depth++;
|
|
11
|
+
if (source[i] === '}')
|
|
12
|
+
depth--;
|
|
13
|
+
if (depth === 0)
|
|
14
|
+
return source.slice(openIndex + 1, i);
|
|
15
|
+
}
|
|
16
|
+
return source.slice(openIndex + 1);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse pgTable declarations from a TypeScript source file.
|
|
20
|
+
* Uses regex + brace-depth extraction to handle nested objects (e.g., { withTimezone: true }).
|
|
21
|
+
*/
|
|
22
|
+
function parseTables(source, file) {
|
|
23
|
+
const tables = [];
|
|
24
|
+
// Find each pgTable declaration
|
|
25
|
+
const headerRegex = /export\s+const\s+(\w+)\s*=\s*pgTable\(\s*['"]([^'"]+)['"]\s*,\s*\{/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = headerRegex.exec(source)) !== null) {
|
|
28
|
+
const exportName = match[1];
|
|
29
|
+
const dbName = match[2];
|
|
30
|
+
// The opening brace position is at the end of the match minus 1
|
|
31
|
+
const openBrace = match.index + match[0].length - 1;
|
|
32
|
+
const columnsBlock = extractBraceBlock(source, openBrace);
|
|
33
|
+
const columns = parseColumns(columnsBlock);
|
|
34
|
+
tables.push({ name: dbName, exportName, columns, file });
|
|
35
|
+
}
|
|
36
|
+
return tables;
|
|
37
|
+
}
|
|
38
|
+
function parseColumns(block) {
|
|
39
|
+
const columns = [];
|
|
40
|
+
// Match: columnName: type('db_name', ...optional args...)...chain...
|
|
41
|
+
// The type call may have extra args after the string (e.g., timestamp('x', { withTimezone: true }))
|
|
42
|
+
const colRegex = /(\w+)\s*:\s*(\w+)\(\s*['"]([^'"]+)['"][^)]*\)([\s\S]*?)(?=\n\s*\w+\s*:|$)/g;
|
|
43
|
+
let match;
|
|
44
|
+
while ((match = colRegex.exec(block)) !== null) {
|
|
45
|
+
const name = match[1];
|
|
46
|
+
const type = match[2];
|
|
47
|
+
const dbName = match[3];
|
|
48
|
+
const rest = match[4];
|
|
49
|
+
const modifiers = [];
|
|
50
|
+
if (rest.includes('.primaryKey()'))
|
|
51
|
+
modifiers.push('primaryKey');
|
|
52
|
+
if (rest.includes('.notNull()'))
|
|
53
|
+
modifiers.push('notNull');
|
|
54
|
+
if (rest.includes('.unique()'))
|
|
55
|
+
modifiers.push('unique');
|
|
56
|
+
if (rest.includes('.defaultNow()'))
|
|
57
|
+
modifiers.push('defaultNow');
|
|
58
|
+
if (rest.includes('.defaultRandom()'))
|
|
59
|
+
modifiers.push('defaultRandom');
|
|
60
|
+
if (rest.includes('.default('))
|
|
61
|
+
modifiers.push('default');
|
|
62
|
+
if (rest.includes('.references('))
|
|
63
|
+
modifiers.push('references');
|
|
64
|
+
columns.push({ name, dbName, type, modifiers });
|
|
65
|
+
}
|
|
66
|
+
return columns;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse relations() declarations from source.
|
|
70
|
+
*/
|
|
71
|
+
function parseRelations(source) {
|
|
72
|
+
const relations = [];
|
|
73
|
+
// Match: export const nameRelations = relations(tableName, ({ one, many }) => ({
|
|
74
|
+
const relBlockRegex = /export\s+const\s+\w+\s*=\s*relations\(\s*(\w+)\s*,\s*\(\s*\{[^}]*\}\s*\)\s*=>\s*\(\{([\s\S]*?)\}\)\s*\)/g;
|
|
75
|
+
let blockMatch;
|
|
76
|
+
while ((blockMatch = relBlockRegex.exec(source)) !== null) {
|
|
77
|
+
const table = blockMatch[1];
|
|
78
|
+
const body = blockMatch[2];
|
|
79
|
+
// Match individual relations: name: one(target, { fields: [...], references: [...] })
|
|
80
|
+
const relRegex = /(\w+)\s*:\s*(one|many)\(\s*(\w+)(?:\s*,\s*\{([^}]*)\})?\s*\)/g;
|
|
81
|
+
let relMatch;
|
|
82
|
+
while ((relMatch = relRegex.exec(body)) !== null) {
|
|
83
|
+
const name = relMatch[1];
|
|
84
|
+
const type = relMatch[2];
|
|
85
|
+
const target = relMatch[3];
|
|
86
|
+
const opts = relMatch[4] || '';
|
|
87
|
+
const fields = [];
|
|
88
|
+
const references = [];
|
|
89
|
+
// Parse fields: [table.column]
|
|
90
|
+
const fieldsMatch = opts.match(/fields:\s*\[([^\]]+)\]/);
|
|
91
|
+
if (fieldsMatch) {
|
|
92
|
+
fields.push(...fieldsMatch[1].split(',').map((f) => f.trim().replace(/.*\./, '')));
|
|
93
|
+
}
|
|
94
|
+
const refsMatch = opts.match(/references:\s*\[([^\]]+)\]/);
|
|
95
|
+
if (refsMatch) {
|
|
96
|
+
references.push(...refsMatch[1].split(',').map((r) => r.trim().replace(/.*\./, '')));
|
|
97
|
+
}
|
|
98
|
+
const nameMatch = opts.match(/relationName:\s*['"]([^'"]+)['"]/);
|
|
99
|
+
relations.push({
|
|
100
|
+
name,
|
|
101
|
+
table: target,
|
|
102
|
+
type,
|
|
103
|
+
fields,
|
|
104
|
+
references,
|
|
105
|
+
...(nameMatch ? { relationName: nameMatch[1] } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return relations;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse handler configuration from the server handler/plugin file.
|
|
113
|
+
*/
|
|
114
|
+
function parseHandlerConfig(source) {
|
|
115
|
+
const config = {};
|
|
116
|
+
// exposedTables
|
|
117
|
+
const exposedMatch = source.match(/exposedTables:\s*\[([\s\S]*?)\]/);
|
|
118
|
+
if (exposedMatch) {
|
|
119
|
+
config.exposedTables = exposedMatch[1]
|
|
120
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
121
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
122
|
+
}
|
|
123
|
+
// hiddenColumns
|
|
124
|
+
const hiddenMatch = source.match(/hiddenColumns:\s*\{([\s\S]*?)\}/);
|
|
125
|
+
if (hiddenMatch) {
|
|
126
|
+
config.hiddenColumns = {};
|
|
127
|
+
const entries = hiddenMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
config.hiddenColumns[entry[1]] = entry[2]
|
|
130
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
131
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// protectedFields
|
|
135
|
+
const protectedMatch = source.match(/protectedFields:\s*\{([\s\S]*?)\}/);
|
|
136
|
+
if (protectedMatch) {
|
|
137
|
+
config.protectedFields = {};
|
|
138
|
+
const entries = protectedMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
config.protectedFields[entry[1]] = entry[2]
|
|
141
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
142
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// rowOwnership — scan for entries after the rowOwnership key
|
|
146
|
+
if (/rowOwnership\s*:/.test(source)) {
|
|
147
|
+
config.rowOwnership = {};
|
|
148
|
+
// Extract everything from rowOwnership: { to the matching closing }
|
|
149
|
+
const startIdx = source.indexOf('rowOwnership');
|
|
150
|
+
if (startIdx !== -1) {
|
|
151
|
+
// Find the opening brace
|
|
152
|
+
const braceStart = source.indexOf('{', startIdx);
|
|
153
|
+
if (braceStart !== -1) {
|
|
154
|
+
// Find the matching closing brace (handle one level of nesting)
|
|
155
|
+
let depth = 0;
|
|
156
|
+
let braceEnd = braceStart;
|
|
157
|
+
for (let i = braceStart; i < source.length; i++) {
|
|
158
|
+
if (source[i] === '{')
|
|
159
|
+
depth++;
|
|
160
|
+
if (source[i] === '}')
|
|
161
|
+
depth--;
|
|
162
|
+
if (depth === 0) {
|
|
163
|
+
braceEnd = i;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const block = source.slice(braceStart + 1, braceEnd);
|
|
168
|
+
const entryRegex = /(\w+):\s*\{\s*column:\s*['"]([^'"]+)['"]\s*,\s*userField:\s*['"]([^'"]+)['"]\s*\}/g;
|
|
169
|
+
const entries = block.matchAll(entryRegex);
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
config.rowOwnership[entry[1]] = { column: entry[2], userField: entry[3] };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// softDelete
|
|
177
|
+
const softDeleteMatch = source.match(/softDelete:\s*\{[\s\S]*?tables:\s*\[([\s\S]*?)\]/);
|
|
178
|
+
if (softDeleteMatch) {
|
|
179
|
+
config.softDelete = {
|
|
180
|
+
tables: softDeleteMatch[1]
|
|
181
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
182
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// pgSettings
|
|
186
|
+
config.pgSettings = /pgSettings\s*:/.test(source);
|
|
187
|
+
// publicRoutes
|
|
188
|
+
const publicRoutesMatch = source.match(/publicRoutes:\s*\[([\s\S]*?)\]/);
|
|
189
|
+
if (publicRoutesMatch) {
|
|
190
|
+
config.publicRoutes = publicRoutesMatch[1]
|
|
191
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
192
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
193
|
+
}
|
|
194
|
+
// publicRpc
|
|
195
|
+
const publicRpcMatch = source.match(/publicRpc:\s*\[([\s\S]*?)\]/);
|
|
196
|
+
if (publicRpcMatch) {
|
|
197
|
+
config.publicRpc = publicRpcMatch[1]
|
|
198
|
+
.match(/['"]([^'"]+)['"]/g)
|
|
199
|
+
?.map((s) => s.replace(/['"]/g, '')) ?? [];
|
|
200
|
+
}
|
|
201
|
+
return config;
|
|
202
|
+
}
|
|
203
|
+
export function analyzeSchema(projectPath) {
|
|
204
|
+
const issues = [];
|
|
205
|
+
const suggestions = [];
|
|
206
|
+
const allTables = [];
|
|
207
|
+
const allRelations = [];
|
|
208
|
+
// Find schema files
|
|
209
|
+
const dbDir = join(projectPath, 'db');
|
|
210
|
+
const schemaFiles = [];
|
|
211
|
+
if (existsSync(dbDir)) {
|
|
212
|
+
try {
|
|
213
|
+
const files = readdirSync(dbDir);
|
|
214
|
+
for (const f of files) {
|
|
215
|
+
if (f.endsWith('.ts') && !f.endsWith('.test.ts') && !f.startsWith('seed')) {
|
|
216
|
+
schemaFiles.push(join(dbDir, f));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// ignore read errors
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (schemaFiles.length === 0) {
|
|
225
|
+
return {
|
|
226
|
+
tables: [],
|
|
227
|
+
relations: [],
|
|
228
|
+
handlerConfig: null,
|
|
229
|
+
issues: ['No schema files found in db/ directory'],
|
|
230
|
+
suggestions: ['Create db/schema.ts with your Drizzle pgTable definitions'],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// Parse each schema file
|
|
234
|
+
for (const file of schemaFiles) {
|
|
235
|
+
try {
|
|
236
|
+
const source = readFileSync(file, 'utf-8');
|
|
237
|
+
const relativePath = file.replace(projectPath + '/', '');
|
|
238
|
+
allTables.push(...parseTables(source, relativePath));
|
|
239
|
+
allRelations.push(...parseRelations(source));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
issues.push(`Failed to read ${file}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Find and parse handler config
|
|
246
|
+
let handlerConfig = null;
|
|
247
|
+
const handlerCandidates = [
|
|
248
|
+
'server/plugins/api.ts',
|
|
249
|
+
'server/api.ts',
|
|
250
|
+
'server/handler.ts',
|
|
251
|
+
'server/index.ts',
|
|
252
|
+
];
|
|
253
|
+
for (const candidate of handlerCandidates) {
|
|
254
|
+
const fullPath = join(projectPath, candidate);
|
|
255
|
+
if (existsSync(fullPath)) {
|
|
256
|
+
try {
|
|
257
|
+
const source = readFileSync(fullPath, 'utf-8');
|
|
258
|
+
if (source.includes('createHandler')) {
|
|
259
|
+
handlerConfig = parseHandlerConfig(source);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// continue to next candidate
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Cross-reference analysis
|
|
269
|
+
if (handlerConfig?.exposedTables) {
|
|
270
|
+
const tableNames = allTables.map((t) => t.name);
|
|
271
|
+
for (const exposed of handlerConfig.exposedTables) {
|
|
272
|
+
// Check both db name and export name
|
|
273
|
+
const found = allTables.some((t) => t.name === exposed || t.exportName === exposed);
|
|
274
|
+
if (!found) {
|
|
275
|
+
issues.push(`exposedTables includes '${exposed}' but no matching pgTable definition found in schema`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Tables defined but not exposed
|
|
279
|
+
for (const table of allTables) {
|
|
280
|
+
if (!handlerConfig.exposedTables.includes(table.name) &&
|
|
281
|
+
!handlerConfig.exposedTables.includes(table.exportName)) {
|
|
282
|
+
suggestions.push(`Table '${table.name}' is defined in schema but not in exposedTables — intentional?`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Check for soft delete columns
|
|
287
|
+
if (handlerConfig?.softDelete?.tables) {
|
|
288
|
+
for (const tableName of handlerConfig.softDelete.tables) {
|
|
289
|
+
const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
|
|
290
|
+
if (table) {
|
|
291
|
+
const hasDeletedAt = table.columns.some((c) => c.name === 'deletedAt' || c.dbName === 'deleted_at');
|
|
292
|
+
if (!hasDeletedAt) {
|
|
293
|
+
issues.push(`Table '${tableName}' is in softDelete.tables but has no deletedAt column`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Check for rowOwnership references
|
|
299
|
+
if (handlerConfig?.rowOwnership) {
|
|
300
|
+
for (const [tableName, ownership] of Object.entries(handlerConfig.rowOwnership)) {
|
|
301
|
+
const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
|
|
302
|
+
if (table) {
|
|
303
|
+
const hasCol = table.columns.some((c) => c.name === ownership.column || c.dbName === ownership.column);
|
|
304
|
+
if (!hasCol) {
|
|
305
|
+
issues.push(`rowOwnership for '${tableName}' references column '${ownership.column}' which doesn't exist`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// pgSettings check
|
|
311
|
+
if (!handlerConfig?.pgSettings && allTables.length > 0) {
|
|
312
|
+
suggestions.push('No pgSettings configured — RLS policies cannot read JWT claims without it');
|
|
313
|
+
}
|
|
314
|
+
// Tables without primary keys
|
|
315
|
+
for (const table of allTables) {
|
|
316
|
+
const hasPk = table.columns.some((c) => c.modifiers.includes('primaryKey'));
|
|
317
|
+
if (!hasPk) {
|
|
318
|
+
// Could be a composite primary key (defined in table config, not column)
|
|
319
|
+
suggestions.push(`Table '${table.name}' — no single-column primaryKey() detected (may use composite key)`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Tables with timestamps
|
|
323
|
+
for (const table of allTables) {
|
|
324
|
+
const hasCreatedAt = table.columns.some((c) => c.dbName === 'created_at');
|
|
325
|
+
if (!hasCreatedAt) {
|
|
326
|
+
suggestions.push(`Table '${table.name}' has no created_at column — recommended for audit`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
tables: allTables,
|
|
331
|
+
relations: allRelations,
|
|
332
|
+
handlerConfig,
|
|
333
|
+
issues,
|
|
334
|
+
suggestions,
|
|
335
|
+
};
|
|
336
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Governance layer that governs how any agent builds everystack — grounding, cheat gates, and Model-aware tooling over MCP",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"tsx": "4.21.0",
|
|
41
41
|
"typescript": "5.9.3",
|
|
42
42
|
"zod": "3.25.67",
|
|
43
|
-
"@everystack/cli": "0.
|
|
44
|
-
"@everystack/model": "0.
|
|
43
|
+
"@everystack/cli": "0.4.2",
|
|
44
|
+
"@everystack/model": "0.4.1"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"test": "jest",
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cheat: hand-writing a migration or editing the generated schema.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Under the migration-authority workflow nobody authors migrations at all:
|
|
5
|
+
* the Models declare the state, `db:sync` moves a dev database to it,
|
|
6
|
+
* protected stages take fingerprint-verified plans (`db:plan` → `db:apply`),
|
|
7
|
+
* and CI (`db:check`) refuses generated artifacts that don't match
|
|
8
|
+
* regeneration. A hand-written `.sql` migration or a hand-edit to
|
|
9
|
+
* `db/schema.generated.ts` drifts the database from the Models. Pure path
|
|
10
|
+
* check; no IO.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* The homes that ARE legitimately authored — this gate deliberately matches
|
|
13
|
+
* none of them:
|
|
14
|
+
* - `db/models/` — the Models AND the derived-layer descriptors (defineView /
|
|
15
|
+
* defineMaterializedView / defineFunction / defineSql / trigger()) — the
|
|
16
|
+
* whole declared database, deployed via `db:reconcile`/`db:sync`.
|
|
17
|
+
* - `db/backfills/` — one-shot DATA jobs, run via `db:backfill` (content-
|
|
18
|
+
* addressed, own record, never a schema side effect).
|
|
19
|
+
* - `db/schema.ts` — the thin hand-maintained barrel that re-exports the
|
|
20
|
+
* generated schema plus package-owned schemas.
|
|
21
|
+
*
|
|
22
|
+
* `db/sql/` is RETIRED (B7, read-model-everywhere): raw-SQL derived objects
|
|
23
|
+
* moved into descriptors. A write there is flagged — the CLI verbs fail on the
|
|
24
|
+
* directory too; this gate just says so before the file lands.
|
|
12
25
|
*/
|
|
13
26
|
|
|
14
27
|
import { relative } from 'path';
|
|
@@ -16,8 +29,10 @@ import type { CheatGate, ToolCallContext } from '../types.js';
|
|
|
16
29
|
|
|
17
30
|
/** A `.sql` file under a migrations/drizzle directory — these are generated, never authored. */
|
|
18
31
|
const MIGRATION_SQL = /(?:^|\/)(?:drizzle|migrations)\/[^/]+\.sql$/i;
|
|
19
|
-
/** The generated
|
|
20
|
-
const GENERATED_SCHEMA = /
|
|
32
|
+
/** The generated drizzle schema artifact (any location — monorepos relocate it via --schema-out). */
|
|
33
|
+
const GENERATED_SCHEMA = /schema\.generated\.tsx?$/;
|
|
34
|
+
/** The retired raw-SQL derived home — descriptors in db/models are the single home now. */
|
|
35
|
+
const RETIRED_DB_SQL = /(?:^|\/)db\/sql\/[^/]+\.sql$/i;
|
|
21
36
|
|
|
22
37
|
function rel(ctx: ToolCallContext, p: string): string {
|
|
23
38
|
try {
|
|
@@ -32,15 +47,16 @@ export const handWrittenMigration: CheatGate = {
|
|
|
32
47
|
tier: 'framework',
|
|
33
48
|
severity: 'deny',
|
|
34
49
|
guide:
|
|
35
|
-
'Schema changes have two homes
|
|
36
|
-
conform: 'everystack db:
|
|
37
|
-
verify: 'everystack db:
|
|
50
|
+
'Nobody authors migrations. Schema changes have two homes: the WHOLE declared database — tables/constraints/authz (defineModel) AND functions/views/matviews (defineView/defineMaterializedView/defineFunction/defineSql descriptors) — lives in db/models/ (edit the declaration, then `everystack db:sync` on dev — protected stages take `db:plan` → `db:apply`); one-shot DATA moves are authored in db/backfills/*.sql (run via db:backfill, never as a schema side effect). db/sql/ is retired — raw-SQL derived objects moved into descriptors. No migration file, ever — and generated artifacts (schema.generated.ts) are never edited.',
|
|
51
|
+
conform: 'edit db/models/ (Models + derived descriptors) then `everystack db:sync` (dev) or `db:plan` → `db:apply` (protected) · db/backfills/ for data moves',
|
|
52
|
+
verify: 'everystack db:check passes (declared state composes; generated artifacts match regeneration) AND everystack db:fingerprint reports MATCH',
|
|
38
53
|
detect(ctx: ToolCallContext): string | null {
|
|
39
54
|
if (ctx.tool !== 'Write' && ctx.tool !== 'Edit') return null;
|
|
40
55
|
const p = ctx.filePath;
|
|
41
56
|
if (!p) return null;
|
|
42
57
|
if (MIGRATION_SQL.test(p)) return `${rel(ctx, p)} is a SQL migration being written by hand`;
|
|
43
|
-
if (GENERATED_SCHEMA.test(p)) return `${rel(ctx, p)} is generated from your Models`;
|
|
58
|
+
if (GENERATED_SCHEMA.test(p)) return `${rel(ctx, p)} is a generated artifact (compiled from your Models — db:check refuses hand edits)`;
|
|
59
|
+
if (RETIRED_DB_SQL.test(p)) return `${rel(ctx, p)} is in the retired db/sql home — declare it as a descriptor (defineView/defineMaterializedView/defineFunction/defineSql) in db/models/ instead; the CLI verbs fail on this directory`;
|
|
44
60
|
return null;
|
|
45
61
|
},
|
|
46
62
|
};
|
package/src/index.ts
CHANGED
|
@@ -71,7 +71,7 @@ const INSTRUCTIONS = [
|
|
|
71
71
|
'1. Read everystack://core for architecture and conventions.',
|
|
72
72
|
'2. Read everystack://security before any deployment or auth guidance.',
|
|
73
73
|
'3. Load detail resources on demand when the user asks about specific features.',
|
|
74
|
-
'4. Data lives in PostgreSQL via Models/Modules and is served through the API — never bundle large computed data into the app. Schema
|
|
74
|
+
'4. Data lives in PostgreSQL via Models/Modules and is served through the API — never bundle large computed data into the app. NOBODY AUTHORS MIGRATIONS. Schema work has two homes: the WHOLE declared database — tables/constraints/authz (defineModel) AND functions/views/matviews (defineView/defineMaterializedView/defineFunction/defineSql descriptors) — is DECLARED in `db/models/` (edit the declaration, then `everystack db:sync` moves the dev database to the checkout — verified by fingerprint; the derived layer deploys via `db:reconcile`/`db:sync`); one-shot DATA moves are authored in `db/backfills/*.sql` (run via `db:backfill`, never as a schema side effect). Protected stages never take a sync: `db:plan` mints a reviewable, fingerprint-pinned edge and `db:apply` verifies at both ends — the checkout must descend from the commit declaring the target\'s state ("rebase first"), and destructive plans are confirmed, snapshotted, and approver-gated (`db:approvers`). CI runs `everystack db:check` (the merged declared state must compose; generated artifacts must match regeneration byte-for-byte). Per-branch dev databases: `db:template:refresh` + `db:branch`; deployed feature stages fork data with `db:fork`. Reuse `@everystack/ui` components; never put secret values behind `EXPO_PUBLIC_*`.',
|
|
75
75
|
'5. When the user wants to start a new project, run check_environment (phase "local" for dev, "deploy" for deployment) to verify prerequisites.',
|
|
76
76
|
'6. When the user needs to interact with deployed infrastructure, guide them to use the everystack CLI.',
|
|
77
77
|
].join('\n');
|
|
@@ -105,7 +105,7 @@ const FEATURES: Record<string, { resource: string; packages: string[]; steps: st
|
|
|
105
105
|
packages: [],
|
|
106
106
|
steps: [
|
|
107
107
|
'Events use PostgreSQL LISTEN/NOTIFY — built into the database',
|
|
108
|
-
'
|
|
108
|
+
'Declare the NOTIFY trigger the descriptor way (everystack://derived-objects): a defineFunction returning trigger, wired via trigger() on the Model — never a hand-written SQL trigger or a db/sql file',
|
|
109
109
|
'Set up a listener Lambda with persistent database connection',
|
|
110
110
|
'Add WebSocket fan-out for client delivery',
|
|
111
111
|
'Use the useSignal hook in React components for real-time updates',
|
|
@@ -139,8 +139,8 @@ export function registerAddFeaturePrompt(server: McpServer): void {
|
|
|
139
139
|
`1. Read the ${f.resource} resource for full documentation.`,
|
|
140
140
|
'2. Read everystack://core for architecture context.',
|
|
141
141
|
feature === 'auth' || feature === 'security' ? '3. Read everystack://security for the security model.' : '',
|
|
142
|
-
projectPath ? `4.
|
|
143
|
-
projectPath ?
|
|
142
|
+
projectPath ? `4. Read the project's CLAUDE.md and structure under "${projectPath}" to understand the current state.` : '',
|
|
143
|
+
projectPath ? '5. After changes: `pnpm test`, and if the schema changed, `everystack db:check`.' : '',
|
|
144
144
|
'',
|
|
145
145
|
'## Steps',
|
|
146
146
|
'',
|
|
@@ -151,7 +151,7 @@ export function registerAddFeaturePrompt(server: McpServer): void {
|
|
|
151
151
|
'## After Setup',
|
|
152
152
|
'',
|
|
153
153
|
'- Run tests to verify the integration',
|
|
154
|
-
'-
|
|
154
|
+
'- If the schema changed, run `everystack db:check`',
|
|
155
155
|
'- Read the relevant resource docs for advanced configuration',
|
|
156
156
|
].filter(Boolean).join('\n'),
|
|
157
157
|
},
|
package/src/prompts/claude-md.ts
CHANGED
|
@@ -61,8 +61,10 @@ export function registerClaudeMdPrompt(server: McpServer): void {
|
|
|
61
61
|
'',
|
|
62
62
|
'Reconcile the existing CLAUDE.md against three things, and propose a concrete diff:',
|
|
63
63
|
'1. **Framework drift** — does it teach superseded ways (e.g. `drizzle-kit generate`,',
|
|
64
|
-
' hand-written migrations, "pass your Drizzle schema") instead of
|
|
65
|
-
' (`defineModel` → `everystack db:
|
|
64
|
+
' hand-written migrations, a migration journal, "pass your Drizzle schema") instead of',
|
|
65
|
+
' the migration-authority flow (`defineModel` in db/models/ → `everystack db:sync` on',
|
|
66
|
+
' dev; `db:plan` → `db:apply` on protected stages; `db:check` in CI; data moves in',
|
|
67
|
+
' db/backfills/ via `db:backfill`)? Suggest the current conventions.',
|
|
66
68
|
'2. **Project drift** — do the Structure / Commands / package list match what is actually',
|
|
67
69
|
' installed now (new packages, a new tier, a new models/ dir)? Suggest updates. Also',
|
|
68
70
|
' check the operations manual: if docs/RUNBOOK.md is missing, or stale per',
|
package/src/prompts/debug.ts
CHANGED
|
@@ -22,9 +22,8 @@ export function registerDebugPrompt(server: McpServer): void {
|
|
|
22
22
|
'## Instructions',
|
|
23
23
|
'',
|
|
24
24
|
'1. Read everystack://core for architecture context.',
|
|
25
|
-
projectPath ? `2.
|
|
26
|
-
projectPath ?
|
|
27
|
-
projectPath ? `4. Run schema_analyze with projectPath="${projectPath}" to check schema/handler alignment.` : '',
|
|
25
|
+
projectPath ? `2. Read the project's CLAUDE.md and \`db/models/\` under "${projectPath}" to understand its schema and conventions.` : '',
|
|
26
|
+
projectPath ? '3. If the issue is schema/database-related, run `everystack db:check` (declared state composes) and `everystack db:fingerprint` (live schema matches the Models) to localize drift.' : '',
|
|
28
27
|
'',
|
|
29
28
|
'## Diagnostic Framework',
|
|
30
29
|
'',
|
|
@@ -125,7 +124,7 @@ export function registerDebugPrompt(server: McpServer): void {
|
|
|
125
124
|
'1. Identify the root cause',
|
|
126
125
|
'2. Propose a fix with the specific code change',
|
|
127
126
|
'3. Verify the fix resolves the issue',
|
|
128
|
-
'4. Run
|
|
127
|
+
'4. Run `pnpm test` and (for schema changes) `everystack db:check` to ensure no new issues were introduced',
|
|
129
128
|
].filter(Boolean).join('\n'),
|
|
130
129
|
},
|
|
131
130
|
},
|
package/src/prompts/deploy.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
export function registerDeployPrompt(server: McpServer): void {
|
|
5
5
|
server.prompt(
|
|
6
6
|
'deploy',
|
|
7
|
-
'Step-by-step deployment walkthrough for a specific stage. Covers AWS credentials, SST deploy,
|
|
7
|
+
'Step-by-step deployment walkthrough for a specific stage. Covers AWS credentials, SST deploy, the SAFE schema flow (db:plan → db:apply, snapshot-first), and verification.',
|
|
8
8
|
{
|
|
9
9
|
stage: z.enum(['dev', 'staging', 'production']).describe('Deployment stage'),
|
|
10
10
|
projectPath: z.string().optional().describe('Absolute path to project root'),
|
|
@@ -25,8 +25,8 @@ export function registerDeployPrompt(server: McpServer): void {
|
|
|
25
25
|
'',
|
|
26
26
|
'1. Read everystack://deployment for infrastructure setup.',
|
|
27
27
|
'2. Read everystack://security for AWS credential setup and deployment checklist.',
|
|
28
|
-
|
|
29
|
-
projectPath ? `4. Run
|
|
28
|
+
'3. Read everystack://database-operations for the SAFE schema-migration flow (db:plan → db:apply, snapshot-first) — a protected stage is NOT migrated with db:migrate.',
|
|
29
|
+
projectPath ? `4. Run check_environment with projectPath="${projectPath}" to confirm prerequisites (Node, SST, AWS credentials) before deploying.` : '',
|
|
30
30
|
'',
|
|
31
31
|
'## Pre-Deploy Checklist',
|
|
32
32
|
'',
|
|
@@ -49,8 +49,8 @@ export function registerDeployPrompt(server: McpServer): void {
|
|
|
49
49
|
'### Production Safety',
|
|
50
50
|
'',
|
|
51
51
|
'- [ ] All tests pass: `pnpm test`',
|
|
52
|
-
'- [ ]
|
|
53
|
-
'- [ ] RLS
|
|
52
|
+
'- [ ] `everystack db:check` passes (declared state composes; generated artifacts match)',
|
|
53
|
+
'- [ ] RLS is declared via `can()` abilities on every Model (compiles to policies + grants)',
|
|
54
54
|
'- [ ] pgSettings is configured in handler',
|
|
55
55
|
'- [ ] No hardcoded secrets in source',
|
|
56
56
|
'- [ ] .env files are not committed',
|
|
@@ -71,10 +71,19 @@ export function registerDeployPrompt(server: McpServer): void {
|
|
|
71
71
|
'- SQS queues (V3)',
|
|
72
72
|
'- IAM roles and policies',
|
|
73
73
|
'',
|
|
74
|
-
'###
|
|
74
|
+
'### 2. Database Schema (V2+) — the SAFE flow',
|
|
75
|
+
'',
|
|
76
|
+
'A protected stage is migrated by minting a reviewable plan and applying it credential-free',
|
|
77
|
+
'(the operator never holds the database URL). See everystack://database-operations.',
|
|
75
78
|
'```bash',
|
|
76
|
-
`everystack db:
|
|
77
|
-
|
|
79
|
+
`everystack db:check # gate: declared state composes`,
|
|
80
|
+
`everystack db:plan --stage ${stage} --out ${stage}.plan.json # mint the edge (read-only) — REVIEW it`,
|
|
81
|
+
isProduction ? `everystack db:snapshot --stage ${stage} # physical RDS snapshot first (instant rollback)` : '',
|
|
82
|
+
`everystack db:apply --plan ${stage}.plan.json --stage ${stage} # verify → apply → verify`,
|
|
83
|
+
`# destructive plans (drops / narrowing types) also need: --confirm + a snapshot + the`,
|
|
84
|
+
`# stage's approver set (everystack db:approvers --stage ${stage} --set "…")`,
|
|
85
|
+
`everystack db:reconcile --check --stage ${stage} # confirm the compute layer (views/matviews/functions) matches`,
|
|
86
|
+
stage === 'dev' ? `everystack db:seed --stage ${stage} # dev only` : '',
|
|
78
87
|
'```',
|
|
79
88
|
'',
|
|
80
89
|
'### 4. Verify',
|