intentdna 1.5.2 → 1.5.4
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/sync.d.ts +23 -0
- package/dist/cli/commands/sync.js +76 -5
- package/dist/cli/commands/verify.d.ts +11 -0
- package/dist/cli/commands/verify.js +190 -0
- package/dist/cli/index.js +3 -0
- package/dist/compiler/compile.js +11 -0
- package/dist/governance/index.d.ts +7 -0
- package/dist/governance/index.js +7 -0
- package/dist/governance/types.d.ts +135 -0
- package/dist/governance/types.js +12 -0
- package/dist/hooks/cli.js +16 -2
- package/dist/hooks/enforce.d.ts +2 -0
- package/dist/hooks/enforce.js +71 -10
- package/dist/hooks/state.d.ts +18 -6
- package/dist/hooks/state.js +105 -34
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +40 -0
- package/dist/mcp/server.d.ts +39 -0
- package/dist/mcp/server.js +84 -0
- package/dist/mcp/tools-compile.d.ts +10 -0
- package/dist/mcp/tools-compile.js +214 -0
- package/dist/mcp/tools-enforce.d.ts +27 -0
- package/dist/mcp/tools-enforce.js +178 -0
- package/dist/mcp/tools-state.d.ts +11 -0
- package/dist/mcp/tools-state.js +160 -0
- package/dist/mcp/transport.d.ts +36 -0
- package/dist/mcp/transport.js +48 -0
- package/dist/schema/types.d.ts +16 -0
- package/package.json +3 -2
- package/spec/foundation-hardening.md +8 -8
package/dist/hooks/enforce.js
CHANGED
|
@@ -23,11 +23,21 @@ import { relative, normalize } from "node:path";
|
|
|
23
23
|
* Optional `roles` parameter provides output_schema enforcement.
|
|
24
24
|
*/
|
|
25
25
|
export function enforcePreToolUse(ir, input, state, roles) {
|
|
26
|
+
// G4 Layer 1: Step enforce rules (state-driven)
|
|
27
|
+
if (state?.workflowState && ir.workflows_ir) {
|
|
28
|
+
const stepRuleResult = enforceStepRules(ir, input, state);
|
|
29
|
+
if (stepRuleResult !== null)
|
|
30
|
+
return stepRuleResult;
|
|
31
|
+
}
|
|
26
32
|
// Layer 2: Role scope
|
|
27
33
|
if (input.agent_type && ir.roles_scope_map && ir.roles_scope_map.length > 0) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
// G4: Check if scope is relaxed by iteration-based rule
|
|
35
|
+
const relaxed = isStepScopeRelaxed(ir, state);
|
|
36
|
+
if (!relaxed) {
|
|
37
|
+
const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state));
|
|
38
|
+
if (result)
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
31
41
|
}
|
|
32
42
|
// Layer 3: Tool filters
|
|
33
43
|
if (ir.tool_filters.length > 0) {
|
|
@@ -293,6 +303,53 @@ export function enforceStop(ir, input, workflowState) {
|
|
|
293
303
|
return blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
|
|
294
304
|
messages.map(m => ` - ${m}`).join("\n"));
|
|
295
305
|
}
|
|
306
|
+
// ── G4: State-driven Step Enforcement ─────────────────────
|
|
307
|
+
/** Write tools that step read_only rule should block */
|
|
308
|
+
const ALL_WRITE_TOOLS = new Set(["Edit", "Write", "NotebookEdit", "Bash"]);
|
|
309
|
+
/**
|
|
310
|
+
* Find the StepEnforceRule for the current step in the active workflow.
|
|
311
|
+
*/
|
|
312
|
+
function findStepRule(ir, state) {
|
|
313
|
+
if (!state?.workflowState || !ir.workflows_ir)
|
|
314
|
+
return null;
|
|
315
|
+
const wf = ir.workflows_ir.find(w => w.workflow_name === state.workflowState.workflow);
|
|
316
|
+
if (!wf?.step_enforce_rules)
|
|
317
|
+
return null;
|
|
318
|
+
return wf.step_enforce_rules.find(r => r.step_id === state.workflowState.current_step) ?? null;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* G4: Enforce step-level rules.
|
|
322
|
+
* Returns HookOutput if a rule triggers (block for read_only), null to continue.
|
|
323
|
+
*/
|
|
324
|
+
function enforceStepRules(ir, input, state) {
|
|
325
|
+
const rule = findStepRule(ir, state);
|
|
326
|
+
if (!rule)
|
|
327
|
+
return null;
|
|
328
|
+
// read_only: block all write tools
|
|
329
|
+
if (rule.read_only && ALL_WRITE_TOOLS.has(input.tool_name)) {
|
|
330
|
+
return blockOutput(`[Intent DNA] Step '${rule.step_id}' is read-only. Write tool '${input.tool_name}' blocked.`);
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* G4: Check if scope enforcement is relaxed for this step based on iteration.
|
|
336
|
+
* When iteration >= relax_after_iteration, scope checks are skipped (rescue mode).
|
|
337
|
+
*/
|
|
338
|
+
function isStepScopeRelaxed(ir, state) {
|
|
339
|
+
const rule = findStepRule(ir, state);
|
|
340
|
+
if (!rule?.relax_after_iteration)
|
|
341
|
+
return false;
|
|
342
|
+
const iteration = state?.workflowState?.iteration ?? 1;
|
|
343
|
+
return iteration >= rule.relax_after_iteration;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* G4: Get additional write paths granted to the current step.
|
|
347
|
+
* These extend the role's scope for this specific step only.
|
|
348
|
+
*/
|
|
349
|
+
function getStepAdditionalPaths(ir, state) {
|
|
350
|
+
const rule = findStepRule(ir, state);
|
|
351
|
+
return rule?.additional_write_paths ?? [];
|
|
352
|
+
}
|
|
296
353
|
// ── Internal: Layer Enforcement ────────────────────────────
|
|
297
354
|
/**
|
|
298
355
|
* Enforce handoff consumes — verify that the current step's consumed
|
|
@@ -344,7 +401,7 @@ export function enforceHandoffProduces(ir, wfState) {
|
|
|
344
401
|
}
|
|
345
402
|
return null;
|
|
346
403
|
}
|
|
347
|
-
function enforceRoleScope(rolesScopeMap, input) {
|
|
404
|
+
function enforceRoleScope(rolesScopeMap, input, additionalWritePaths = []) {
|
|
348
405
|
if (!input.agent_type)
|
|
349
406
|
return null;
|
|
350
407
|
// Standard write tools: check single file path
|
|
@@ -352,7 +409,7 @@ function enforceRoleScope(rolesScopeMap, input) {
|
|
|
352
409
|
const filePath = extractFilePath(input);
|
|
353
410
|
if (!filePath)
|
|
354
411
|
return null;
|
|
355
|
-
return checkPathAgainstScope(rolesScopeMap, input, filePath);
|
|
412
|
+
return checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths);
|
|
356
413
|
}
|
|
357
414
|
// Bash: extract potential write targets from command
|
|
358
415
|
if (input.tool_name === "Bash") {
|
|
@@ -363,7 +420,7 @@ function enforceRoleScope(rolesScopeMap, input) {
|
|
|
363
420
|
if (writePaths.length === 0)
|
|
364
421
|
return null;
|
|
365
422
|
for (const p of writePaths) {
|
|
366
|
-
const result = checkPathAgainstScope(rolesScopeMap, input, p);
|
|
423
|
+
const result = checkPathAgainstScope(rolesScopeMap, input, p, additionalWritePaths);
|
|
367
424
|
if (result)
|
|
368
425
|
return result;
|
|
369
426
|
}
|
|
@@ -372,7 +429,7 @@ function enforceRoleScope(rolesScopeMap, input) {
|
|
|
372
429
|
return null;
|
|
373
430
|
}
|
|
374
431
|
/** Check a single file path against role scope. Shared by Write tools and Bash. */
|
|
375
|
-
function checkPathAgainstScope(rolesScopeMap, input, filePath) {
|
|
432
|
+
function checkPathAgainstScope(rolesScopeMap, input, filePath, additionalWritePaths = []) {
|
|
376
433
|
const cwd = input.cwd;
|
|
377
434
|
let relativePath = cwd && filePath.startsWith("/") ? relative(cwd, filePath) : filePath;
|
|
378
435
|
// Normalize to resolve traversal (e.g., "src/../../test/x.ts" → "../test/x.ts")
|
|
@@ -382,11 +439,15 @@ function checkPathAgainstScope(rolesScopeMap, input, filePath) {
|
|
|
382
439
|
if (input.agent_type !== agentTypeName)
|
|
383
440
|
continue;
|
|
384
441
|
const writeGlobs = entry.scope.write ?? [];
|
|
385
|
-
|
|
442
|
+
// G4: merge step-specific additional_write_paths
|
|
443
|
+
const allWriteGlobs = additionalWritePaths.length > 0
|
|
444
|
+
? [...writeGlobs, ...additionalWritePaths]
|
|
445
|
+
: writeGlobs;
|
|
446
|
+
if (allWriteGlobs.length === 0) {
|
|
386
447
|
return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' has no write permission (path: ${filePath}). Merge-time scope gate will filter.`);
|
|
387
448
|
}
|
|
388
|
-
if (!checkWriteAllowed(relativePath,
|
|
389
|
-
return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${
|
|
449
|
+
if (!checkWriteAllowed(relativePath, allWriteGlobs)) {
|
|
450
|
+
return allowOutput(`WARN [Intent DNA]: Role '${entry.role_name}' cannot write to '${filePath}' (allowed: ${allWriteGlobs.join(", ")}). Merge-time scope gate will filter.`);
|
|
390
451
|
}
|
|
391
452
|
return null; // Role matched, write allowed
|
|
392
453
|
}
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -79,19 +79,31 @@ export interface TraceEntry {
|
|
|
79
79
|
timestamp: string;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Append a trace entry
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* Append a trace entry with dual-write strategy:
|
|
83
|
+
* 1. Always → global trace: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (merged view)
|
|
84
|
+
* 2. If sessionId → session-local: `.dna/state/sessions/{id}/trace.jsonl`
|
|
85
|
+
*
|
|
86
|
+
* G1: Session isolation — session traces live in the session directory,
|
|
87
|
+
* global trace provides a unified view across all sessions.
|
|
85
88
|
* Fail-open: never throws.
|
|
86
89
|
*/
|
|
87
90
|
export declare function appendTrace(projectDir: string, entry: TraceEntry, sessionId?: string): Promise<void>;
|
|
88
91
|
/**
|
|
89
|
-
* Read trace entries
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
+
* Read trace entries.
|
|
93
|
+
*
|
|
94
|
+
* G1 session isolation:
|
|
95
|
+
* - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
|
|
96
|
+
* (falls back to legacy global files with sessionId in name for backward compat)
|
|
97
|
+
* - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
|
|
98
|
+
*
|
|
92
99
|
* Returns parsed entries sorted by timestamp.
|
|
93
100
|
*/
|
|
94
101
|
export declare function readTraces(projectDir: string, days?: number, sessionId?: string): Promise<TraceEntry[]>;
|
|
102
|
+
/**
|
|
103
|
+
* List active session directories under `.dna/state/sessions/`.
|
|
104
|
+
* Returns session IDs (directory names).
|
|
105
|
+
*/
|
|
106
|
+
export declare function listSessions(projectDir: string): Promise<string[]>;
|
|
95
107
|
/**
|
|
96
108
|
* Clean up trace files older than retention period.
|
|
97
109
|
* Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
|
package/dist/hooks/state.js
CHANGED
|
@@ -140,48 +140,104 @@ const TRACE_DIR = "trace";
|
|
|
140
140
|
const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
141
141
|
const TRACE_RETENTION_DAYS = 7;
|
|
142
142
|
/**
|
|
143
|
-
* Build trace file name.
|
|
144
|
-
*
|
|
145
|
-
* Without: `trace-{date}.jsonl` (backward compat / merged view)
|
|
143
|
+
* Build global trace file name (merged view, no sessionId).
|
|
144
|
+
* Legacy files with sessionId in name are still readable for backward compat.
|
|
146
145
|
*/
|
|
147
|
-
function
|
|
148
|
-
if (sessionId) {
|
|
149
|
-
return `trace-${date}-${sessionId}.jsonl`;
|
|
150
|
-
}
|
|
146
|
+
function globalTraceFileName(date) {
|
|
151
147
|
return `trace-${date}.jsonl`;
|
|
152
148
|
}
|
|
153
149
|
/**
|
|
154
|
-
* Append a trace entry
|
|
155
|
-
*
|
|
156
|
-
*
|
|
150
|
+
* Append a trace entry with dual-write strategy:
|
|
151
|
+
* 1. Always → global trace: `.dna/state/trace/trace-YYYY-MM-DD.jsonl` (merged view)
|
|
152
|
+
* 2. If sessionId → session-local: `.dna/state/sessions/{id}/trace.jsonl`
|
|
153
|
+
*
|
|
154
|
+
* G1: Session isolation — session traces live in the session directory,
|
|
155
|
+
* global trace provides a unified view across all sessions.
|
|
157
156
|
* Fail-open: never throws.
|
|
158
157
|
*/
|
|
159
158
|
export async function appendTrace(projectDir, entry, sessionId) {
|
|
160
159
|
try {
|
|
161
|
-
const
|
|
162
|
-
await mkdir(traceDir, { recursive: true });
|
|
160
|
+
const line = JSON.stringify(entry) + "\n";
|
|
163
161
|
const date = entry.timestamp.slice(0, 10);
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
// 1. Global trace (merged view)
|
|
163
|
+
const globalTraceDir = join(projectDir, ".dna", "state", TRACE_DIR);
|
|
164
|
+
await mkdir(globalTraceDir, { recursive: true });
|
|
165
|
+
const globalPath = join(globalTraceDir, globalTraceFileName(date));
|
|
166
166
|
try {
|
|
167
|
-
const stats = await stat(
|
|
167
|
+
const stats = await stat(globalPath);
|
|
168
168
|
if (stats.size >= MAX_TRACE_SIZE)
|
|
169
|
-
return;
|
|
169
|
+
return;
|
|
170
170
|
}
|
|
171
171
|
catch { /* file doesn't exist yet */ }
|
|
172
|
-
await appendFile(
|
|
172
|
+
await appendFile(globalPath, line, "utf-8");
|
|
173
|
+
// 2. Session-local trace (if session isolated)
|
|
174
|
+
if (sessionId) {
|
|
175
|
+
const sessionDir = resolveStateDir(projectDir, sessionId);
|
|
176
|
+
await mkdir(sessionDir, { recursive: true });
|
|
177
|
+
const sessionTracePath = join(sessionDir, "trace.jsonl");
|
|
178
|
+
try {
|
|
179
|
+
const stats = await stat(sessionTracePath);
|
|
180
|
+
if (stats.size >= MAX_TRACE_SIZE)
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
catch { /* file doesn't exist yet */ }
|
|
184
|
+
await appendFile(sessionTracePath, line, "utf-8");
|
|
185
|
+
}
|
|
173
186
|
}
|
|
174
187
|
catch {
|
|
175
188
|
// Fail-open: trace write failure never affects hook execution
|
|
176
189
|
}
|
|
177
190
|
}
|
|
178
191
|
/**
|
|
179
|
-
* Read trace entries
|
|
180
|
-
*
|
|
181
|
-
*
|
|
192
|
+
* Read trace entries.
|
|
193
|
+
*
|
|
194
|
+
* G1 session isolation:
|
|
195
|
+
* - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
|
|
196
|
+
* (falls back to legacy global files with sessionId in name for backward compat)
|
|
197
|
+
* - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
|
|
198
|
+
*
|
|
182
199
|
* Returns parsed entries sorted by timestamp.
|
|
183
200
|
*/
|
|
184
201
|
export async function readTraces(projectDir, days = 1, sessionId) {
|
|
202
|
+
const entries = [];
|
|
203
|
+
if (sessionId) {
|
|
204
|
+
// Session-specific: read from session directory first
|
|
205
|
+
const sessionDir = resolveStateDir(projectDir, sessionId);
|
|
206
|
+
const sessionTracePath = join(sessionDir, "trace.jsonl");
|
|
207
|
+
const sessionEntries = await readTraceFile(sessionTracePath);
|
|
208
|
+
entries.push(...sessionEntries);
|
|
209
|
+
// Backward compat: also check legacy files in global trace dir
|
|
210
|
+
if (entries.length === 0) {
|
|
211
|
+
const legacyEntries = await readGlobalTraces(projectDir, days, sessionId);
|
|
212
|
+
entries.push(...legacyEntries);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// Global merged view
|
|
217
|
+
const globalEntries = await readGlobalTraces(projectDir, days);
|
|
218
|
+
entries.push(...globalEntries);
|
|
219
|
+
}
|
|
220
|
+
return entries.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
221
|
+
}
|
|
222
|
+
/** Read all entries from a single trace file. */
|
|
223
|
+
async function readTraceFile(filePath) {
|
|
224
|
+
const entries = [];
|
|
225
|
+
try {
|
|
226
|
+
const content = await readFile(filePath, "utf-8");
|
|
227
|
+
for (const line of content.trim().split("\n")) {
|
|
228
|
+
if (!line)
|
|
229
|
+
continue;
|
|
230
|
+
try {
|
|
231
|
+
entries.push(JSON.parse(line));
|
|
232
|
+
}
|
|
233
|
+
catch { /* skip malformed */ }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch { /* file doesn't exist */ }
|
|
237
|
+
return entries;
|
|
238
|
+
}
|
|
239
|
+
/** Read global trace files from `.dna/state/trace/`. */
|
|
240
|
+
async function readGlobalTraces(projectDir, days, sessionIdFilter) {
|
|
185
241
|
const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
|
|
186
242
|
const entries = [];
|
|
187
243
|
const cutoff = new Date();
|
|
@@ -192,28 +248,20 @@ export async function readTraces(projectDir, days = 1, sessionId) {
|
|
|
192
248
|
const traceFiles = files
|
|
193
249
|
.filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"))
|
|
194
250
|
.filter(f => {
|
|
195
|
-
// Extract date from filename: trace-YYYY-MM-DD.jsonl or trace-YYYY-MM-DD-{sessionId}.jsonl
|
|
196
251
|
const fileDate = f.slice(6, 16); // "trace-YYYY-MM-DD..."
|
|
197
252
|
if (fileDate < cutoffDate)
|
|
198
253
|
return false;
|
|
199
|
-
//
|
|
200
|
-
if (
|
|
201
|
-
const suffix = f.slice(16);
|
|
202
|
-
return suffix === `-${
|
|
254
|
+
// Legacy session filter: files with sessionId in name
|
|
255
|
+
if (sessionIdFilter) {
|
|
256
|
+
const suffix = f.slice(16);
|
|
257
|
+
return suffix === `-${sessionIdFilter}.jsonl` || suffix === ".jsonl";
|
|
203
258
|
}
|
|
204
259
|
return true;
|
|
205
260
|
})
|
|
206
261
|
.sort();
|
|
207
262
|
for (const file of traceFiles) {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
if (!line)
|
|
211
|
-
continue;
|
|
212
|
-
try {
|
|
213
|
-
entries.push(JSON.parse(line));
|
|
214
|
-
}
|
|
215
|
-
catch { /* skip malformed */ }
|
|
216
|
-
}
|
|
263
|
+
const fileEntries = await readTraceFile(join(traceDir, file));
|
|
264
|
+
entries.push(...fileEntries);
|
|
217
265
|
}
|
|
218
266
|
}
|
|
219
267
|
catch {
|
|
@@ -221,6 +269,29 @@ export async function readTraces(projectDir, days = 1, sessionId) {
|
|
|
221
269
|
}
|
|
222
270
|
return entries;
|
|
223
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* List active session directories under `.dna/state/sessions/`.
|
|
274
|
+
* Returns session IDs (directory names).
|
|
275
|
+
*/
|
|
276
|
+
export async function listSessions(projectDir) {
|
|
277
|
+
const sessionsDir = join(projectDir, ".dna", "state", "sessions");
|
|
278
|
+
try {
|
|
279
|
+
const entries = await readdir(sessionsDir);
|
|
280
|
+
const sessions = [];
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
try {
|
|
283
|
+
const entryStat = await stat(join(sessionsDir, entry));
|
|
284
|
+
if (entryStat.isDirectory())
|
|
285
|
+
sessions.push(entry);
|
|
286
|
+
}
|
|
287
|
+
catch { /* skip */ }
|
|
288
|
+
}
|
|
289
|
+
return sessions;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
224
295
|
/**
|
|
225
296
|
* Clean up trace files older than retention period.
|
|
226
297
|
* Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Intent DNA — MCP Server (`dna-mcp`)
|
|
4
|
+
*
|
|
5
|
+
* Claude Code MCP server providing DNA governance tools:
|
|
6
|
+
* G2: State management (workflow, trace, status)
|
|
7
|
+
* G3: Compile toolchain (compile, validate, sync)
|
|
8
|
+
*
|
|
9
|
+
* Usage: dna-mcp [--project-dir <path>]
|
|
10
|
+
*
|
|
11
|
+
* Speaks JSON-RPC 2.0 over stdio (MCP protocol).
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Intent DNA — MCP Server (`dna-mcp`)
|
|
4
|
+
*
|
|
5
|
+
* Claude Code MCP server providing DNA governance tools:
|
|
6
|
+
* G2: State management (workflow, trace, status)
|
|
7
|
+
* G3: Compile toolchain (compile, validate, sync)
|
|
8
|
+
*
|
|
9
|
+
* Usage: dna-mcp [--project-dir <path>]
|
|
10
|
+
*
|
|
11
|
+
* Speaks JSON-RPC 2.0 over stdio (MCP protocol).
|
|
12
|
+
*/
|
|
13
|
+
import { createMCPServer } from "./server.js";
|
|
14
|
+
import { createStateTools } from "./tools-state.js";
|
|
15
|
+
import { createCompileTools } from "./tools-compile.js";
|
|
16
|
+
import { createEnforceTools } from "./tools-enforce.js";
|
|
17
|
+
// Parse args
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
let projectDir = process.cwd();
|
|
20
|
+
const dirIdx = args.indexOf("--project-dir");
|
|
21
|
+
if (dirIdx !== -1 && args[dirIdx + 1]) {
|
|
22
|
+
projectDir = args[dirIdx + 1];
|
|
23
|
+
}
|
|
24
|
+
// Resolve version from package.json (best-effort)
|
|
25
|
+
let version = "1.5.3";
|
|
26
|
+
try {
|
|
27
|
+
const { readFileSync } = await import("node:fs");
|
|
28
|
+
const { resolve } = await import("node:path");
|
|
29
|
+
const { fileURLToPath } = await import("node:url");
|
|
30
|
+
const pkgPath = resolve(fileURLToPath(import.meta.url), "..", "..", "..", "package.json");
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
32
|
+
version = pkg.version ?? version;
|
|
33
|
+
}
|
|
34
|
+
catch { /* use default */ }
|
|
35
|
+
const tools = [
|
|
36
|
+
...createStateTools(projectDir),
|
|
37
|
+
...createCompileTools(projectDir),
|
|
38
|
+
...createEnforceTools(projectDir),
|
|
39
|
+
];
|
|
40
|
+
createMCPServer({ name: "intentdna", version }, tools);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Server Framework
|
|
3
|
+
*
|
|
4
|
+
* Minimal MCP server supporting:
|
|
5
|
+
* - initialize / initialized handshake
|
|
6
|
+
* - tools/list — list available tools
|
|
7
|
+
* - tools/call — execute a tool
|
|
8
|
+
* - ping — health check
|
|
9
|
+
*
|
|
10
|
+
* Zero external dependencies. Speaks MCP protocol over stdio.
|
|
11
|
+
*/
|
|
12
|
+
export interface ToolDef {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: "object";
|
|
17
|
+
properties: Record<string, unknown>;
|
|
18
|
+
required?: string[];
|
|
19
|
+
};
|
|
20
|
+
handler: (args: Record<string, unknown>) => Promise<ToolResult>;
|
|
21
|
+
}
|
|
22
|
+
export interface ToolResult {
|
|
23
|
+
content: Array<{
|
|
24
|
+
type: "text";
|
|
25
|
+
text: string;
|
|
26
|
+
}>;
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function textResult(text: string): ToolResult;
|
|
30
|
+
export declare function errorResult(text: string): ToolResult;
|
|
31
|
+
export interface ServerInfo {
|
|
32
|
+
name: string;
|
|
33
|
+
version: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create and start an MCP server with the given tools.
|
|
37
|
+
* Blocks on stdin — call this as the last thing in your entry point.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createMCPServer(info: ServerInfo, tools: ToolDef[]): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Server Framework
|
|
3
|
+
*
|
|
4
|
+
* Minimal MCP server supporting:
|
|
5
|
+
* - initialize / initialized handshake
|
|
6
|
+
* - tools/list — list available tools
|
|
7
|
+
* - tools/call — execute a tool
|
|
8
|
+
* - ping — health check
|
|
9
|
+
*
|
|
10
|
+
* Zero external dependencies. Speaks MCP protocol over stdio.
|
|
11
|
+
*/
|
|
12
|
+
import { startTransport, sendResult, sendError } from "./transport.js";
|
|
13
|
+
// ── Result Helpers ───────────────────────────────────────
|
|
14
|
+
export function textResult(text) {
|
|
15
|
+
return { content: [{ type: "text", text }] };
|
|
16
|
+
}
|
|
17
|
+
export function errorResult(text) {
|
|
18
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create and start an MCP server with the given tools.
|
|
22
|
+
* Blocks on stdin — call this as the last thing in your entry point.
|
|
23
|
+
*/
|
|
24
|
+
export function createMCPServer(info, tools) {
|
|
25
|
+
const toolMap = new Map();
|
|
26
|
+
for (const tool of tools) {
|
|
27
|
+
toolMap.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
startTransport(async (msg) => {
|
|
30
|
+
switch (msg.method) {
|
|
31
|
+
case "initialize":
|
|
32
|
+
sendResult(msg.id ?? null, {
|
|
33
|
+
protocolVersion: "2024-11-05",
|
|
34
|
+
capabilities: { tools: {} },
|
|
35
|
+
serverInfo: { name: info.name, version: info.version },
|
|
36
|
+
});
|
|
37
|
+
break;
|
|
38
|
+
case "notifications/initialized":
|
|
39
|
+
// Client acknowledgement — no response needed
|
|
40
|
+
break;
|
|
41
|
+
case "ping":
|
|
42
|
+
sendResult(msg.id ?? null, {});
|
|
43
|
+
break;
|
|
44
|
+
case "tools/list":
|
|
45
|
+
sendResult(msg.id ?? null, {
|
|
46
|
+
tools: tools.map(t => ({
|
|
47
|
+
name: t.name,
|
|
48
|
+
description: t.description,
|
|
49
|
+
inputSchema: t.inputSchema,
|
|
50
|
+
})),
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
case "tools/call": {
|
|
54
|
+
const params = msg.params ?? {};
|
|
55
|
+
const toolName = params.name;
|
|
56
|
+
const args = (params.arguments ?? {});
|
|
57
|
+
const tool = toolMap.get(toolName);
|
|
58
|
+
if (!tool) {
|
|
59
|
+
sendResult(msg.id ?? null, {
|
|
60
|
+
content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
|
|
61
|
+
isError: true,
|
|
62
|
+
});
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const result = await tool.handler(args);
|
|
67
|
+
sendResult(msg.id ?? null, result);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
sendResult(msg.id ?? null, {
|
|
71
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
72
|
+
isError: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
default:
|
|
78
|
+
if (msg.id !== undefined) {
|
|
79
|
+
sendError(msg.id, -32601, `Method not found: ${msg.method}`);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Compile Tools (G3)
|
|
3
|
+
*
|
|
4
|
+
* Tools for compiling and managing DNA via MCP:
|
|
5
|
+
* - dna_compile: Compile DNA config to Constraint IR
|
|
6
|
+
* - dna_validate: Validate a DNA config file
|
|
7
|
+
* - dna_sync: Run full sync pipeline (compile + inject + hooks)
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolDef } from "./server.js";
|
|
10
|
+
export declare function createCompileTools(projectDir: string): ToolDef[];
|