intentdna 1.5.2 → 1.5.3
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.js +27 -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 +1 -0
- 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 +38 -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-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 +5 -5
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
// Parse args
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
let projectDir = process.cwd();
|
|
19
|
+
const dirIdx = args.indexOf("--project-dir");
|
|
20
|
+
if (dirIdx !== -1 && args[dirIdx + 1]) {
|
|
21
|
+
projectDir = args[dirIdx + 1];
|
|
22
|
+
}
|
|
23
|
+
// Resolve version from package.json (best-effort)
|
|
24
|
+
let version = "1.5.3";
|
|
25
|
+
try {
|
|
26
|
+
const { readFileSync } = await import("node:fs");
|
|
27
|
+
const { resolve } = await import("node:path");
|
|
28
|
+
const { fileURLToPath } = await import("node:url");
|
|
29
|
+
const pkgPath = resolve(fileURLToPath(import.meta.url), "..", "..", "..", "package.json");
|
|
30
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
31
|
+
version = pkg.version ?? version;
|
|
32
|
+
}
|
|
33
|
+
catch { /* use default */ }
|
|
34
|
+
const tools = [
|
|
35
|
+
...createStateTools(projectDir),
|
|
36
|
+
...createCompileTools(projectDir),
|
|
37
|
+
];
|
|
38
|
+
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[];
|
|
@@ -0,0 +1,214 @@
|
|
|
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 { resolve } from "node:path";
|
|
10
|
+
import { stat } from "node:fs/promises";
|
|
11
|
+
import { loadDNA, compileFromFiles } from "../compiler/index.js";
|
|
12
|
+
import { textResult, errorResult } from "./server.js";
|
|
13
|
+
async function fileExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await stat(path);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Auto-detect DNA config files in the project directory. */
|
|
23
|
+
async function detectConfigs(projectDir) {
|
|
24
|
+
const { readdir } = await import("node:fs/promises");
|
|
25
|
+
// Multi-config: .dna/configs/*.yaml
|
|
26
|
+
const configsDir = resolve(projectDir, ".dna", "configs");
|
|
27
|
+
try {
|
|
28
|
+
const files = await readdir(configsDir);
|
|
29
|
+
const yamls = files
|
|
30
|
+
.filter(f => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
31
|
+
.sort()
|
|
32
|
+
.map(f => resolve(configsDir, f));
|
|
33
|
+
if (yamls.length > 0)
|
|
34
|
+
return yamls;
|
|
35
|
+
}
|
|
36
|
+
catch { /* dir doesn't exist */ }
|
|
37
|
+
// Legacy single-config
|
|
38
|
+
const candidates = [
|
|
39
|
+
".dna/config.yaml", ".dna/config.yml", ".dna/config.json",
|
|
40
|
+
".dna.yaml", ".dna.yml", ".dna.json",
|
|
41
|
+
];
|
|
42
|
+
for (const name of candidates) {
|
|
43
|
+
const path = resolve(projectDir, name);
|
|
44
|
+
if (await fileExists(path))
|
|
45
|
+
return [path];
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
export function createCompileTools(projectDir) {
|
|
50
|
+
return [
|
|
51
|
+
// ── dna_validate ─────────────────────────────────────
|
|
52
|
+
{
|
|
53
|
+
name: "dna_validate",
|
|
54
|
+
description: "Validate a DNA config file for schema errors without compiling",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
config_path: { type: "string", description: "Path to DNA config (auto-detected if omitted)" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
handler: async (args) => {
|
|
62
|
+
const configPath = typeof args.config_path === "string"
|
|
63
|
+
? resolve(projectDir, args.config_path)
|
|
64
|
+
: undefined;
|
|
65
|
+
const files = configPath ? [configPath] : await detectConfigs(projectDir);
|
|
66
|
+
if (files.length === 0)
|
|
67
|
+
return errorResult("No DNA config files found.");
|
|
68
|
+
const results = [];
|
|
69
|
+
let allValid = true;
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
try {
|
|
72
|
+
// loadDNA calls validateDNA internally — throws on invalid
|
|
73
|
+
await loadDNA(file);
|
|
74
|
+
results.push(`${file}: valid`);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
allValid = false;
|
|
78
|
+
results.push(`${file}: INVALID — ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
results.unshift(allValid ? "All configs valid." : "Validation errors found.");
|
|
82
|
+
return textResult(results.join("\n"));
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
// ── dna_compile ──────────────────────────────────────
|
|
86
|
+
{
|
|
87
|
+
name: "dna_compile",
|
|
88
|
+
description: "Compile DNA config(s) to Constraint IR and return a summary of the compiled constraints",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
config_path: { type: "string", description: "Path to DNA config (auto-detected if omitted)" },
|
|
93
|
+
context: { type: "string", description: "Active context name (e.g., 'work', 'personal')" },
|
|
94
|
+
role: { type: "string", description: "Active role name" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
handler: async (args) => {
|
|
98
|
+
const configPath = typeof args.config_path === "string"
|
|
99
|
+
? resolve(projectDir, args.config_path)
|
|
100
|
+
: undefined;
|
|
101
|
+
const files = configPath ? [configPath] : await detectConfigs(projectDir);
|
|
102
|
+
if (files.length === 0)
|
|
103
|
+
return errorResult("No DNA config files found.");
|
|
104
|
+
try {
|
|
105
|
+
const ir = await compileFromFiles(files, {
|
|
106
|
+
context: typeof args.context === "string" ? args.context : undefined,
|
|
107
|
+
role: typeof args.role === "string" ? args.role : undefined,
|
|
108
|
+
});
|
|
109
|
+
const lines = [
|
|
110
|
+
`Compiled from: ${ir.source_dna_ids.join(", ")}`,
|
|
111
|
+
` Directives: ${ir.prompt_directives.length}`,
|
|
112
|
+
` Gates: ${ir.pre_execution_gates.length}`,
|
|
113
|
+
` Filters: ${ir.tool_filters.length}`,
|
|
114
|
+
` Validators: ${ir.post_execution_validators.length}`,
|
|
115
|
+
];
|
|
116
|
+
if (ir.roles_scope_map?.length) {
|
|
117
|
+
lines.push(` Roles: ${ir.roles_scope_map.map(r => r.role_name).join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
if (ir.workflows_ir?.length) {
|
|
120
|
+
lines.push(` Workflows: ${ir.workflows_ir.map(w => w.workflow_name).join(", ")}`);
|
|
121
|
+
for (const wf of ir.workflows_ir) {
|
|
122
|
+
lines.push(` ${wf.workflow_name}: ${wf.active_roles.length} roles, ${wf.step_checkpoints.length} checkpoints`);
|
|
123
|
+
if (wf.step_enforce_rules?.length) {
|
|
124
|
+
lines.push(` Enforce rules: ${wf.step_enforce_rules.length}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return textResult(lines.join("\n"));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return errorResult(`Compilation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
// ── dna_sync ─────────────────────────────────────────
|
|
136
|
+
{
|
|
137
|
+
name: "dna_sync",
|
|
138
|
+
description: "Run full DNA sync pipeline: compile + inject CLAUDE.md + generate agents/skills/hooks. Equivalent to CLI `dna sync`.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
evolve: { type: "boolean", description: "Run evolution engine before compiling (default false)" },
|
|
143
|
+
context: { type: "string", description: "Active context name" },
|
|
144
|
+
role: { type: "string", description: "Active role name" },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
handler: async (args) => {
|
|
148
|
+
// Import sync command dynamically to avoid circular deps
|
|
149
|
+
const { runSync, autoDetectConfigs: detectAll } = await import("../cli/commands/sync.js");
|
|
150
|
+
const files = await detectAll();
|
|
151
|
+
if (files.length === 0)
|
|
152
|
+
return errorResult("No DNA config files found.");
|
|
153
|
+
const exitCode = await runSync({
|
|
154
|
+
files,
|
|
155
|
+
target: "claude-md",
|
|
156
|
+
inject: resolve(projectDir, "CLAUDE.md"),
|
|
157
|
+
evolve: typeof args.evolve === "boolean" ? args.evolve : false,
|
|
158
|
+
context: typeof args.context === "string" ? args.context : undefined,
|
|
159
|
+
agentsDir: resolve(projectDir, ".claude", "agents"),
|
|
160
|
+
skillsDir: resolve(projectDir, ".claude", "skills"),
|
|
161
|
+
settingsPath: resolve(projectDir, ".claude", "settings.json"),
|
|
162
|
+
remove: false,
|
|
163
|
+
});
|
|
164
|
+
if (exitCode === 0) {
|
|
165
|
+
return textResult("Sync completed successfully.");
|
|
166
|
+
}
|
|
167
|
+
return errorResult(`Sync failed with exit code ${exitCode}.`);
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
// ── dna_generate ─────────────────────────────────────
|
|
171
|
+
{
|
|
172
|
+
name: "dna_generate",
|
|
173
|
+
description: "Generate a DNA config from a natural-language description. Lists matching templates or provides guidance for custom configs.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
description: { type: "string", description: "Natural-language description of the desired DNA config" },
|
|
178
|
+
},
|
|
179
|
+
required: ["description"],
|
|
180
|
+
},
|
|
181
|
+
handler: async (args) => {
|
|
182
|
+
const description = String(args.description);
|
|
183
|
+
// List available templates
|
|
184
|
+
const { readdir } = await import("node:fs/promises");
|
|
185
|
+
const { resolve: resolvePath, dirname } = await import("node:path");
|
|
186
|
+
const { fileURLToPath } = await import("node:url");
|
|
187
|
+
let templates = [];
|
|
188
|
+
try {
|
|
189
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
190
|
+
const templatesDir = resolvePath(thisDir, "..", "templates");
|
|
191
|
+
const files = await readdir(templatesDir);
|
|
192
|
+
templates = files.filter(f => f.endsWith(".yaml")).map(f => f.replace(".yaml", ""));
|
|
193
|
+
}
|
|
194
|
+
catch { /* templates dir not found */ }
|
|
195
|
+
const lines = [
|
|
196
|
+
`Generate DNA for: "${description}"`,
|
|
197
|
+
"",
|
|
198
|
+
"Options:",
|
|
199
|
+
` 1. Use a template: dna init --template <name>`,
|
|
200
|
+
` 2. Generate prompt: dna generate "${description}" --output .dna/prompt.md`,
|
|
201
|
+
` 3. Create manually: write .dna/config.yaml following the DNA schema`,
|
|
202
|
+
"",
|
|
203
|
+
];
|
|
204
|
+
if (templates.length > 0) {
|
|
205
|
+
lines.push(`Available templates (${templates.length}):`);
|
|
206
|
+
for (const t of templates) {
|
|
207
|
+
lines.push(` - ${t}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return textResult(lines.join("\n"));
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP State Management Tools (G2)
|
|
3
|
+
*
|
|
4
|
+
* Tools for reading/writing DNA governance state via MCP:
|
|
5
|
+
* - dna_status: Overview of DNA state
|
|
6
|
+
* - dna_workflow_read: Read current workflow state
|
|
7
|
+
* - dna_workflow_write: Update workflow state
|
|
8
|
+
* - dna_trace_query: Query trace data with filters
|
|
9
|
+
*/
|
|
10
|
+
import type { ToolDef } from "./server.js";
|
|
11
|
+
export declare function createStateTools(projectDir: string): ToolDef[];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP State Management Tools (G2)
|
|
3
|
+
*
|
|
4
|
+
* Tools for reading/writing DNA governance state via MCP:
|
|
5
|
+
* - dna_status: Overview of DNA state
|
|
6
|
+
* - dna_workflow_read: Read current workflow state
|
|
7
|
+
* - dna_workflow_write: Update workflow state
|
|
8
|
+
* - dna_trace_query: Query trace data with filters
|
|
9
|
+
*/
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { readWorkflowState, writeWorkflowState, readTraces, listSessions } from "../hooks/state.js";
|
|
12
|
+
import { loadCompiledIR } from "../runtime/plugin-adapter.js";
|
|
13
|
+
import { textResult } from "./server.js";
|
|
14
|
+
export function createStateTools(projectDir) {
|
|
15
|
+
return [
|
|
16
|
+
// ── dna_status ────────────────────────────────────────
|
|
17
|
+
{
|
|
18
|
+
name: "dna_status",
|
|
19
|
+
description: "Overview of DNA governance state: compiled IR info, active workflow, session list, and recent enforcement stats",
|
|
20
|
+
inputSchema: { type: "object", properties: {} },
|
|
21
|
+
handler: async () => {
|
|
22
|
+
const lines = [];
|
|
23
|
+
// IR info
|
|
24
|
+
const irPath = resolve(projectDir, ".dna", "compiled", "ir.json");
|
|
25
|
+
const compiled = await loadCompiledIR(irPath);
|
|
26
|
+
if (compiled) {
|
|
27
|
+
lines.push(`IR: v${compiled.ir_version}, compiled ${compiled.compiled_at}`);
|
|
28
|
+
lines.push(` Templates: ${compiled.source_dna_ids.join(", ")}`);
|
|
29
|
+
const ir = compiled.ir;
|
|
30
|
+
lines.push(` Directives: ${ir.prompt_directives.length}, Gates: ${ir.pre_execution_gates.length}, Filters: ${ir.tool_filters.length}`);
|
|
31
|
+
if (ir.roles_scope_map?.length) {
|
|
32
|
+
lines.push(` Roles: ${ir.roles_scope_map.map(r => r.role_name).join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (ir.workflows_ir?.length) {
|
|
35
|
+
lines.push(` Workflows: ${ir.workflows_ir.map(w => w.workflow_name).join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
lines.push("IR: not found (run `dna sync` to compile)");
|
|
40
|
+
}
|
|
41
|
+
// Active workflow
|
|
42
|
+
const wf = await readWorkflowState(projectDir);
|
|
43
|
+
if (wf) {
|
|
44
|
+
lines.push(`\nWorkflow: ${wf.workflow} (active)`);
|
|
45
|
+
lines.push(` Step: ${wf.current_step}, Role: ${wf.current_role}, Iteration: ${wf.iteration}`);
|
|
46
|
+
lines.push(` Started: ${wf.started_at}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
lines.push("\nWorkflow: none active");
|
|
50
|
+
}
|
|
51
|
+
// Sessions
|
|
52
|
+
const sessions = await listSessions(projectDir);
|
|
53
|
+
if (sessions.length > 0) {
|
|
54
|
+
const preview = sessions.slice(0, 5).join(", ");
|
|
55
|
+
lines.push(`\nSessions: ${sessions.length} (${preview}${sessions.length > 5 ? "..." : ""})`);
|
|
56
|
+
}
|
|
57
|
+
// Recent trace stats
|
|
58
|
+
const traces = await readTraces(projectDir, 1);
|
|
59
|
+
if (traces.length > 0) {
|
|
60
|
+
const blocks = traces.filter(t => t.decision === "block").length;
|
|
61
|
+
const warns = traces.filter(t => t.decision === "warn").length;
|
|
62
|
+
lines.push(`\nTrace (24h): ${traces.length} events, ${blocks} blocks, ${warns} warns`);
|
|
63
|
+
}
|
|
64
|
+
return textResult(lines.join("\n"));
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
// ── dna_workflow_read ─────────────────────────────────
|
|
68
|
+
{
|
|
69
|
+
name: "dna_workflow_read",
|
|
70
|
+
description: "Read current workflow state (step, role, iteration, artifacts)",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
session_id: { type: "string", description: "Session ID (reads root state if omitted)" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
handler: async (args) => {
|
|
78
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : undefined;
|
|
79
|
+
const wf = await readWorkflowState(projectDir, sessionId, 0);
|
|
80
|
+
if (!wf)
|
|
81
|
+
return textResult("No active workflow state found.");
|
|
82
|
+
return textResult(JSON.stringify(wf, null, 2));
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
// ── dna_workflow_write ────────────────────────────────
|
|
86
|
+
{
|
|
87
|
+
name: "dna_workflow_write",
|
|
88
|
+
description: "Update workflow state (set current step, role, iteration, or deactivate)",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
workflow: { type: "string", description: "Workflow name" },
|
|
93
|
+
current_step: { type: "string", description: "Current step ID" },
|
|
94
|
+
current_role: { type: "string", description: "Current role name" },
|
|
95
|
+
iteration: { type: "number", description: "Current iteration number" },
|
|
96
|
+
active: { type: "boolean", description: "Whether workflow is active (false to deactivate)" },
|
|
97
|
+
session_id: { type: "string", description: "Session ID for isolation" },
|
|
98
|
+
},
|
|
99
|
+
required: ["workflow", "current_step", "current_role"],
|
|
100
|
+
},
|
|
101
|
+
handler: async (args) => {
|
|
102
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : undefined;
|
|
103
|
+
const existing = await readWorkflowState(projectDir, sessionId, 0);
|
|
104
|
+
const state = {
|
|
105
|
+
active: typeof args.active === "boolean" ? args.active : true,
|
|
106
|
+
workflow: String(args.workflow),
|
|
107
|
+
current_step: String(args.current_step),
|
|
108
|
+
current_role: String(args.current_role),
|
|
109
|
+
iteration: typeof args.iteration === "number" ? args.iteration : (existing?.iteration ?? 1),
|
|
110
|
+
session_id: sessionId ?? existing?.session_id ?? "",
|
|
111
|
+
started_at: existing?.started_at ?? new Date().toISOString(),
|
|
112
|
+
completed_artifacts: existing?.completed_artifacts,
|
|
113
|
+
};
|
|
114
|
+
await writeWorkflowState(projectDir, state, sessionId);
|
|
115
|
+
return textResult(`Workflow state updated: ${state.workflow} step=${state.current_step} role=${state.current_role} iteration=${state.iteration}`);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
// ── dna_trace_query ──────────────────────────────────
|
|
119
|
+
{
|
|
120
|
+
name: "dna_trace_query",
|
|
121
|
+
description: "Query enforcement trace data with filters (blocks, warns, by tool, by path)",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
days: { type: "number", description: "Days to look back (default 1)" },
|
|
126
|
+
session_id: { type: "string", description: "Filter by session ID" },
|
|
127
|
+
decision: { type: "string", enum: ["block", "warn", "allow"], description: "Filter by decision type" },
|
|
128
|
+
tool_name: { type: "string", description: "Filter by tool name" },
|
|
129
|
+
limit: { type: "number", description: "Max entries to return (default 50)" },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
handler: async (args) => {
|
|
133
|
+
const days = typeof args.days === "number" ? args.days : 1;
|
|
134
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : undefined;
|
|
135
|
+
const decision = typeof args.decision === "string" ? args.decision : undefined;
|
|
136
|
+
const toolName = typeof args.tool_name === "string" ? args.tool_name : undefined;
|
|
137
|
+
const limit = typeof args.limit === "number" ? args.limit : 50;
|
|
138
|
+
let traces = await readTraces(projectDir, days, sessionId);
|
|
139
|
+
if (decision)
|
|
140
|
+
traces = traces.filter(t => t.decision === decision);
|
|
141
|
+
if (toolName)
|
|
142
|
+
traces = traces.filter(t => t.tool_name === toolName);
|
|
143
|
+
const total = traces.length;
|
|
144
|
+
traces = traces.slice(-limit);
|
|
145
|
+
const lines = [`Trace: ${total} matching entries (showing last ${traces.length})`];
|
|
146
|
+
for (const t of traces) {
|
|
147
|
+
const parts = [t.timestamp.slice(11, 19), t.event, t.decision];
|
|
148
|
+
if (t.tool_name)
|
|
149
|
+
parts.push(t.tool_name);
|
|
150
|
+
if (t.target_path)
|
|
151
|
+
parts.push(t.target_path);
|
|
152
|
+
if (t.reason)
|
|
153
|
+
parts.push(`"${t.reason.slice(0, 80)}"`);
|
|
154
|
+
lines.push(" " + parts.join(" | "));
|
|
155
|
+
}
|
|
156
|
+
return textResult(lines.join("\n"));
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Transport (stdio)
|
|
3
|
+
*
|
|
4
|
+
* Minimal JSON-RPC 2.0 over stdio transport for MCP protocol.
|
|
5
|
+
* Zero external dependencies — implements the protocol directly.
|
|
6
|
+
*
|
|
7
|
+
* Messages are newline-delimited JSON (one JSON object per line).
|
|
8
|
+
*/
|
|
9
|
+
export interface JsonRpcRequest {
|
|
10
|
+
jsonrpc: "2.0";
|
|
11
|
+
id?: number | string;
|
|
12
|
+
method: string;
|
|
13
|
+
params?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface JsonRpcResponse {
|
|
16
|
+
jsonrpc: "2.0";
|
|
17
|
+
id: number | string | null;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
error?: {
|
|
20
|
+
code: number;
|
|
21
|
+
message: string;
|
|
22
|
+
data?: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Send a JSON-RPC response to stdout. */
|
|
26
|
+
export declare function sendResponse(response: JsonRpcResponse): void;
|
|
27
|
+
/** Send a JSON-RPC success response. */
|
|
28
|
+
export declare function sendResult(id: number | string | null, result: unknown): void;
|
|
29
|
+
/** Send a JSON-RPC error response. */
|
|
30
|
+
export declare function sendError(id: number | string | null, code: number, message: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Start reading JSON-RPC messages from stdin.
|
|
33
|
+
* Calls handler for each valid message.
|
|
34
|
+
* Exits cleanly when stdin closes.
|
|
35
|
+
*/
|
|
36
|
+
export declare function startTransport(handler: (msg: JsonRpcRequest) => Promise<void>): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Transport (stdio)
|
|
3
|
+
*
|
|
4
|
+
* Minimal JSON-RPC 2.0 over stdio transport for MCP protocol.
|
|
5
|
+
* Zero external dependencies — implements the protocol directly.
|
|
6
|
+
*
|
|
7
|
+
* Messages are newline-delimited JSON (one JSON object per line).
|
|
8
|
+
*/
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
// ── Response Helpers ─────────────────────────────────────
|
|
11
|
+
/** Send a JSON-RPC response to stdout. */
|
|
12
|
+
export function sendResponse(response) {
|
|
13
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
14
|
+
}
|
|
15
|
+
/** Send a JSON-RPC success response. */
|
|
16
|
+
export function sendResult(id, result) {
|
|
17
|
+
sendResponse({ jsonrpc: "2.0", id, result });
|
|
18
|
+
}
|
|
19
|
+
/** Send a JSON-RPC error response. */
|
|
20
|
+
export function sendError(id, code, message) {
|
|
21
|
+
sendResponse({ jsonrpc: "2.0", id, error: { code, message } });
|
|
22
|
+
}
|
|
23
|
+
// ── Transport ────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Start reading JSON-RPC messages from stdin.
|
|
26
|
+
* Calls handler for each valid message.
|
|
27
|
+
* Exits cleanly when stdin closes.
|
|
28
|
+
*/
|
|
29
|
+
export function startTransport(handler) {
|
|
30
|
+
const rl = createInterface({ input: process.stdin });
|
|
31
|
+
rl.on("line", async (line) => {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
const msg = JSON.parse(trimmed);
|
|
37
|
+
if (msg.jsonrpc !== "2.0")
|
|
38
|
+
return;
|
|
39
|
+
await handler(msg);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Skip malformed messages — fail-open
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
rl.on("close", () => {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
});
|
|
48
|
+
}
|