@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,369 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ type Severity = 'error' | 'warning' | 'info';
5
+
6
+ interface ValidationResult {
7
+ severity: Severity;
8
+ category: string;
9
+ message: string;
10
+ file?: string;
11
+ fix?: string;
12
+ }
13
+
14
+ interface ValidationReport {
15
+ results: ValidationResult[];
16
+ summary: {
17
+ errors: number;
18
+ warnings: number;
19
+ info: number;
20
+ };
21
+ }
22
+
23
+ function readFileSafe(path: string): string | null {
24
+ if (!existsSync(path)) return null;
25
+ try {
26
+ return readFileSync(path, 'utf-8');
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function readJsonSafe(path: string): unknown | null {
33
+ const content = readFileSafe(path);
34
+ if (!content) return null;
35
+ try {
36
+ return JSON.parse(content);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function checkPackageJson(projectPath: string, results: ValidationResult[]): void {
43
+ const pkg = readJsonSafe(join(projectPath, 'package.json')) as Record<string, unknown> | null;
44
+ if (!pkg) {
45
+ results.push({
46
+ severity: 'error',
47
+ category: 'project',
48
+ message: 'No package.json found',
49
+ fix: 'Run `npm init` or create package.json manually',
50
+ });
51
+ return;
52
+ }
53
+
54
+ const deps = {
55
+ ...(pkg.dependencies as Record<string, string> || {}),
56
+ ...(pkg.devDependencies as Record<string, string> || {}),
57
+ };
58
+
59
+ // Check for required peer dependencies
60
+ if (deps['@everystack/api'] && !deps['drizzle-orm']) {
61
+ results.push({
62
+ severity: 'error',
63
+ category: 'dependencies',
64
+ message: '@everystack/api requires drizzle-orm as a peer dependency',
65
+ fix: 'Run `pnpm add drizzle-orm drizzle-kit`',
66
+ });
67
+ }
68
+
69
+ if (deps['@everystack/server'] && !deps['sst']) {
70
+ results.push({
71
+ severity: 'warning',
72
+ category: 'dependencies',
73
+ message: '@everystack/server expects sst as a peer dependency for deployment',
74
+ fix: 'Run `pnpm add -D sst`',
75
+ });
76
+ }
77
+
78
+ if (deps['@everystack/query'] && !deps['@tanstack/react-query']) {
79
+ results.push({
80
+ severity: 'error',
81
+ category: 'dependencies',
82
+ message: '@everystack/query requires @tanstack/react-query as a peer dependency',
83
+ fix: 'Run `pnpm add @tanstack/react-query`',
84
+ });
85
+ }
86
+ }
87
+
88
+ function checkSecurityConfig(projectPath: string, results: ValidationResult[]): void {
89
+ // Check for exposed secrets in common locations
90
+ const dangerousFiles = ['.env', '.env.local', '.env.production'];
91
+ for (const f of dangerousFiles) {
92
+ const content = readFileSafe(join(projectPath, f));
93
+ if (content) {
94
+ if (content.includes('JWT_SECRET') || content.includes('DATABASE_URL')) {
95
+ results.push({
96
+ severity: 'warning',
97
+ category: 'security',
98
+ message: `${f} contains sensitive values — use SST secrets instead`,
99
+ file: f,
100
+ fix: 'Run `pnpm sst secret set JwtSecret "value" --stage dev` and remove from .env',
101
+ });
102
+ }
103
+
104
+ // Check gitignore
105
+ const gitignore = readFileSafe(join(projectPath, '.gitignore'));
106
+ if (gitignore && !gitignore.includes(f)) {
107
+ results.push({
108
+ severity: 'error',
109
+ category: 'security',
110
+ message: `${f} is not in .gitignore — secrets may be committed`,
111
+ file: '.gitignore',
112
+ fix: `Add '${f}' to .gitignore`,
113
+ });
114
+ }
115
+ }
116
+ }
117
+
118
+ // Check for hardcoded secrets in handler files
119
+ const serverFiles = ['server/api.ts', 'server/handler.ts', 'server/plugins/api.ts'];
120
+ for (const f of serverFiles) {
121
+ const content = readFileSafe(join(projectPath, f));
122
+ if (content) {
123
+ if (/['"](?:sk_|pk_|secret_)[a-zA-Z0-9]+['"]/.test(content)) {
124
+ results.push({
125
+ severity: 'error',
126
+ category: 'security',
127
+ message: `Possible hardcoded API key in ${f}`,
128
+ file: f,
129
+ fix: 'Move secrets to SST secret management',
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ function checkHandlerConfig(projectPath: string, results: ValidationResult[]): void {
137
+ const handlerCandidates = [
138
+ 'server/plugins/api.ts',
139
+ 'server/api.ts',
140
+ 'server/handler.ts',
141
+ ];
142
+
143
+ let handlerSource: string | null = null;
144
+ let handlerFile: string | null = null;
145
+
146
+ for (const f of handlerCandidates) {
147
+ const content = readFileSafe(join(projectPath, f));
148
+ if (content && content.includes('createHandler')) {
149
+ handlerSource = content;
150
+ handlerFile = f;
151
+ break;
152
+ }
153
+ }
154
+
155
+ if (!handlerSource || !handlerFile) return;
156
+
157
+ // pgSettings is required for RLS
158
+ if (!handlerSource.includes('pgSettings')) {
159
+ results.push({
160
+ severity: 'warning',
161
+ category: 'security',
162
+ message: 'No pgSettings configured — RLS policies will not receive JWT claims',
163
+ file: handlerFile,
164
+ fix: 'Add pgSettings to your createHandler() config. Read everystack://security for the pattern.',
165
+ });
166
+ }
167
+
168
+ // Check for auth configuration
169
+ if (!handlerSource.includes('verifyToken')) {
170
+ results.push({
171
+ severity: 'warning',
172
+ category: 'security',
173
+ message: 'No auth.verifyToken configured — all routes are unauthenticated',
174
+ file: handlerFile,
175
+ fix: 'Add auth.verifyToken to createHandler() config',
176
+ });
177
+ }
178
+
179
+ // exposedTables should be set
180
+ if (!handlerSource.includes('exposedTables')) {
181
+ results.push({
182
+ severity: 'warning',
183
+ category: 'security',
184
+ message: 'No exposedTables set — all schema tables are accessible via API',
185
+ file: handlerFile,
186
+ fix: 'Add exposedTables to restrict which tables are accessible',
187
+ });
188
+ }
189
+
190
+ // maxEmbedDepth should be set
191
+ if (!handlerSource.includes('maxEmbedDepth')) {
192
+ results.push({
193
+ severity: 'info',
194
+ category: 'security',
195
+ message: 'maxEmbedDepth not set — using default (3). Set explicitly to prevent deep query DoS.',
196
+ file: handlerFile,
197
+ });
198
+ }
199
+
200
+ // rowOwnership for user-scoped tables
201
+ if (!handlerSource.includes('rowOwnership')) {
202
+ results.push({
203
+ severity: 'info',
204
+ category: 'security',
205
+ message: 'No rowOwnership configured — consider it for user-scoped mutation control',
206
+ file: handlerFile,
207
+ fix: 'Add rowOwnership for tables where users should only modify their own rows',
208
+ });
209
+ }
210
+ }
211
+
212
+ function checkSchemaConventions(projectPath: string, results: ValidationResult[]): void {
213
+ const schemaPath = join(projectPath, 'db', 'schema.ts');
214
+ const source = readFileSafe(schemaPath);
215
+ if (!source) return;
216
+
217
+ // Check for UUID primary keys (recommended)
218
+ const serialPkRegex = /serial\(['"]id['"]\)\.primaryKey/;
219
+ if (serialPkRegex.test(source)) {
220
+ results.push({
221
+ severity: 'info',
222
+ category: 'conventions',
223
+ message: 'Using serial IDs — UUID primary keys are recommended for distributed systems',
224
+ file: 'db/schema.ts',
225
+ });
226
+ }
227
+
228
+ // Check for timestamps
229
+ const tableNames = [...source.matchAll(/export\s+const\s+(\w+)\s*=\s*pgTable/g)].map((m) => m[1]);
230
+ for (const name of tableNames) {
231
+ // Find the table definition block
232
+ const tableRegex = new RegExp(`export\\s+const\\s+${name}\\s*=\\s*pgTable\\([^,]+,\\s*\\{([^}]+)\\}`);
233
+ const match = source.match(tableRegex);
234
+ if (match) {
235
+ const block = match[1];
236
+ if (!block.includes('created_at') && !block.includes('createdAt')) {
237
+ results.push({
238
+ severity: 'info',
239
+ category: 'conventions',
240
+ message: `Table '${name}' has no createdAt timestamp`,
241
+ file: 'db/schema.ts',
242
+ });
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ function checkMigrations(projectPath: string, results: ValidationResult[]): void {
249
+ const migrationsDir = join(projectPath, 'db', 'migrations');
250
+ const serverDrizzle = join(projectPath, 'server', 'drizzle');
251
+
252
+ const hasDbMigrations = existsSync(migrationsDir);
253
+ const hasServerDrizzle = existsSync(serverDrizzle);
254
+
255
+ if (!hasDbMigrations && !hasServerDrizzle) {
256
+ const hasSchema = existsSync(join(projectPath, 'db', 'schema.ts'));
257
+ if (hasSchema) {
258
+ results.push({
259
+ severity: 'warning',
260
+ category: 'database',
261
+ message: 'Schema exists but no migrations directory found',
262
+ fix: 'Run `npx drizzle-kit generate` to create migrations',
263
+ });
264
+ }
265
+ return;
266
+ }
267
+
268
+ // Check for RLS in migrations
269
+ const dir = hasServerDrizzle ? serverDrizzle : migrationsDir;
270
+ try {
271
+ const files = readdirSync(dir).filter((f) => f.endsWith('.sql'));
272
+ let hasRls = false;
273
+
274
+ for (const f of files) {
275
+ const content = readFileSafe(join(dir, f));
276
+ if (content && content.includes('ENABLE ROW LEVEL SECURITY')) {
277
+ hasRls = true;
278
+ break;
279
+ }
280
+ }
281
+
282
+ if (!hasRls && files.length > 0) {
283
+ results.push({
284
+ severity: 'warning',
285
+ category: 'security',
286
+ message: 'No RLS policies found in migrations — tables are not protected at database level',
287
+ fix: 'Add RLS policies via custom SQL migrations. Read everystack://security for templates.',
288
+ });
289
+ }
290
+ } catch {
291
+ // ignore
292
+ }
293
+ }
294
+
295
+ function checkDeploymentReadiness(projectPath: string, results: ValidationResult[]): void {
296
+ // SST config
297
+ if (!existsSync(join(projectPath, 'sst.config.ts'))) {
298
+ results.push({
299
+ severity: 'info',
300
+ category: 'deployment',
301
+ message: 'No sst.config.ts — project is not configured for AWS deployment',
302
+ fix: 'Create sst.config.ts following everystack://deployment',
303
+ });
304
+ return;
305
+ }
306
+
307
+ const sstConfig = readFileSafe(join(projectPath, 'sst.config.ts'));
308
+ if (sstConfig) {
309
+ // Check for VPC configuration
310
+ if (!sstConfig.includes('Vpc') && !sstConfig.includes('vpc')) {
311
+ results.push({
312
+ severity: 'info',
313
+ category: 'deployment',
314
+ message: 'No VPC configured in sst.config.ts — RDS requires a VPC',
315
+ });
316
+ }
317
+ }
318
+ }
319
+
320
+ function checkFileConventions(projectPath: string, results: ValidationResult[]): void {
321
+ // Check for default exports (everystack uses named exports)
322
+ const srcDirs = ['server', 'db', 'lib'];
323
+ for (const dir of srcDirs) {
324
+ const dirPath = join(projectPath, dir);
325
+ if (!existsSync(dirPath)) continue;
326
+
327
+ try {
328
+ const files = readdirSync(dirPath).filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts'));
329
+ for (const f of files) {
330
+ const content = readFileSafe(join(dirPath, f));
331
+ if (content && /^export\s+default\s/m.test(content)) {
332
+ results.push({
333
+ severity: 'info',
334
+ category: 'conventions',
335
+ message: `${dir}/${f} uses default export — everystack convention prefers named exports`,
336
+ file: `${dir}/${f}`,
337
+ });
338
+ }
339
+ }
340
+ } catch {
341
+ // ignore
342
+ }
343
+ }
344
+ }
345
+
346
+ export function validateProject(projectPath: string): ValidationReport {
347
+ const results: ValidationResult[] = [];
348
+
349
+ checkPackageJson(projectPath, results);
350
+ checkSecurityConfig(projectPath, results);
351
+ checkHandlerConfig(projectPath, results);
352
+ checkSchemaConventions(projectPath, results);
353
+ checkMigrations(projectPath, results);
354
+ checkDeploymentReadiness(projectPath, results);
355
+ checkFileConventions(projectPath, results);
356
+
357
+ // Sort: errors first, then warnings, then info
358
+ const order: Record<Severity, number> = { error: 0, warning: 1, info: 2 };
359
+ results.sort((a, b) => order[a.severity] - order[b.severity]);
360
+
361
+ return {
362
+ results,
363
+ summary: {
364
+ errors: results.filter((r) => r.severity === 'error').length,
365
+ warnings: results.filter((r) => r.severity === 'warning').length,
366
+ info: results.filter((r) => r.severity === 'info').length,
367
+ },
368
+ };
369
+ }