@everystack/mcp 0.2.3 → 0.3.1
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 +37 -10
- package/dist/adding-database.md +169 -0
- package/dist/admin.md +81 -0
- package/dist/auth.md +115 -0
- package/dist/aws-setup.md +276 -0
- package/dist/cli.md +108 -0
- package/dist/client-api.md +145 -0
- package/dist/core.md +196 -0
- package/dist/deployment.md +146 -0
- package/dist/events.md +87 -0
- package/dist/first-run.md +100 -0
- package/dist/getting-started.md +75 -0
- package/dist/handler-options.md +114 -0
- package/dist/images.md +93 -0
- package/dist/index.cjs +23796 -0
- package/dist/jobs.md +97 -0
- package/dist/logging.md +91 -0
- package/dist/plugins.md +68 -0
- package/dist/project-claude-md.md +103 -0
- package/dist/query-protocol.md +129 -0
- package/dist/schema-patterns.md +167 -0
- package/dist/security-device.md +99 -0
- package/dist/security.md +270 -0
- package/dist/ssr.md +82 -0
- package/dist/storage.md +63 -0
- package/dist/testing.md +118 -0
- package/package.json +11 -9
- package/src/gates/detectors/embedded-data-bundle.ts +58 -0
- package/src/gates/detectors/hand-written-migration.ts +42 -0
- package/src/gates/detectors/secret-in-public-env.ts +41 -0
- package/src/gates/engine.ts +80 -0
- package/src/gates/registry.ts +25 -0
- package/src/gates/telemetry.ts +143 -0
- package/src/gates/types.ts +70 -0
- package/src/governance/cli.ts +193 -0
- package/src/governance/grounding.ts +344 -0
- package/src/index.ts +97 -50
- package/src/prompts/claude-md.ts +92 -0
- package/src/prompts/governance-setup.ts +85 -0
- package/src/prompts/index.ts +6 -0
- package/src/prompts/new-app.ts +4 -1
- package/src/prompts/runbook.ts +77 -0
- package/src/resources/project-claude-md.md +70 -94
- package/src/tools/index.ts +6 -39
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grounding — enforce reading the project contract before any non-Read tool.
|
|
3
|
+
*
|
|
4
|
+
* Ported from local-agent's grounding gate (the proven implementation; see
|
|
5
|
+
* ~/Sites/local-agent/docs/grounding-gate.md). The contract: the CLAUDE.md files
|
|
6
|
+
* (global + project) and their declared `REQUIRED-READS:` companions define how
|
|
7
|
+
* the build is meant to go — for everystack that includes the Model/Module-first
|
|
8
|
+
* resources a project names. A model does not get to glide past them as scenery.
|
|
9
|
+
* Until the required files are actually Read this session, every tool EXCEPT Read
|
|
10
|
+
* is denied.
|
|
11
|
+
*
|
|
12
|
+
* everystack adaptation: state lives under ~/.everystack/governance/grounding so
|
|
13
|
+
* it never collides with local-agent's own gate (~/.agent/grounding). The pure
|
|
14
|
+
* functions (requiredFiles / gate / mark / context / groundStatus) are exported
|
|
15
|
+
* for governance/cli.ts to drive from hook JSON; the CLI shim lives there.
|
|
16
|
+
*
|
|
17
|
+
* Honest ceiling: this forces the *read* (deliberate, in-context, links followed).
|
|
18
|
+
* It cannot force comprehension. It kills the one specific failure — skipping the
|
|
19
|
+
* files — dead. Nothing more is claimed.
|
|
20
|
+
*
|
|
21
|
+
* Design decisions (from the bash → TS rewrite that proved this out):
|
|
22
|
+
* - Required set is DECLARED, not inferred: the global and project CLAUDE.md are
|
|
23
|
+
* always required, and each names its mandatory companions with an explicit
|
|
24
|
+
* `REQUIRED-READS:` line. (Scraping every Markdown link over-captures incidental
|
|
25
|
+
* prose links, so the contract declares its own reads instead.)
|
|
26
|
+
* - Paths are canonicalised with realpath on BOTH sides, so a symlinked root
|
|
27
|
+
* (/tmp -> /private/tmp) can't silently fail to match.
|
|
28
|
+
* - Partial reads (Read with offset/limit) do NOT count as grounding.
|
|
29
|
+
* - State is one JSON file per session; resets only on startup/clear, never on
|
|
30
|
+
* resume/compact — and `context` re-injects the contract on compact so it
|
|
31
|
+
* re-enters context after summarisation.
|
|
32
|
+
* - Fail-OPEN on any internal error or empty required set (never brick tool use);
|
|
33
|
+
* fail-CLOSED only on the explicit "not yet grounded" condition.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
existsSync,
|
|
38
|
+
realpathSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
readdirSync,
|
|
41
|
+
statSync,
|
|
42
|
+
mkdirSync,
|
|
43
|
+
writeFileSync,
|
|
44
|
+
unlinkSync,
|
|
45
|
+
} from 'fs';
|
|
46
|
+
import { join, dirname, resolve, isAbsolute } from 'path';
|
|
47
|
+
import { homedir } from 'os';
|
|
48
|
+
|
|
49
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
50
|
+
|
|
51
|
+
/** Home dir, overridable via $HOME (hook contexts + tests); falls back to os.homedir(). */
|
|
52
|
+
function home(): string {
|
|
53
|
+
return process.env.HOME || homedir();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function globalClaudeMd(): string {
|
|
57
|
+
return join(home(), '.claude', 'CLAUDE.md');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stateDir(): string {
|
|
61
|
+
return join(home(), '.everystack', 'governance', 'grounding');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Canonical absolute path: resolve symlinks if the file exists, else normalise. */
|
|
65
|
+
function canonical(p: string): string {
|
|
66
|
+
try {
|
|
67
|
+
return realpathSync(p);
|
|
68
|
+
} catch {
|
|
69
|
+
return resolve(p);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Expand a leading `~` (or `~/`) to the home directory. */
|
|
74
|
+
function expandTilde(p: string): string {
|
|
75
|
+
if (p === '~') return home();
|
|
76
|
+
if (p.startsWith('~/')) return join(home(), p.slice(2));
|
|
77
|
+
return p;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Nearest project CLAUDE.md walking up from cwd, stopping at $HOME or filesystem root. */
|
|
81
|
+
function nearestProjectClaudeMd(cwd: string): string | null {
|
|
82
|
+
let d = resolve(cwd);
|
|
83
|
+
const stop = resolve(home());
|
|
84
|
+
while (d && d !== '/' && d !== stop) {
|
|
85
|
+
const candidate = join(d, 'CLAUDE.md');
|
|
86
|
+
if (existsSync(candidate)) return candidate;
|
|
87
|
+
const parent = dirname(d);
|
|
88
|
+
if (parent === d) break;
|
|
89
|
+
d = parent;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Parse an optional `REQUIRED-READS: a.md, b/c.md` line; paths relative to the CLAUDE.md dir. */
|
|
95
|
+
function parseRequiredReads(claudeMdPath: string): string[] {
|
|
96
|
+
let text: string;
|
|
97
|
+
try {
|
|
98
|
+
text = readFileSync(claudeMdPath, 'utf-8');
|
|
99
|
+
} catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const m = text.match(/^[ \t]*REQUIRED-READS:[ \t]*(.+)$/im);
|
|
103
|
+
if (!m) return [];
|
|
104
|
+
const base = dirname(claudeMdPath);
|
|
105
|
+
return m[1]
|
|
106
|
+
.split(',')
|
|
107
|
+
.map((s) => s.trim())
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.map(expandTilde)
|
|
110
|
+
.map((rel) => (isAbsolute(rel) ? rel : resolve(base, rel)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RequiredSet {
|
|
114
|
+
/** Canonical paths of required files that exist (the gate can be satisfied by these). */
|
|
115
|
+
present: string[];
|
|
116
|
+
/** Required paths referenced but missing on disk (logged/warned, never block on them). */
|
|
117
|
+
missing: string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the required-reads set for a working directory:
|
|
122
|
+
* global CLAUDE.md + its REQUIRED-READS entries
|
|
123
|
+
* + nearest project CLAUDE.md + its REQUIRED-READS entries.
|
|
124
|
+
* Returns canonical, de-duplicated paths partitioned into present / missing.
|
|
125
|
+
*/
|
|
126
|
+
export function requiredFiles(cwd: string): RequiredSet {
|
|
127
|
+
const candidates: string[] = [];
|
|
128
|
+
|
|
129
|
+
const gmd = globalClaudeMd();
|
|
130
|
+
if (existsSync(gmd)) {
|
|
131
|
+
candidates.push(gmd);
|
|
132
|
+
candidates.push(...parseRequiredReads(gmd));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const proj = nearestProjectClaudeMd(cwd);
|
|
136
|
+
if (proj) {
|
|
137
|
+
candidates.push(proj);
|
|
138
|
+
candidates.push(...parseRequiredReads(proj));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const present: string[] = [];
|
|
142
|
+
const missing: string[] = [];
|
|
143
|
+
const seen = new Set<string>();
|
|
144
|
+
for (const c of candidates) {
|
|
145
|
+
if (existsSync(c)) {
|
|
146
|
+
const canon = canonical(c);
|
|
147
|
+
if (!seen.has(canon)) {
|
|
148
|
+
seen.add(canon);
|
|
149
|
+
present.push(canon);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
const norm = resolve(c);
|
|
153
|
+
if (!seen.has(norm)) {
|
|
154
|
+
seen.add(norm);
|
|
155
|
+
missing.push(norm);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { present, missing };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Per-session state
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
interface State {
|
|
167
|
+
reads: string[];
|
|
168
|
+
grounded: boolean;
|
|
169
|
+
updated: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function safeId(sessionId: string): string {
|
|
173
|
+
return (sessionId || 'nosession').replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function statePath(sessionId: string): string {
|
|
177
|
+
return join(stateDir(), `${safeId(sessionId)}.json`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function loadState(sessionId: string): State {
|
|
181
|
+
try {
|
|
182
|
+
const raw = JSON.parse(readFileSync(statePath(sessionId), 'utf-8'));
|
|
183
|
+
return {
|
|
184
|
+
reads: Array.isArray(raw.reads) ? raw.reads : [],
|
|
185
|
+
grounded: !!raw.grounded,
|
|
186
|
+
updated: typeof raw.updated === 'string' ? raw.updated : '',
|
|
187
|
+
};
|
|
188
|
+
} catch {
|
|
189
|
+
return { reads: [], grounded: false, updated: '' };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function saveState(sessionId: string, state: State): void {
|
|
194
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
195
|
+
writeFileSync(statePath(sessionId), JSON.stringify(state));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function resetState(sessionId: string): void {
|
|
199
|
+
try {
|
|
200
|
+
unlinkSync(statePath(sessionId));
|
|
201
|
+
} catch {
|
|
202
|
+
/* nothing to reset */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Delete session-state files older than 7 days. */
|
|
207
|
+
function prune(): void {
|
|
208
|
+
let files: string[];
|
|
209
|
+
try {
|
|
210
|
+
files = readdirSync(stateDir());
|
|
211
|
+
} catch {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
for (const f of files) {
|
|
216
|
+
const p = join(stateDir(), f);
|
|
217
|
+
try {
|
|
218
|
+
if (now - statSync(p).mtimeMs > MAX_AGE_MS) unlinkSync(p);
|
|
219
|
+
} catch {
|
|
220
|
+
/* skip */
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Public surface: gate / mark / context
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
export interface GateResult {
|
|
230
|
+
decision: 'allow' | 'deny';
|
|
231
|
+
unread: string[];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* PreToolUse decision. Always allows `Read`. Allows everything once the session
|
|
236
|
+
* is grounded. Fails open if no required files resolve. Otherwise denies any
|
|
237
|
+
* non-Read tool while required files remain unread.
|
|
238
|
+
*/
|
|
239
|
+
export function gate(sessionId: string, cwd: string, toolName: string): GateResult {
|
|
240
|
+
if (toolName === 'Read') return { decision: 'allow', unread: [] };
|
|
241
|
+
|
|
242
|
+
const { present } = requiredFiles(cwd);
|
|
243
|
+
if (present.length === 0) return { decision: 'allow', unread: [] }; // nothing to ground on
|
|
244
|
+
|
|
245
|
+
const state = loadState(sessionId);
|
|
246
|
+
if (state.grounded) return { decision: 'allow', unread: [] };
|
|
247
|
+
|
|
248
|
+
const read = new Set(state.reads);
|
|
249
|
+
const unread = present.filter((p) => !read.has(p));
|
|
250
|
+
if (unread.length === 0) {
|
|
251
|
+
saveState(sessionId, { reads: state.reads, grounded: true, updated: new Date().toISOString() });
|
|
252
|
+
return { decision: 'allow', unread: [] };
|
|
253
|
+
}
|
|
254
|
+
return { decision: 'deny', unread };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface MarkResult {
|
|
258
|
+
grounded: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* PostToolUse(Read) record. A full read of a required file marks it grounded.
|
|
263
|
+
* Partial reads (offset/limit present) are ignored — a one-line peek is not
|
|
264
|
+
* grounding. Reads of non-required files are ignored.
|
|
265
|
+
*/
|
|
266
|
+
export function mark(sessionId: string, cwd: string, filePath: string, partial = false): MarkResult {
|
|
267
|
+
if (partial || !filePath) return { grounded: loadState(sessionId).grounded };
|
|
268
|
+
|
|
269
|
+
const { present } = requiredFiles(cwd);
|
|
270
|
+
const canon = canonical(filePath);
|
|
271
|
+
if (!present.includes(canon)) return { grounded: loadState(sessionId).grounded };
|
|
272
|
+
|
|
273
|
+
const state = loadState(sessionId);
|
|
274
|
+
const reads = state.reads.includes(canon) ? state.reads : [...state.reads, canon];
|
|
275
|
+
const grounded = present.every((p) => reads.includes(p));
|
|
276
|
+
saveState(sessionId, { reads, grounded, updated: new Date().toISOString() });
|
|
277
|
+
return { grounded };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* SessionStart payload: the required-file list injected into context. Resets
|
|
282
|
+
* per-session state on `startup`/`clear` (fresh grounding required); leaves it
|
|
283
|
+
* intact on `resume`/`compact` but still re-injects the contract so it survives
|
|
284
|
+
* summarisation. Also runs 7-day housekeeping.
|
|
285
|
+
*/
|
|
286
|
+
export function context(sessionId: string, cwd: string, source: string): string {
|
|
287
|
+
prune();
|
|
288
|
+
if (source === 'startup' || source === 'clear') resetState(sessionId);
|
|
289
|
+
|
|
290
|
+
const { present, missing } = requiredFiles(cwd);
|
|
291
|
+
const lines: string[] = [];
|
|
292
|
+
lines.push('GROUNDING GATE ACTIVE — this is the contract for how this project is built.');
|
|
293
|
+
lines.push(
|
|
294
|
+
'Before ANY other tool this session you MUST Read these files (Read is the only tool allowed until you have). Read them, act on them, follow their links:'
|
|
295
|
+
);
|
|
296
|
+
if (present.length === 0) {
|
|
297
|
+
lines.push(' (no CLAUDE.md contract found for this directory — gate is inactive)');
|
|
298
|
+
} else {
|
|
299
|
+
for (const f of present) lines.push(` - ${f}`);
|
|
300
|
+
}
|
|
301
|
+
if (missing.length > 0) {
|
|
302
|
+
lines.push('');
|
|
303
|
+
lines.push('Note — referenced but missing on disk (skipped, not required):');
|
|
304
|
+
for (const f of missing) lines.push(` - ${f}`);
|
|
305
|
+
}
|
|
306
|
+
lines.push('');
|
|
307
|
+
lines.push('You do not get the right to ignore them.');
|
|
308
|
+
return lines.join('\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Human-readable grounding status for a session/cwd (the MCP introspection path). */
|
|
312
|
+
export function groundStatus(cwd: string, sessionId?: string): string {
|
|
313
|
+
const { present, missing } = requiredFiles(cwd);
|
|
314
|
+
const lines: string[] = [];
|
|
315
|
+
lines.push(`Required reads for ${resolve(cwd)}:`);
|
|
316
|
+
if (present.length === 0) lines.push(' (none — no CLAUDE.md contract found; gate inactive)');
|
|
317
|
+
|
|
318
|
+
if (sessionId) {
|
|
319
|
+
const state = loadState(sessionId);
|
|
320
|
+
const read = new Set(state.reads);
|
|
321
|
+
for (const f of present) lines.push(` [${read.has(f) ? 'x' : ' '}] ${f}`);
|
|
322
|
+
lines.push('');
|
|
323
|
+
lines.push(`Grounded: ${state.grounded ? 'yes' : 'no'} (session ${safeId(sessionId)})`);
|
|
324
|
+
} else {
|
|
325
|
+
for (const f of present) lines.push(` - ${f}`);
|
|
326
|
+
}
|
|
327
|
+
if (missing.length > 0) {
|
|
328
|
+
lines.push('');
|
|
329
|
+
lines.push('Referenced but missing (skipped):');
|
|
330
|
+
for (const f of missing) lines.push(` - ${f}`);
|
|
331
|
+
}
|
|
332
|
+
return lines.join('\n');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The PreToolUse deny reason shown when grounding is incomplete. */
|
|
336
|
+
export function denyReason(unread: string[]): string {
|
|
337
|
+
return [
|
|
338
|
+
'GROUNDING INCOMPLETE — you have not read the contract. Read these files first',
|
|
339
|
+
'(Read is allowed; every other tool is blocked until you have):',
|
|
340
|
+
...unread.map((f) => ` - ${f}`),
|
|
341
|
+
'',
|
|
342
|
+
'This is how this project is built. You do not get to skip it.',
|
|
343
|
+
].join('\n');
|
|
344
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,58 +1,105 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
|
|
3
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
3
|
import { registerResources } from './resources/index.js';
|
|
6
4
|
import { registerTools } from './tools/index.js';
|
|
7
5
|
import { registerPrompts } from './prompts/index.js';
|
|
6
|
+
import { GOVERNANCE_COMMANDS, runGovernanceCli } from './governance/cli.js';
|
|
7
|
+
|
|
8
|
+
// Injected at build time by scripts/build.mjs (esbuild --define) from package.json.
|
|
9
|
+
// Unbundled (dev via tsx), the identifier is undefined and we fall back to a literal.
|
|
10
|
+
declare const __MCP_VERSION__: string | undefined;
|
|
11
|
+
const version = (typeof __MCP_VERSION__ === 'string' ? __MCP_VERSION__ : null) ?? '0.3.0-dev';
|
|
12
|
+
|
|
13
|
+
const INSTRUCTIONS = [
|
|
14
|
+
'You govern how any agent builds everystack — a self-hosted application stack for Expo apps on AWS.',
|
|
15
|
+
'Your job is not only to advise but to keep the build on-script: the architecture the maintainer',
|
|
16
|
+
'set up (Models/Modules, generated migrations, the ui package, the secrets boundary) is the way,',
|
|
17
|
+
'and the training-consensus shortcut (bundle the JSON, hand-write the migration, hand-roll a',
|
|
18
|
+
'component) is the cheat. The cheat gates enforce this structurally; you reinforce it in guidance.',
|
|
19
|
+
'',
|
|
20
|
+
'## Governance setup (do this once, early)',
|
|
21
|
+
'',
|
|
22
|
+
'This server is the knowledge + helpful path. The ENFORCEMENT (grounding gate + cheat gates) runs',
|
|
23
|
+
'as Claude Code hooks, NOT from this server alone. If the hooks are not installed, none of the',
|
|
24
|
+
'gates fire — so check and offer to install them at the start of work on an everystack project:',
|
|
25
|
+
'1. Look for the everystack governance hooks in the project\'s `.claude/settings.json` (a',
|
|
26
|
+
' `hooks` block whose commands call `everystack-mcp gate`/`context`/`mark`/`validate`).',
|
|
27
|
+
'2. If they are absent, read this package\'s `hooks.example.json` and `docs/governance-setup.md`',
|
|
28
|
+
' (in @everystack/mcp), then OFFER to merge the `hooks` block into `.claude/settings.json`.',
|
|
29
|
+
'3. NEVER install them silently — hooks run a shell command on every tool call. Show the human',
|
|
30
|
+
' the exact block and the command form (workspace `npx tsx packages/mcp/src/index.ts <sub>` vs',
|
|
31
|
+
' installed `everystack-mcp <sub>`), and let them confirm.',
|
|
32
|
+
'4. The `governance_setup` prompt produces the exact block for the current project.',
|
|
33
|
+
'',
|
|
34
|
+
'## The project contract (CLAUDE.md)',
|
|
35
|
+
'',
|
|
36
|
+
'The grounding gate has nothing to enforce without a project `CLAUDE.md` — and writing one is',
|
|
37
|
+
'what turns grounding ON (the nearest project CLAUDE.md is auto-required reading). Early in any',
|
|
38
|
+
'everystack project, ensure a CLAUDE.md exists AND is current:',
|
|
39
|
+
'- If it is missing, offer to scaffold it (the `claude_md` prompt; template at',
|
|
40
|
+
' everystack://project-claude-md). It carries the non-negotiables the cheat gates enforce.',
|
|
41
|
+
'- If it exists, it is a living contract — as the project adds packages, models, or a tier, it',
|
|
42
|
+
' drifts. Offer to reconcile it (also `claude_md`): suggest updates against the project reality',
|
|
43
|
+
' and the current conventions, confirm-first, and never clobber the human\'s own content.',
|
|
44
|
+
'',
|
|
45
|
+
'## Beginner Detection',
|
|
46
|
+
'',
|
|
47
|
+
'Detect the user\'s experience level from context:',
|
|
48
|
+
'- If the user describes an app idea without technical terms, has no existing project, or asks basic questions ("how do I start?", "I want to build..."), treat them as a BEGINNER.',
|
|
49
|
+
'- If the user references specific packages, tiers (V1/V2/V3), tools (Drizzle, PostgREST, SST), or has an existing everystack project, treat them as a DEVELOPER.',
|
|
50
|
+
'',
|
|
51
|
+
'## Beginner Flow',
|
|
52
|
+
'',
|
|
53
|
+
'For beginners:',
|
|
54
|
+
'1. Read everystack://getting-started first. This is your guide for how to talk to beginners.',
|
|
55
|
+
'2. Ask what they want to build. Listen to their idea before anything technical. Do NOT categorize into tiers or ask them to choose V1/V2/V3.',
|
|
56
|
+
'3. Run check_environment with phase "local". Only V1 prerequisites matter now (Node.js, git, pnpm). PostgreSQL is NOT needed yet.',
|
|
57
|
+
' Do NOT require AWS tools (AWS CLI, credentials, SST) upfront. The user will build and run locally first.',
|
|
58
|
+
'4. Walk through installing missing prerequisites ONE AT A TIME. Do not list them all at once. Install the first missing tool, confirm it works, then move to the next.',
|
|
59
|
+
'5. Create a stock Expo app: `npx create-expo-app@latest`, install V1 packages (`@everystack/server`, `@everystack/cli`, `@everystack/ui`), and `pnpm add -D sst`.',
|
|
60
|
+
'6. Run `npx expo start`. Read everystack://first-run for the walkthrough. The first milestone is seeing the app running in a browser.',
|
|
61
|
+
'7. Build together. Discuss what the app should look like. Create screens, navigation, components. Everything is static and visual.',
|
|
62
|
+
'8. When a feature needs user accounts or saved data, read everystack://adding-database. This introduces PostgreSQL, the API handler, auth, and database setup step by step.',
|
|
63
|
+
'9. When a feature needs file uploads or background tasks, add the relevant packages incrementally (everystack://storage, everystack://jobs, everystack://images).',
|
|
64
|
+
'10. When the user is ready to deploy to the internet, run check_environment with phase "deploy" and walk through AWS setup.',
|
|
65
|
+
' For AWS credentials specifically, read everystack://aws-setup and walk them through account creation and IAM setup step by step.',
|
|
66
|
+
'11. Never use jargon without explaining it. "Lambda" is "the server that runs your code." "S3" is "file storage." "RDS" is "the database server." "CloudFront" is "the CDN that makes your app fast worldwide."',
|
|
67
|
+
'',
|
|
68
|
+
'## Developer Flow',
|
|
69
|
+
'',
|
|
70
|
+
'For experienced developers:',
|
|
71
|
+
'1. Read everystack://core for architecture and conventions.',
|
|
72
|
+
'2. Read everystack://security before any deployment or auth guidance.',
|
|
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 changes go through generated migrations (`db:generate`), never hand-written DDL. Reuse `@everystack/ui` components; never put secret values behind `EXPO_PUBLIC_*`.',
|
|
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
|
+
'6. When the user needs to interact with deployed infrastructure, guide them to use the everystack CLI.',
|
|
77
|
+
].join('\n');
|
|
8
78
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
instructions:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'',
|
|
21
|
-
'## Beginner Flow',
|
|
22
|
-
'',
|
|
23
|
-
'For beginners:',
|
|
24
|
-
'1. Read everystack://getting-started first. This is your guide for how to talk to beginners.',
|
|
25
|
-
'2. Ask what they want to build. Listen to their idea before anything technical. Do NOT categorize into tiers or ask them to choose V1/V2/V3.',
|
|
26
|
-
'3. Run check_environment with phase "local". Only V1 prerequisites matter now (Node.js, git, pnpm). PostgreSQL is NOT needed yet.',
|
|
27
|
-
' Do NOT require AWS tools (AWS CLI, credentials, SST) upfront. The user will build and run locally first.',
|
|
28
|
-
'4. Walk through installing missing prerequisites ONE AT A TIME. Do not list them all at once. Install the first missing tool, confirm it works, then move to the next.',
|
|
29
|
-
'5. Create a stock Expo app: `npx create-expo-app@latest`, install V1 packages (`@everystack/server`, `@everystack/cli`, `@everystack/ui`), and `pnpm add -D sst`.',
|
|
30
|
-
'6. Run `npx expo start`. Read everystack://first-run for the walkthrough. The first milestone is seeing the app running in a browser.',
|
|
31
|
-
'7. Build together. Discuss what the app should look like. Create screens, navigation, components. Everything is static and visual.',
|
|
32
|
-
'8. When a feature needs user accounts or saved data, read everystack://adding-database. This introduces PostgreSQL, the API handler, auth, and database setup step by step.',
|
|
33
|
-
'9. When a feature needs file uploads or background tasks, add the relevant packages incrementally (everystack://storage, everystack://jobs, everystack://images).',
|
|
34
|
-
'10. When the user is ready to deploy to the internet, run check_environment with phase "deploy" and walk through AWS setup.',
|
|
35
|
-
' For AWS credentials specifically, read everystack://aws-setup and walk them through account creation and IAM setup step by step.',
|
|
36
|
-
'11. Never use jargon without explaining it. "Lambda" is "the server that runs your code." "S3" is "file storage." "RDS" is "the database server." "CloudFront" is "the CDN that makes your app fast worldwide."',
|
|
37
|
-
'',
|
|
38
|
-
'## Developer Flow',
|
|
39
|
-
'',
|
|
40
|
-
'For experienced developers:',
|
|
41
|
-
'1. Read everystack://core for architecture and conventions.',
|
|
42
|
-
'2. Read everystack://security before any deployment or auth guidance.',
|
|
43
|
-
'3. Load detail resources on demand when the user asks about specific features.',
|
|
44
|
-
'4. Use the project_status tool to understand the user\'s current project state.',
|
|
45
|
-
'5. Use the schema_analyze tool when helping with database schema or RLS.',
|
|
46
|
-
'6. Use the project_validate tool to check for common mistakes before deployment.',
|
|
47
|
-
'7. When the user wants to start a new project, run check_environment (phase "local" for dev, "deploy" for deployment) to verify prerequisites.',
|
|
48
|
-
'8. When the user needs to interact with deployed infrastructure, guide them to use the everystack CLI.',
|
|
49
|
-
].join('\n'),
|
|
50
|
-
},
|
|
51
|
-
);
|
|
79
|
+
async function startServer(): Promise<void> {
|
|
80
|
+
const server = new McpServer(
|
|
81
|
+
{ name: '@everystack/mcp', version },
|
|
82
|
+
{ instructions: INSTRUCTIONS },
|
|
83
|
+
);
|
|
84
|
+
registerResources(server);
|
|
85
|
+
registerTools(server);
|
|
86
|
+
registerPrompts(server);
|
|
87
|
+
const transport = new StdioServerTransport();
|
|
88
|
+
await server.connect(transport);
|
|
89
|
+
}
|
|
52
90
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
async function main(): Promise<void> {
|
|
92
|
+
const sub = process.argv[2];
|
|
93
|
+
// Hook-shim CLI: `everystack-mcp <context|gate|mark|validate|report>` reads hook JSON on stdin.
|
|
94
|
+
if (sub && (GOVERNANCE_COMMANDS as readonly string[]).includes(sub)) {
|
|
95
|
+
await runGovernanceCli(process.argv.slice(2));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Default: the stdio MCP server.
|
|
99
|
+
await startServer();
|
|
100
|
+
}
|
|
56
101
|
|
|
57
|
-
|
|
58
|
-
|
|
102
|
+
main().catch((err) => {
|
|
103
|
+
console.error(err);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `claude_md` — create or reconcile the project's CLAUDE.md.
|
|
6
|
+
*
|
|
7
|
+
* The CLAUDE.md is the project contract. Writing it activates the grounding gate
|
|
8
|
+
* (the nearest project CLAUDE.md is auto-required). But a contract is a living
|
|
9
|
+
* document — it drifts from reality as the project grows. So this prompt is
|
|
10
|
+
* bidirectional: scaffold it when missing, and review + suggest updates when it
|
|
11
|
+
* exists (a drift check on the contract itself), never clobbering human content.
|
|
12
|
+
*/
|
|
13
|
+
export function registerClaudeMdPrompt(server: McpServer): void {
|
|
14
|
+
server.prompt(
|
|
15
|
+
'claude_md',
|
|
16
|
+
'Create or keep the project CLAUDE.md current. Missing → scaffold from the everystack contract. Exists → review against the project reality + the cheat-gate rules and suggest updates (confirm-first, never clobber). Writing it activates the grounding gate.',
|
|
17
|
+
{
|
|
18
|
+
projectPath: z.string().optional().describe('Absolute path to the project root (where CLAUDE.md lives)'),
|
|
19
|
+
},
|
|
20
|
+
async ({ projectPath }) => {
|
|
21
|
+
const root = projectPath ?? '.';
|
|
22
|
+
return {
|
|
23
|
+
messages: [
|
|
24
|
+
{
|
|
25
|
+
role: 'user' as const,
|
|
26
|
+
content: {
|
|
27
|
+
type: 'text' as const,
|
|
28
|
+
text: [
|
|
29
|
+
`Create or reconcile the CLAUDE.md for the project at ${root}.`,
|
|
30
|
+
'',
|
|
31
|
+
'## Why this matters',
|
|
32
|
+
'',
|
|
33
|
+
'CLAUDE.md is this project\'s contract. The grounding gate auto-requires the nearest',
|
|
34
|
+
'project CLAUDE.md — so writing it is what turns grounding ON (the agent must read it',
|
|
35
|
+
'before editing). A contract also drifts as the project grows, so it must be kept current.',
|
|
36
|
+
'',
|
|
37
|
+
'## Step 1 — detect the project reality',
|
|
38
|
+
'',
|
|
39
|
+
`- Read ${root}/package.json: which @everystack/* packages are installed (tier: V1 server/cli/ui;`,
|
|
40
|
+
' V2 adds api/auth/admin/logging/security/query; V3 adds jobs/storage/images).',
|
|
41
|
+
`- Check for a models/ directory (defineModel files) and a db/ directory.`,
|
|
42
|
+
`- Note the project name and what the app does.`,
|
|
43
|
+
'',
|
|
44
|
+
'## Step 2 — read the template',
|
|
45
|
+
'',
|
|
46
|
+
'Read the everystack://project-claude-md resource. It is the everystack contract: the',
|
|
47
|
+
'Non-negotiables (which mirror the cheat gates), Key Principles, Commands, and the',
|
|
48
|
+
'What NOT to Do list. Fill {PROJECT_NAME}, {ONE_LINE_DESCRIPTION}, {ANNOTATED_DIRECTORY_TREE}',
|
|
49
|
+
'and tailor Structure/Commands to the packages actually installed.',
|
|
50
|
+
'',
|
|
51
|
+
`## Step 3 — does ${root}/CLAUDE.md already exist?`,
|
|
52
|
+
'',
|
|
53
|
+
'### If it does NOT exist → scaffold',
|
|
54
|
+
'',
|
|
55
|
+
`- Write ${root}/CLAUDE.md from the template, tailored to the detected reality.`,
|
|
56
|
+
'- Keep it tight and load-bearing — a wall of prose gets read as scenery. Lead with the',
|
|
57
|
+
' Non-negotiables.',
|
|
58
|
+
'- Tell the human: this activates the grounding gate; new sessions must read it first.',
|
|
59
|
+
'',
|
|
60
|
+
'### If it EXISTS → review and suggest updates (DO NOT clobber)',
|
|
61
|
+
'',
|
|
62
|
+
'Reconcile the existing CLAUDE.md against three things, and propose a concrete diff:',
|
|
63
|
+
'1. **Framework drift** — does it teach superseded ways (e.g. `drizzle-kit generate`,',
|
|
64
|
+
' hand-written migrations, "pass your Drizzle schema") instead of the v3 Model flow',
|
|
65
|
+
' (`defineModel` → `everystack db:generate`)? Suggest the current conventions.',
|
|
66
|
+
'2. **Project drift** — do the Structure / Commands / package list match what is actually',
|
|
67
|
+
' installed now (new packages, a new tier, a new models/ dir)? Suggest updates. Also',
|
|
68
|
+
' check the operations manual: if docs/RUNBOOK.md is missing, or stale per',
|
|
69
|
+
' `everystack runbook --check`, suggest the `runbook` prompt.',
|
|
70
|
+
'3. **Contract-vs-gates drift** — is it missing any Non-negotiable the cheat gates enforce',
|
|
71
|
+
' (data→DB, generated migrations, declared authz, reuse @everystack/ui, secrets boundary)?',
|
|
72
|
+
' Suggest adding it, so the human contract and the enforced rules agree.',
|
|
73
|
+
'',
|
|
74
|
+
'Rules for reconciling:',
|
|
75
|
+
'- **Preserve all human/project-specific content** — project rules, domain notes, the',
|
|
76
|
+
' REQUIRED-READS line. Only update the framework/structure/commands sections and flag gaps.',
|
|
77
|
+
'- **Propose, then confirm.** Show the human the suggested changes (a diff) and let them',
|
|
78
|
+
' approve before writing. The CLAUDE.md is theirs; you assist, you do not own it.',
|
|
79
|
+
'- **Only flag real drift**, not cosmetic rewording.',
|
|
80
|
+
'',
|
|
81
|
+
'## Step 4 — confirm',
|
|
82
|
+
'',
|
|
83
|
+
'After writing/updating, confirm the file is at the project root so the grounding gate',
|
|
84
|
+
'picks it up, and summarize what changed.',
|
|
85
|
+
].join('\n'),
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
}
|