sysprom 1.6.0 → 1.7.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.
@@ -12,9 +12,13 @@ export declare const RELATIONSHIP_ENDPOINT_TYPES: Record<RelationshipType, {
12
12
  }>;
13
13
  /**
14
14
  * Check if a relationship type is valid for the given endpoint node types.
15
- * @param relType The relationship type
16
- * @param fromType The source node type
17
- * @param toType The target node type
15
+ * @param relType - The relationship type
16
+ * @param fromType - The source node type
17
+ * @param toType - The target node type
18
18
  * @returns true if the endpoint types are valid for this relationship
19
+ * @example
20
+ * ```ts
21
+ * isValidEndpointPair("refines", "intent", "concept") // true
22
+ * ```
19
23
  */
20
24
  export declare function isValidEndpointPair(relType: RelationshipType, fromType: NodeType, toType: NodeType): boolean;
@@ -288,16 +288,16 @@ export const RELATIONSHIP_ENDPOINT_TYPES = {
288
288
  };
289
289
  /**
290
290
  * Check if a relationship type is valid for the given endpoint node types.
291
- * @param relType The relationship type
292
- * @param fromType The source node type
293
- * @param toType The target node type
291
+ * @param relType - The relationship type
292
+ * @param fromType - The source node type
293
+ * @param toType - The target node type
294
294
  * @returns true if the endpoint types are valid for this relationship
295
+ * @example
296
+ * ```ts
297
+ * isValidEndpointPair("refines", "intent", "concept") // true
298
+ * ```
295
299
  */
296
300
  export function isValidEndpointPair(relType, fromType, toType) {
297
301
  const endpoints = RELATIONSHIP_ENDPOINT_TYPES[relType];
298
- if (!endpoints) {
299
- // Unknown relationship type — should be caught by schema validation
300
- return false;
301
- }
302
- return (endpoints.from.includes(fromType) && endpoints.to.includes(toType));
302
+ return endpoints.from.includes(fromType) && endpoints.to.includes(toType);
303
303
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./server.js";
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // @fileoverview MCP server entry point - runs the SysProM MCP server
3
+ import "./server.js";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import * as z from "zod";
5
+ import { loadDocument } from "../io.js";
6
+ import { RelationshipType } from "../schema.js";
7
+ import { validateOp, statsOp, queryNodesOp, queryNodeOp, queryRelationshipsOp, traceFromNodeOp, addNodeOp, removeNodeOp, updateNodeOp, addRelationshipOp, removeRelationshipOp, nextIdOp, } from "../operations/index.js";
8
+ // Create MCP server instance
9
+ const server = new McpServer({
10
+ name: "sysprom-mcp",
11
+ version: "1.0.0",
12
+ });
13
+ // Register validate tool
14
+ server.registerTool("validate", {
15
+ description: "Validate a SysProM document and return any validation issues",
16
+ inputSchema: z.object({
17
+ path: z.string().describe("Path to SysProM file"),
18
+ }),
19
+ }, ({ path }) => {
20
+ const { doc } = loadDocument(path);
21
+ const result = validateOp({ doc });
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: JSON.stringify(result, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ });
31
+ // Register stats tool
32
+ server.registerTool("stats", {
33
+ description: "Get statistics about a SysProM document",
34
+ inputSchema: z.object({
35
+ path: z.string().describe("Path to SysProM file"),
36
+ }),
37
+ }, ({ path }) => {
38
+ const { doc } = loadDocument(path);
39
+ const result = statsOp({ doc });
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: JSON.stringify(result, null, 2),
45
+ },
46
+ ],
47
+ };
48
+ });
49
+ // Register query-nodes tool
50
+ server.registerTool("query-nodes", {
51
+ description: "Query nodes by type, status, or other criteria",
52
+ inputSchema: z.object({
53
+ path: z.string().describe("Path to SysProM file"),
54
+ type: z.string().optional().describe("Filter by node type"),
55
+ status: z.string().optional().describe("Filter by node status"),
56
+ }),
57
+ }, ({ path, type, status }) => {
58
+ const { doc } = loadDocument(path);
59
+ const results = queryNodesOp({
60
+ doc,
61
+ type,
62
+ status,
63
+ });
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: JSON.stringify(results, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ });
73
+ // Register query-node tool
74
+ server.registerTool("query-node", {
75
+ description: "Get a specific node by ID",
76
+ inputSchema: z.object({
77
+ path: z.string().describe("Path to SysProM file"),
78
+ id: z.string().describe("Node ID"),
79
+ }),
80
+ }, ({ path, id }) => {
81
+ const { doc } = loadDocument(path);
82
+ const result = queryNodeOp({ doc, id });
83
+ return {
84
+ content: [
85
+ {
86
+ type: "text",
87
+ text: JSON.stringify(result, null, 2),
88
+ },
89
+ ],
90
+ };
91
+ });
92
+ // Register query-relationships tool
93
+ server.registerTool("query-relationships", {
94
+ description: "Query relationships by source, target, or type",
95
+ inputSchema: z.object({
96
+ path: z.string().describe("Path to SysProM file"),
97
+ from: z.string().optional().describe("Filter by source node ID"),
98
+ to: z.string().optional().describe("Filter by target node ID"),
99
+ type: z.string().optional().describe("Filter by relationship type"),
100
+ }),
101
+ }, ({ path, from, to, type }) => {
102
+ const { doc } = loadDocument(path);
103
+ const results = queryRelationshipsOp({
104
+ doc,
105
+ from,
106
+ to,
107
+ type,
108
+ });
109
+ return {
110
+ content: [
111
+ {
112
+ type: "text",
113
+ text: JSON.stringify(results, null, 2),
114
+ },
115
+ ],
116
+ };
117
+ });
118
+ // Register trace tool
119
+ server.registerTool("trace", {
120
+ description: "Trace impacts from a node through the graph",
121
+ inputSchema: z.object({
122
+ path: z.string().describe("Path to SysProM file"),
123
+ from: z.string().describe("Starting node ID"),
124
+ }),
125
+ }, ({ path, from }) => {
126
+ const { doc } = loadDocument(path);
127
+ const result = traceFromNodeOp({ doc, startId: from });
128
+ return {
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: JSON.stringify(result, null, 2),
133
+ },
134
+ ],
135
+ };
136
+ });
137
+ // Register add-node tool
138
+ server.registerTool("add-node", {
139
+ description: "Add a new node to the SysProM document",
140
+ inputSchema: z.object({
141
+ path: z.string().describe("Path to SysProM file"),
142
+ type: z.string().describe("Node type"),
143
+ id: z.string().optional().describe("Node ID (auto-generated if omitted)"),
144
+ name: z.string().describe("Node name"),
145
+ description: z.string().optional().describe("Node description"),
146
+ }),
147
+ }, ({ path, type, id, name, description }) => {
148
+ const { doc } = loadDocument(path);
149
+ const nodeType = z
150
+ .enum([
151
+ "intent",
152
+ "concept",
153
+ "capability",
154
+ "element",
155
+ "realisation",
156
+ "invariant",
157
+ "principle",
158
+ "policy",
159
+ "protocol",
160
+ "stage",
161
+ "role",
162
+ "gate",
163
+ "mode",
164
+ "artefact",
165
+ "artefact_flow",
166
+ "decision",
167
+ "change",
168
+ "view",
169
+ "version",
170
+ ])
171
+ .safeParse(type);
172
+ if (!nodeType.success) {
173
+ throw new Error(`Invalid node type: ${type}`);
174
+ }
175
+ const nodeId = id ?? nextIdOp({ doc, type: nodeType.data });
176
+ const updated = addNodeOp({
177
+ doc,
178
+ node: {
179
+ id: nodeId,
180
+ type: nodeType.data,
181
+ name,
182
+ ...(description && { description }),
183
+ },
184
+ });
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: JSON.stringify({
190
+ message: "Node added",
191
+ id: nodeId,
192
+ nodeCount: updated.nodes.length,
193
+ }, null, 2),
194
+ },
195
+ ],
196
+ };
197
+ });
198
+ // Register remove-node tool
199
+ server.registerTool("remove-node", {
200
+ description: "Remove a node from the SysProM document",
201
+ inputSchema: z.object({
202
+ path: z.string().describe("Path to SysProM file"),
203
+ id: z.string().describe("Node ID"),
204
+ }),
205
+ }, ({ path, id }) => {
206
+ const { doc } = loadDocument(path);
207
+ const result = removeNodeOp({ doc, id });
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: JSON.stringify({
213
+ message: `Node ${id} removed`,
214
+ nodeCount: result.doc.nodes.length,
215
+ warnings: result.warnings,
216
+ }, null, 2),
217
+ },
218
+ ],
219
+ };
220
+ });
221
+ // Register update-node tool
222
+ server.registerTool("update-node", {
223
+ description: "Update node properties",
224
+ inputSchema: z.object({
225
+ path: z.string().describe("Path to SysProM file"),
226
+ id: z.string().describe("Node ID"),
227
+ fields: z.record(z.string(), z.unknown()).describe("Fields to update"),
228
+ }),
229
+ }, ({ path, id, fields }) => {
230
+ const { doc } = loadDocument(path);
231
+ // Validate fields are valid node property updates
232
+ const validFields = Object.entries(fields).reduce((acc, [key, value]) => {
233
+ // Allow common node fields; unknown fields are silently ignored
234
+ if ([
235
+ "name",
236
+ "description",
237
+ "status",
238
+ "context",
239
+ "options",
240
+ "selected",
241
+ "rationale",
242
+ "scope",
243
+ "operations",
244
+ "plan",
245
+ "propagation",
246
+ "includes",
247
+ "input",
248
+ "output",
249
+ "external_references",
250
+ ].includes(key)) {
251
+ acc[key] = value;
252
+ }
253
+ return acc;
254
+ }, {});
255
+ const updated = updateNodeOp({
256
+ doc,
257
+ id,
258
+ fields: validFields,
259
+ });
260
+ const node = updated.nodes.find((n) => n.id === id);
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: JSON.stringify({ message: "Node updated", node }, null, 2),
266
+ },
267
+ ],
268
+ };
269
+ });
270
+ // Register add-relationship tool
271
+ server.registerTool("add-relationship", {
272
+ description: "Add a relationship between two nodes",
273
+ inputSchema: z.object({
274
+ path: z.string().describe("Path to SysProM file"),
275
+ from: z.string().describe("Source node ID"),
276
+ to: z.string().describe("Target node ID"),
277
+ type: z.string().describe("Relationship type"),
278
+ }),
279
+ }, ({ path, from, to, type }) => {
280
+ const { doc } = loadDocument(path);
281
+ const relType = RelationshipType.safeParse(type);
282
+ if (!relType.success) {
283
+ throw new Error(`Invalid relationship type: ${type}`);
284
+ }
285
+ const updated = addRelationshipOp({
286
+ doc,
287
+ rel: {
288
+ from,
289
+ to,
290
+ type: relType.data,
291
+ },
292
+ });
293
+ return {
294
+ content: [
295
+ {
296
+ type: "text",
297
+ text: JSON.stringify({
298
+ message: "Relationship added",
299
+ relationshipCount: (updated.relationships ?? []).length,
300
+ }, null, 2),
301
+ },
302
+ ],
303
+ };
304
+ });
305
+ // Register remove-relationship tool
306
+ server.registerTool("remove-relationship", {
307
+ description: "Remove a relationship between two nodes",
308
+ inputSchema: z.object({
309
+ path: z.string().describe("Path to SysProM file"),
310
+ from: z.string().describe("Source node ID"),
311
+ to: z.string().describe("Target node ID"),
312
+ type: z.string().describe("Relationship type"),
313
+ }),
314
+ }, ({ path, from, to, type }) => {
315
+ const { doc } = loadDocument(path);
316
+ const relType = RelationshipType.safeParse(type);
317
+ if (!relType.success) {
318
+ throw new Error(`Invalid relationship type: ${type}`);
319
+ }
320
+ const result = removeRelationshipOp({
321
+ doc,
322
+ from,
323
+ to,
324
+ type: relType.data,
325
+ });
326
+ return {
327
+ content: [
328
+ {
329
+ type: "text",
330
+ text: JSON.stringify({
331
+ message: "Relationship removed",
332
+ relationshipCount: (result.doc.relationships ?? []).length,
333
+ }, null, 2),
334
+ },
335
+ ],
336
+ };
337
+ });
338
+ // Start server
339
+ async function main() {
340
+ const transport = new StdioServerTransport();
341
+ await server.connect(transport);
342
+ console.error("SysProM MCP server running...");
343
+ }
344
+ main().catch((error) => {
345
+ const message = error instanceof Error ? error.message : String(error);
346
+ console.error("Server error:", message);
347
+ process.exit(1);
348
+ });
@@ -35,7 +35,15 @@ function parseText(raw) {
35
35
  const lines = raw.split("\n");
36
36
  return lines.length === 1 ? lines[0] : lines;
37
37
  }
38
- /** Separate $schema from front matter so it becomes a top-level document key. */
38
+ /**
39
+ * Separate $schema from front matter so it becomes a top-level document key.
40
+ * @param front - The front matter object
41
+ * @returns An object with extracted schema and remaining metadata
42
+ * @example
43
+ * ```ts
44
+ * const { schema, metadata } = extractSchema({ $schema: "...", foo: "bar" });
45
+ * ```
46
+ */
39
47
  function extractSchema(front) {
40
48
  const schema = typeof front.$schema === "string" ? front.$schema : undefined;
41
49
  const metadata = { ...front };
@@ -12,15 +12,18 @@ export interface DetectionResult {
12
12
  /**
13
13
  * Detect whether JSON and/or Markdown have changed.
14
14
  * Strategy:
15
- * 1. Parse both JSON and Markdown to document objects
16
- * 2. If documents are identical no change
17
- * 3. If documents differ:
18
- * - Use file modification times to determine which was edited more recently
19
- * - The newer file is considered the "changed" one
20
- * - If modification times are very close (< 100ms), treat as conflict
21
- *
22
- * @param jsonPath Path to JSON file
23
- * @param mdPath Path to Markdown file (single or multi-doc)
15
+ * 1. Parse both JSON and Markdown to document objects.
16
+ * 2. If documents are identical, no change.
17
+ * 3. If documents differ, use file modification times to determine which was
18
+ * edited more recently. The newer file is considered the "changed" one.
19
+ * If modification times are very close (< 100ms), treat as conflict.
20
+ * @param jsonPath - Path to JSON file
21
+ * @param mdPath - Path to Markdown file (single or multi-doc)
24
22
  * @returns Detection result with jsonChanged, mdChanged, and conflict flags
23
+ * @example
24
+ * ```ts
25
+ * const result = detectChanges("doc.spm.json", "doc.spm.md");
26
+ * if (result.conflict) throw new Error("Both files changed");
27
+ * ```
25
28
  */
26
29
  export declare function detectChanges(jsonPath: string, mdPath: string): DetectionResult;
package/dist/src/sync.js CHANGED
@@ -5,31 +5,38 @@ import { SysProMDocument } from "./schema.js";
5
5
  /**
6
6
  * Compute a normalised hash of a document for comparison.
7
7
  * Uses canonical JSON representation.
8
- * @param doc The SysProM document
8
+ * @param doc - The SysProM document
9
9
  * @returns SHA256 hash of the canonicalised document
10
+ * @example
11
+ * ```ts
12
+ * const hash = normaliseHash({ nodes: [], relationships: [] });
13
+ * ```
10
14
  */
11
15
  function normaliseHash(doc) {
12
- const sorted = JSON.stringify(doc, Object.keys(doc).sort());
16
+ const keys = doc && typeof doc === "object" ? Object.keys(doc).sort() : [];
17
+ const sorted = JSON.stringify(doc, keys);
13
18
  return createHash("sha256").update(sorted).digest("hex");
14
19
  }
15
20
  /**
16
21
  * Detect whether JSON and/or Markdown have changed.
17
22
  * Strategy:
18
- * 1. Parse both JSON and Markdown to document objects
19
- * 2. If documents are identical no change
20
- * 3. If documents differ:
21
- * - Use file modification times to determine which was edited more recently
22
- * - The newer file is considered the "changed" one
23
- * - If modification times are very close (< 100ms), treat as conflict
24
- *
25
- * @param jsonPath Path to JSON file
26
- * @param mdPath Path to Markdown file (single or multi-doc)
23
+ * 1. Parse both JSON and Markdown to document objects.
24
+ * 2. If documents are identical, no change.
25
+ * 3. If documents differ, use file modification times to determine which was
26
+ * edited more recently. The newer file is considered the "changed" one.
27
+ * If modification times are very close (< 100ms), treat as conflict.
28
+ * @param jsonPath - Path to JSON file
29
+ * @param mdPath - Path to Markdown file (single or multi-doc)
27
30
  * @returns Detection result with jsonChanged, mdChanged, and conflict flags
31
+ * @example
32
+ * ```ts
33
+ * const result = detectChanges("doc.spm.json", "doc.spm.md");
34
+ * if (result.conflict) throw new Error("Both files changed");
35
+ * ```
28
36
  */
29
37
  export function detectChanges(jsonPath, mdPath) {
30
38
  // Read files
31
39
  const jsonContent = readFileSync(jsonPath, "utf8");
32
- const mdContent = readFileSync(mdPath, "utf8");
33
40
  // Parse JSON
34
41
  const jsonDoc = JSON.parse(jsonContent);
35
42
  if (!SysProMDocument.is(jsonDoc)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sysprom",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "SysProM — System Provenance Model CLI and library",
5
5
  "author": "ExaDev",
6
6
  "homepage": "https://exadev.github.io/SysProM",
@@ -35,7 +35,8 @@
35
35
  ],
36
36
  "bin": {
37
37
  "sysprom": "dist/src/cli/index.js",
38
- "spm": "dist/src/cli/index.js"
38
+ "spm": "dist/src/cli/index.js",
39
+ "sysprom-mcp": "dist/src/mcp/index.js"
39
40
  },
40
41
  "scripts": {
41
42
  "build": "turbo run _build",
@@ -59,6 +60,7 @@
59
60
  "prepare": "husky"
60
61
  },
61
62
  "dependencies": {
63
+ "@modelcontextprotocol/sdk": "1.27.1",
62
64
  "commander": "14.0.3",
63
65
  "picocolors": "1.1.1",
64
66
  "zod": "4.3.6"
@@ -66,6 +68,7 @@
66
68
  "devDependencies": {
67
69
  "@commitlint/cli": "20.5.0",
68
70
  "@commitlint/config-conventional": "20.5.0",
71
+ "@eslint-community/eslint-plugin-eslint-comments": "4.7.1",
69
72
  "@eslint/js": "10.0.1",
70
73
  "@semantic-release/changelog": "6.0.3",
71
74
  "@semantic-release/git": "10.0.1",