sysprom 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/commands/sync.d.ts +18 -0
- package/dist/src/cli/commands/sync.js +112 -0
- package/dist/src/cli/program.js +2 -0
- package/dist/src/json-to-md.js +2 -0
- package/dist/src/mcp/index.d.ts +2 -0
- package/dist/src/mcp/index.js +3 -0
- package/dist/src/mcp/server.d.ts +2 -0
- package/dist/src/mcp/server.js +348 -0
- package/dist/src/md-to-json.js +32 -8
- package/package.json +5 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CommandDef } from "../define-command.js";
|
|
2
|
+
import { type BidirectionalSyncResult, type ConflictStrategy } from "../../operations/index.js";
|
|
3
|
+
interface SyncCommandInput {
|
|
4
|
+
jsonPath: string;
|
|
5
|
+
mdPath: string;
|
|
6
|
+
strategy?: ConflictStrategy;
|
|
7
|
+
dryRun?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Synchronise JSON and Markdown representations of a SysProM document.
|
|
11
|
+
* @param input - Configuration for sync operation
|
|
12
|
+
* @returns Result of the synchronisation
|
|
13
|
+
* @example
|
|
14
|
+
* const result = syncCommand({ jsonPath: "doc.spm.json", mdPath: "doc.spm.md" });
|
|
15
|
+
*/
|
|
16
|
+
export declare function syncCommand(input: SyncCommandInput): BidirectionalSyncResult;
|
|
17
|
+
export declare const syncCommandDef: CommandDef;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { syncDocumentsOp, } from "../../operations/index.js";
|
|
5
|
+
import { detectChanges } from "../../sync.js";
|
|
6
|
+
import { markdownToJson } from "../../md-to-json.js";
|
|
7
|
+
import { jsonToMarkdownSingle } from "../../json-to-md.js";
|
|
8
|
+
import { canonicalise } from "../../canonical-json.js";
|
|
9
|
+
import { SysProMDocument } from "../../schema.js";
|
|
10
|
+
/**
|
|
11
|
+
* Synchronise JSON and Markdown representations of a SysProM document.
|
|
12
|
+
* @param input - Configuration for sync operation
|
|
13
|
+
* @returns Result of the synchronisation
|
|
14
|
+
* @example
|
|
15
|
+
* const result = syncCommand({ jsonPath: "doc.spm.json", mdPath: "doc.spm.md" });
|
|
16
|
+
*/
|
|
17
|
+
export function syncCommand(input) {
|
|
18
|
+
const { jsonPath, mdPath, strategy = "json", dryRun = false } = input;
|
|
19
|
+
// Read JSON document
|
|
20
|
+
const jsonContent = readFileSync(jsonPath, "utf8");
|
|
21
|
+
const jsonDoc = JSON.parse(jsonContent);
|
|
22
|
+
if (!SysProMDocument.is(jsonDoc)) {
|
|
23
|
+
throw new Error("JSON file is not a valid SysProM document");
|
|
24
|
+
}
|
|
25
|
+
// Parse Markdown to document
|
|
26
|
+
const mdDoc = markdownToJson(mdPath);
|
|
27
|
+
// Detect which side changed
|
|
28
|
+
const changes = detectChanges(jsonPath, mdPath);
|
|
29
|
+
// Perform sync operation
|
|
30
|
+
const result = syncDocumentsOp({
|
|
31
|
+
jsonDoc,
|
|
32
|
+
mdDoc,
|
|
33
|
+
jsonChanged: changes.jsonChanged,
|
|
34
|
+
mdChanged: changes.mdChanged,
|
|
35
|
+
strategy,
|
|
36
|
+
});
|
|
37
|
+
// Write results if not dry-run
|
|
38
|
+
if (!dryRun) {
|
|
39
|
+
// Write synced document back to both formats
|
|
40
|
+
if (result.jsonChanged || result.mdChanged || result.conflict) {
|
|
41
|
+
// Update JSON
|
|
42
|
+
writeFileSync(jsonPath, canonicalise(result.synced, { indent: "\t" }) + "\n");
|
|
43
|
+
// Update Markdown
|
|
44
|
+
const mdContent = jsonToMarkdownSingle(result.synced);
|
|
45
|
+
writeFileSync(mdPath, mdContent);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
function isArgs(arg) {
|
|
51
|
+
return (typeof arg === "object" && arg !== null && "input" in arg && "output" in arg);
|
|
52
|
+
}
|
|
53
|
+
function isOpts(opt) {
|
|
54
|
+
return typeof opt === "object" && opt !== null;
|
|
55
|
+
}
|
|
56
|
+
export const syncCommandDef = {
|
|
57
|
+
name: "sync",
|
|
58
|
+
description: "Synchronise JSON and Markdown representations with conflict resolution",
|
|
59
|
+
apiLink: "syncDocuments",
|
|
60
|
+
args: z.object({
|
|
61
|
+
input: z.string().describe("Path to JSON file"),
|
|
62
|
+
output: z.string().describe("Path to Markdown file"),
|
|
63
|
+
}),
|
|
64
|
+
opts: z
|
|
65
|
+
.object({
|
|
66
|
+
preferJson: z
|
|
67
|
+
.boolean()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Prefer JSON as source of truth in conflicts"),
|
|
70
|
+
preferMd: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Prefer Markdown as source of truth in conflicts"),
|
|
74
|
+
dryRun: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("Preview changes without writing files"),
|
|
78
|
+
report: z
|
|
79
|
+
.boolean()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Report conflicts without resolving"),
|
|
82
|
+
})
|
|
83
|
+
.strict(),
|
|
84
|
+
action(args, opts) {
|
|
85
|
+
if (!isArgs(args))
|
|
86
|
+
throw new Error("Invalid args");
|
|
87
|
+
if (!isOpts(opts))
|
|
88
|
+
throw new Error("Invalid opts");
|
|
89
|
+
const jsonPath = resolve(args.input);
|
|
90
|
+
const mdPath = resolve(args.output);
|
|
91
|
+
// Determine conflict strategy
|
|
92
|
+
let strategy = "json";
|
|
93
|
+
if (opts.preferMd)
|
|
94
|
+
strategy = "md";
|
|
95
|
+
if (opts.report)
|
|
96
|
+
strategy = "report";
|
|
97
|
+
const result = syncCommand({
|
|
98
|
+
jsonPath,
|
|
99
|
+
mdPath,
|
|
100
|
+
strategy,
|
|
101
|
+
dryRun: opts.dryRun ?? false,
|
|
102
|
+
});
|
|
103
|
+
// Output results
|
|
104
|
+
console.log(`Sync complete:`);
|
|
105
|
+
console.log(` JSON changed: ${String(result.jsonChanged)}`);
|
|
106
|
+
console.log(` Markdown changed: ${String(result.mdChanged)}`);
|
|
107
|
+
console.log(` Conflict: ${String(result.conflict)}`);
|
|
108
|
+
if (result.changedNodes.length > 0) {
|
|
109
|
+
console.log(` Changed nodes: ${result.changedNodes.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
package/dist/src/cli/program.js
CHANGED
|
@@ -16,6 +16,7 @@ import { updateCommand } from "./commands/update.js";
|
|
|
16
16
|
import { speckitCommand } from "./commands/speckit.js";
|
|
17
17
|
import { taskCommand } from "./commands/task.js";
|
|
18
18
|
import { planCommand } from "./commands/plan.js";
|
|
19
|
+
import { syncCommandDef } from "./commands/sync.js";
|
|
19
20
|
const VERSION = "1.0.0";
|
|
20
21
|
export const program = new Command();
|
|
21
22
|
program
|
|
@@ -40,6 +41,7 @@ export const commands = [
|
|
|
40
41
|
speckitCommand,
|
|
41
42
|
taskCommand,
|
|
42
43
|
planCommand,
|
|
44
|
+
syncCommandDef,
|
|
43
45
|
];
|
|
44
46
|
for (const cmd of commands) {
|
|
45
47
|
buildCommander(cmd, program);
|
package/dist/src/json-to-md.js
CHANGED
|
@@ -261,6 +261,7 @@ function generateReadme(doc, fromIdx) {
|
|
|
261
261
|
const lines = [];
|
|
262
262
|
const title = doc.metadata?.title ?? "SysProM";
|
|
263
263
|
lines.push(renderFrontMatter({
|
|
264
|
+
...(doc.$schema ? { $schema: doc.$schema } : {}),
|
|
264
265
|
title,
|
|
265
266
|
doc_type: doc.metadata?.doc_type ?? "sysprom",
|
|
266
267
|
scope: doc.metadata?.scope,
|
|
@@ -373,6 +374,7 @@ export function jsonToMarkdownSingle(doc) {
|
|
|
373
374
|
const lines = [];
|
|
374
375
|
const title = doc.metadata?.title ?? "SysProM";
|
|
375
376
|
lines.push(renderFrontMatter({
|
|
377
|
+
...(doc.$schema ? { $schema: doc.$schema } : {}),
|
|
376
378
|
title,
|
|
377
379
|
doc_type: doc.metadata?.doc_type ?? "sysprom",
|
|
378
380
|
scope: doc.metadata?.scope,
|
|
@@ -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
|
+
});
|
package/dist/src/md-to-json.js
CHANGED
|
@@ -35,6 +35,13 @@ 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. */
|
|
39
|
+
function extractSchema(front) {
|
|
40
|
+
const schema = typeof front.$schema === "string" ? front.$schema : undefined;
|
|
41
|
+
const metadata = { ...front };
|
|
42
|
+
delete metadata.$schema;
|
|
43
|
+
return { schema, metadata };
|
|
44
|
+
}
|
|
38
45
|
function parseFrontMatter(content) {
|
|
39
46
|
if (!content.startsWith("---\n"))
|
|
40
47
|
return { front: {}, body: content };
|
|
@@ -44,7 +51,7 @@ function parseFrontMatter(content) {
|
|
|
44
51
|
const yaml = content.slice(4, end);
|
|
45
52
|
const front = {};
|
|
46
53
|
for (const line of yaml.split("\n")) {
|
|
47
|
-
const match = /^(\w+):\s*(.+)$/.exec(line);
|
|
54
|
+
const match = /^([\w$]+):\s*(.+)$/.exec(line);
|
|
48
55
|
if (!match)
|
|
49
56
|
continue;
|
|
50
57
|
const [, key, raw] = match;
|
|
@@ -167,9 +174,22 @@ function parseListItems(body, prefix) {
|
|
|
167
174
|
return items;
|
|
168
175
|
}
|
|
169
176
|
function parseSingleValue(body, prefix) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
const lines = body.split("\n");
|
|
178
|
+
for (let i = 0; i < lines.length; i++) {
|
|
179
|
+
if (lines[i].startsWith(`${prefix}: `)) {
|
|
180
|
+
const firstLine = lines[i].slice(prefix.length + 2);
|
|
181
|
+
const continuationLines = [firstLine];
|
|
182
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
183
|
+
const next = lines[j];
|
|
184
|
+
if (next === "")
|
|
185
|
+
break;
|
|
186
|
+
if (next.startsWith("- ") || next.startsWith("#"))
|
|
187
|
+
break;
|
|
188
|
+
if (/^[A-Z][a-z]+: /.test(next))
|
|
189
|
+
break;
|
|
190
|
+
continuationLines.push(next);
|
|
191
|
+
}
|
|
192
|
+
return continuationLines.join("\n");
|
|
173
193
|
}
|
|
174
194
|
}
|
|
175
195
|
return undefined;
|
|
@@ -450,14 +470,16 @@ export function markdownSingleToJson(content) {
|
|
|
450
470
|
const { nodes, rels } = parseDocFile(content, allTypes);
|
|
451
471
|
const tableRels = parseRelationshipTable(body);
|
|
452
472
|
const extRefs = parseExternalReferences(body);
|
|
473
|
+
const { schema, metadata: metaFront } = extractSchema(front);
|
|
453
474
|
const doc = {
|
|
454
|
-
|
|
475
|
+
...(schema ? { $schema: schema } : {}),
|
|
476
|
+
metadata: Object.keys(metaFront).length > 0 ? metaFront : undefined,
|
|
455
477
|
nodes,
|
|
456
478
|
relationships: [...rels, ...tableRels].length > 0 ? [...rels, ...tableRels] : undefined,
|
|
457
479
|
external_references: extRefs.length > 0 ? extRefs : undefined,
|
|
458
480
|
};
|
|
459
|
-
if (
|
|
460
|
-
doc.metadata = { ...
|
|
481
|
+
if (metaFront.title && typeof metaFront.title === "string") {
|
|
482
|
+
doc.metadata = { ...metaFront };
|
|
461
483
|
}
|
|
462
484
|
return doc;
|
|
463
485
|
}
|
|
@@ -532,8 +554,10 @@ export function markdownMultiDocToJson(dir) {
|
|
|
532
554
|
}
|
|
533
555
|
}
|
|
534
556
|
scanForSubsystems(dir);
|
|
557
|
+
const { schema, metadata: metaFront } = extractSchema(front);
|
|
535
558
|
const doc = {
|
|
536
|
-
|
|
559
|
+
...(schema ? { $schema: schema } : {}),
|
|
560
|
+
metadata: Object.keys(metaFront).length > 0 ? metaFront : undefined,
|
|
537
561
|
nodes,
|
|
538
562
|
relationships: rels.length > 0 ? rels : undefined,
|
|
539
563
|
external_references: extRefs.length > 0 ? extRefs : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sysprom",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
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",
|