@everystack/mcp 0.2.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.
Files changed (40) hide show
  1. package/README.md +100 -0
  2. package/package.json +39 -0
  3. package/src/index.ts +58 -0
  4. package/src/prompts/add-feature.ts +163 -0
  5. package/src/prompts/debug.ts +136 -0
  6. package/src/prompts/deploy.ts +131 -0
  7. package/src/prompts/design-schema.ts +104 -0
  8. package/src/prompts/index.ts +16 -0
  9. package/src/prompts/new-app.ts +211 -0
  10. package/src/prompts/secure.ts +231 -0
  11. package/src/resources/adding-database.md +169 -0
  12. package/src/resources/admin.md +81 -0
  13. package/src/resources/auth.md +115 -0
  14. package/src/resources/aws-setup.md +173 -0
  15. package/src/resources/cli.md +108 -0
  16. package/src/resources/client-api.md +145 -0
  17. package/src/resources/core.md +196 -0
  18. package/src/resources/deployment.md +146 -0
  19. package/src/resources/events.md +87 -0
  20. package/src/resources/first-run.md +100 -0
  21. package/src/resources/getting-started.md +75 -0
  22. package/src/resources/handler-options.md +114 -0
  23. package/src/resources/images.md +73 -0
  24. package/src/resources/index.ts +224 -0
  25. package/src/resources/jobs.md +97 -0
  26. package/src/resources/logging.md +91 -0
  27. package/src/resources/plugins.md +68 -0
  28. package/src/resources/project-claude-md.md +127 -0
  29. package/src/resources/query-protocol.md +129 -0
  30. package/src/resources/schema-patterns.md +167 -0
  31. package/src/resources/security-device.md +99 -0
  32. package/src/resources/security.md +270 -0
  33. package/src/resources/ssr.md +82 -0
  34. package/src/resources/storage.md +63 -0
  35. package/src/resources/testing.md +118 -0
  36. package/src/tools/check-environment.ts +319 -0
  37. package/src/tools/index.ts +58 -0
  38. package/src/tools/project-status.ts +183 -0
  39. package/src/tools/project-validate.ts +369 -0
  40. package/src/tools/schema-analyze.ts +410 -0
@@ -0,0 +1,410 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ interface ColumnInfo {
5
+ name: string;
6
+ dbName: string;
7
+ type: string;
8
+ modifiers: string[];
9
+ }
10
+
11
+ interface TableInfo {
12
+ name: string;
13
+ exportName: string;
14
+ columns: ColumnInfo[];
15
+ file: string;
16
+ }
17
+
18
+ interface RelationInfo {
19
+ name: string;
20
+ table: string;
21
+ type: 'one' | 'many';
22
+ fields: string[];
23
+ references: string[];
24
+ relationName?: string;
25
+ }
26
+
27
+ interface HandlerConfig {
28
+ exposedTables: string[];
29
+ hiddenColumns: Record<string, string[]>;
30
+ protectedFields: Record<string, string[]>;
31
+ rowOwnership: Record<string, { column: string; userField: string }>;
32
+ softDelete: { tables: string[] };
33
+ pgSettings: boolean;
34
+ relations: Record<string, Record<string, unknown>>;
35
+ publicRoutes: string[];
36
+ publicRpc: string[];
37
+ }
38
+
39
+ interface SchemaAnalysis {
40
+ tables: TableInfo[];
41
+ relations: RelationInfo[];
42
+ handlerConfig: Partial<HandlerConfig> | null;
43
+ issues: string[];
44
+ suggestions: string[];
45
+ }
46
+
47
+ /**
48
+ * Extract a brace-balanced block starting from an opening brace.
49
+ */
50
+ function extractBraceBlock(source: string, openIndex: number): string {
51
+ let depth = 0;
52
+ for (let i = openIndex; i < source.length; i++) {
53
+ if (source[i] === '{') depth++;
54
+ if (source[i] === '}') depth--;
55
+ if (depth === 0) return source.slice(openIndex + 1, i);
56
+ }
57
+ return source.slice(openIndex + 1);
58
+ }
59
+
60
+ /**
61
+ * Parse pgTable declarations from a TypeScript source file.
62
+ * Uses regex + brace-depth extraction to handle nested objects (e.g., { withTimezone: true }).
63
+ */
64
+ function parseTables(source: string, file: string): TableInfo[] {
65
+ const tables: TableInfo[] = [];
66
+
67
+ // Find each pgTable declaration
68
+ const headerRegex = /export\s+const\s+(\w+)\s*=\s*pgTable\(\s*['"]([^'"]+)['"]\s*,\s*\{/g;
69
+ let match: RegExpExecArray | null;
70
+
71
+ while ((match = headerRegex.exec(source)) !== null) {
72
+ const exportName = match[1];
73
+ const dbName = match[2];
74
+ // The opening brace position is at the end of the match minus 1
75
+ const openBrace = match.index + match[0].length - 1;
76
+ const columnsBlock = extractBraceBlock(source, openBrace);
77
+
78
+ const columns = parseColumns(columnsBlock);
79
+ tables.push({ name: dbName, exportName, columns, file });
80
+ }
81
+
82
+ return tables;
83
+ }
84
+
85
+ function parseColumns(block: string): ColumnInfo[] {
86
+ const columns: ColumnInfo[] = [];
87
+ // Match: columnName: type('db_name', ...optional args...)...chain...
88
+ // The type call may have extra args after the string (e.g., timestamp('x', { withTimezone: true }))
89
+ const colRegex = /(\w+)\s*:\s*(\w+)\(\s*['"]([^'"]+)['"][^)]*\)([\s\S]*?)(?=\n\s*\w+\s*:|$)/g;
90
+ let match: RegExpExecArray | null;
91
+
92
+ while ((match = colRegex.exec(block)) !== null) {
93
+ const name = match[1];
94
+ const type = match[2];
95
+ const dbName = match[3];
96
+ const rest = match[4];
97
+
98
+ const modifiers: string[] = [];
99
+ if (rest.includes('.primaryKey()')) modifiers.push('primaryKey');
100
+ if (rest.includes('.notNull()')) modifiers.push('notNull');
101
+ if (rest.includes('.unique()')) modifiers.push('unique');
102
+ if (rest.includes('.defaultNow()')) modifiers.push('defaultNow');
103
+ if (rest.includes('.defaultRandom()')) modifiers.push('defaultRandom');
104
+ if (rest.includes('.default(')) modifiers.push('default');
105
+ if (rest.includes('.references(')) modifiers.push('references');
106
+
107
+ columns.push({ name, dbName, type, modifiers });
108
+ }
109
+
110
+ return columns;
111
+ }
112
+
113
+ /**
114
+ * Parse relations() declarations from source.
115
+ */
116
+ function parseRelations(source: string): RelationInfo[] {
117
+ const relations: RelationInfo[] = [];
118
+
119
+ // Match: export const nameRelations = relations(tableName, ({ one, many }) => ({
120
+ const relBlockRegex = /export\s+const\s+\w+\s*=\s*relations\(\s*(\w+)\s*,\s*\(\s*\{[^}]*\}\s*\)\s*=>\s*\(\{([\s\S]*?)\}\)\s*\)/g;
121
+ let blockMatch: RegExpExecArray | null;
122
+
123
+ while ((blockMatch = relBlockRegex.exec(source)) !== null) {
124
+ const table = blockMatch[1];
125
+ const body = blockMatch[2];
126
+
127
+ // Match individual relations: name: one(target, { fields: [...], references: [...] })
128
+ const relRegex = /(\w+)\s*:\s*(one|many)\(\s*(\w+)(?:\s*,\s*\{([^}]*)\})?\s*\)/g;
129
+ let relMatch: RegExpExecArray | null;
130
+
131
+ while ((relMatch = relRegex.exec(body)) !== null) {
132
+ const name = relMatch[1];
133
+ const type = relMatch[2] as 'one' | 'many';
134
+ const target = relMatch[3];
135
+ const opts = relMatch[4] || '';
136
+
137
+ const fields: string[] = [];
138
+ const references: string[] = [];
139
+
140
+ // Parse fields: [table.column]
141
+ const fieldsMatch = opts.match(/fields:\s*\[([^\]]+)\]/);
142
+ if (fieldsMatch) {
143
+ fields.push(...fieldsMatch[1].split(',').map((f) => f.trim().replace(/.*\./, '')));
144
+ }
145
+
146
+ const refsMatch = opts.match(/references:\s*\[([^\]]+)\]/);
147
+ if (refsMatch) {
148
+ references.push(...refsMatch[1].split(',').map((r) => r.trim().replace(/.*\./, '')));
149
+ }
150
+
151
+ const nameMatch = opts.match(/relationName:\s*['"]([^'"]+)['"]/);
152
+
153
+ relations.push({
154
+ name,
155
+ table: target,
156
+ type,
157
+ fields,
158
+ references,
159
+ ...(nameMatch ? { relationName: nameMatch[1] } : {}),
160
+ });
161
+ }
162
+ }
163
+
164
+ return relations;
165
+ }
166
+
167
+ /**
168
+ * Parse handler configuration from the server handler/plugin file.
169
+ */
170
+ function parseHandlerConfig(source: string): Partial<HandlerConfig> {
171
+ const config: Partial<HandlerConfig> = {};
172
+
173
+ // exposedTables
174
+ const exposedMatch = source.match(/exposedTables:\s*\[([\s\S]*?)\]/);
175
+ if (exposedMatch) {
176
+ config.exposedTables = exposedMatch[1]
177
+ .match(/['"]([^'"]+)['"]/g)
178
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
179
+ }
180
+
181
+ // hiddenColumns
182
+ const hiddenMatch = source.match(/hiddenColumns:\s*\{([\s\S]*?)\}/);
183
+ if (hiddenMatch) {
184
+ config.hiddenColumns = {};
185
+ const entries = hiddenMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
186
+ for (const entry of entries) {
187
+ config.hiddenColumns[entry[1]] = entry[2]
188
+ .match(/['"]([^'"]+)['"]/g)
189
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
190
+ }
191
+ }
192
+
193
+ // protectedFields
194
+ const protectedMatch = source.match(/protectedFields:\s*\{([\s\S]*?)\}/);
195
+ if (protectedMatch) {
196
+ config.protectedFields = {};
197
+ const entries = protectedMatch[1].matchAll(/(\w+):\s*\[([\s\S]*?)\]/g);
198
+ for (const entry of entries) {
199
+ config.protectedFields[entry[1]] = entry[2]
200
+ .match(/['"]([^'"]+)['"]/g)
201
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
202
+ }
203
+ }
204
+
205
+ // rowOwnership — scan for entries after the rowOwnership key
206
+ if (/rowOwnership\s*:/.test(source)) {
207
+ config.rowOwnership = {};
208
+ // Extract everything from rowOwnership: { to the matching closing }
209
+ const startIdx = source.indexOf('rowOwnership');
210
+ if (startIdx !== -1) {
211
+ // Find the opening brace
212
+ const braceStart = source.indexOf('{', startIdx);
213
+ if (braceStart !== -1) {
214
+ // Find the matching closing brace (handle one level of nesting)
215
+ let depth = 0;
216
+ let braceEnd = braceStart;
217
+ for (let i = braceStart; i < source.length; i++) {
218
+ if (source[i] === '{') depth++;
219
+ if (source[i] === '}') depth--;
220
+ if (depth === 0) { braceEnd = i; break; }
221
+ }
222
+ const block = source.slice(braceStart + 1, braceEnd);
223
+ const entryRegex = /(\w+):\s*\{\s*column:\s*['"]([^'"]+)['"]\s*,\s*userField:\s*['"]([^'"]+)['"]\s*\}/g;
224
+ const entries = block.matchAll(entryRegex);
225
+ for (const entry of entries) {
226
+ config.rowOwnership[entry[1]] = { column: entry[2], userField: entry[3] };
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ // softDelete
233
+ const softDeleteMatch = source.match(/softDelete:\s*\{[\s\S]*?tables:\s*\[([\s\S]*?)\]/);
234
+ if (softDeleteMatch) {
235
+ config.softDelete = {
236
+ tables: softDeleteMatch[1]
237
+ .match(/['"]([^'"]+)['"]/g)
238
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [],
239
+ };
240
+ }
241
+
242
+ // pgSettings
243
+ config.pgSettings = /pgSettings\s*:/.test(source);
244
+
245
+ // publicRoutes
246
+ const publicRoutesMatch = source.match(/publicRoutes:\s*\[([\s\S]*?)\]/);
247
+ if (publicRoutesMatch) {
248
+ config.publicRoutes = publicRoutesMatch[1]
249
+ .match(/['"]([^'"]+)['"]/g)
250
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
251
+ }
252
+
253
+ // publicRpc
254
+ const publicRpcMatch = source.match(/publicRpc:\s*\[([\s\S]*?)\]/);
255
+ if (publicRpcMatch) {
256
+ config.publicRpc = publicRpcMatch[1]
257
+ .match(/['"]([^'"]+)['"]/g)
258
+ ?.map((s) => s.replace(/['"]/g, '')) ?? [];
259
+ }
260
+
261
+ return config;
262
+ }
263
+
264
+ export function analyzeSchema(projectPath: string): SchemaAnalysis {
265
+ const issues: string[] = [];
266
+ const suggestions: string[] = [];
267
+ const allTables: TableInfo[] = [];
268
+ const allRelations: RelationInfo[] = [];
269
+
270
+ // Find schema files
271
+ const dbDir = join(projectPath, 'db');
272
+ const schemaFiles: string[] = [];
273
+
274
+ if (existsSync(dbDir)) {
275
+ try {
276
+ const files = readdirSync(dbDir);
277
+ for (const f of files) {
278
+ if (f.endsWith('.ts') && !f.endsWith('.test.ts') && !f.startsWith('seed')) {
279
+ schemaFiles.push(join(dbDir, f));
280
+ }
281
+ }
282
+ } catch {
283
+ // ignore read errors
284
+ }
285
+ }
286
+
287
+ if (schemaFiles.length === 0) {
288
+ return {
289
+ tables: [],
290
+ relations: [],
291
+ handlerConfig: null,
292
+ issues: ['No schema files found in db/ directory'],
293
+ suggestions: ['Create db/schema.ts with your Drizzle pgTable definitions'],
294
+ };
295
+ }
296
+
297
+ // Parse each schema file
298
+ for (const file of schemaFiles) {
299
+ try {
300
+ const source = readFileSync(file, 'utf-8');
301
+ const relativePath = file.replace(projectPath + '/', '');
302
+ allTables.push(...parseTables(source, relativePath));
303
+ allRelations.push(...parseRelations(source));
304
+ } catch {
305
+ issues.push(`Failed to read ${file}`);
306
+ }
307
+ }
308
+
309
+ // Find and parse handler config
310
+ let handlerConfig: Partial<HandlerConfig> | null = null;
311
+ const handlerCandidates = [
312
+ 'server/plugins/api.ts',
313
+ 'server/api.ts',
314
+ 'server/handler.ts',
315
+ 'server/index.ts',
316
+ ];
317
+
318
+ for (const candidate of handlerCandidates) {
319
+ const fullPath = join(projectPath, candidate);
320
+ if (existsSync(fullPath)) {
321
+ try {
322
+ const source = readFileSync(fullPath, 'utf-8');
323
+ if (source.includes('createHandler')) {
324
+ handlerConfig = parseHandlerConfig(source);
325
+ break;
326
+ }
327
+ } catch {
328
+ // continue to next candidate
329
+ }
330
+ }
331
+ }
332
+
333
+ // Cross-reference analysis
334
+ if (handlerConfig?.exposedTables) {
335
+ const tableNames = allTables.map((t) => t.name);
336
+ for (const exposed of handlerConfig.exposedTables) {
337
+ // Check both db name and export name
338
+ const found = allTables.some((t) => t.name === exposed || t.exportName === exposed);
339
+ if (!found) {
340
+ issues.push(`exposedTables includes '${exposed}' but no matching pgTable definition found in schema`);
341
+ }
342
+ }
343
+
344
+ // Tables defined but not exposed
345
+ for (const table of allTables) {
346
+ if (!handlerConfig.exposedTables.includes(table.name) &&
347
+ !handlerConfig.exposedTables.includes(table.exportName)) {
348
+ suggestions.push(`Table '${table.name}' is defined in schema but not in exposedTables — intentional?`);
349
+ }
350
+ }
351
+ }
352
+
353
+ // Check for soft delete columns
354
+ if (handlerConfig?.softDelete?.tables) {
355
+ for (const tableName of handlerConfig.softDelete.tables) {
356
+ const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
357
+ if (table) {
358
+ const hasDeletedAt = table.columns.some((c) => c.name === 'deletedAt' || c.dbName === 'deleted_at');
359
+ if (!hasDeletedAt) {
360
+ issues.push(`Table '${tableName}' is in softDelete.tables but has no deletedAt column`);
361
+ }
362
+ }
363
+ }
364
+ }
365
+
366
+ // Check for rowOwnership references
367
+ if (handlerConfig?.rowOwnership) {
368
+ for (const [tableName, ownership] of Object.entries(handlerConfig.rowOwnership)) {
369
+ const table = allTables.find((t) => t.name === tableName || t.exportName === tableName);
370
+ if (table) {
371
+ const hasCol = table.columns.some(
372
+ (c) => c.name === ownership.column || c.dbName === ownership.column,
373
+ );
374
+ if (!hasCol) {
375
+ issues.push(`rowOwnership for '${tableName}' references column '${ownership.column}' which doesn't exist`);
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ // pgSettings check
382
+ if (!handlerConfig?.pgSettings && allTables.length > 0) {
383
+ suggestions.push('No pgSettings configured — RLS policies cannot read JWT claims without it');
384
+ }
385
+
386
+ // Tables without primary keys
387
+ for (const table of allTables) {
388
+ const hasPk = table.columns.some((c) => c.modifiers.includes('primaryKey'));
389
+ if (!hasPk) {
390
+ // Could be a composite primary key (defined in table config, not column)
391
+ suggestions.push(`Table '${table.name}' — no single-column primaryKey() detected (may use composite key)`);
392
+ }
393
+ }
394
+
395
+ // Tables with timestamps
396
+ for (const table of allTables) {
397
+ const hasCreatedAt = table.columns.some((c) => c.dbName === 'created_at');
398
+ if (!hasCreatedAt) {
399
+ suggestions.push(`Table '${table.name}' has no created_at column — recommended for audit`);
400
+ }
401
+ }
402
+
403
+ return {
404
+ tables: allTables,
405
+ relations: allRelations,
406
+ handlerConfig,
407
+ issues,
408
+ suggestions,
409
+ };
410
+ }