@zivis/mcp 0.1.10 → 0.1.12

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.
@@ -0,0 +1,181 @@
1
+ import { redactString } from "../redact.js";
2
+ function redact(text) {
3
+ if (text === undefined || text === null || text === "")
4
+ return undefined;
5
+ return redactString(String(text)).text;
6
+ }
7
+ function normalizeSeverity(raw) {
8
+ const s = String(raw ?? "").toLowerCase();
9
+ if (s.startsWith("crit"))
10
+ return "critical";
11
+ if (s === "high" || s === "error")
12
+ return "high";
13
+ if (s === "medium" || s === "moderate" || s === "warning" || s === "warn")
14
+ return "medium";
15
+ if (s === "low")
16
+ return "low";
17
+ if (s === "info" || s === "informational" || s === "unknown" || s === "none" || s === "") {
18
+ return s === "" || s === "unknown" || s === "none" ? "unknown" : "info";
19
+ }
20
+ return "unknown";
21
+ }
22
+ function asArray(v) {
23
+ return Array.isArray(v) ? v : [];
24
+ }
25
+ function lead(base) {
26
+ return { lane: "lead", ...base };
27
+ }
28
+ export function normalizeGitleaks(parsed) {
29
+ const findings = asArray(parsed);
30
+ return findings.map((f) => {
31
+ const tags = Array.isArray(f.Tags) ? f.Tags : [];
32
+ const sevTag = tags.find((t) => ["critical", "high", "medium", "low"].includes(String(t).toLowerCase()));
33
+ return lead({
34
+ source: "gitleaks",
35
+ rule_id: f.RuleID ?? "gitleaks-secret",
36
+ title: f.Description ?? f.RuleID ?? "Potential secret",
37
+ severity: sevTag ? normalizeSeverity(sevTag) : "high",
38
+ location: { file: f.File, start_line: f.StartLine, end_line: f.EndLine },
39
+ excerpt: redact(f.Match),
40
+ metadata: tags.length ? { tags } : undefined,
41
+ });
42
+ });
43
+ }
44
+ export function normalizeTrivy(parsed) {
45
+ const results = asArray(parsed?.Results);
46
+ const out = [];
47
+ for (const r of results) {
48
+ for (const v of asArray(r.Vulnerabilities)) {
49
+ out.push(lead({
50
+ source: "trivy",
51
+ rule_id: v.VulnerabilityID ?? "trivy-vuln",
52
+ title: v.Title ?? `${v.PkgName ?? "dependency"} ${v.VulnerabilityID ?? ""}`.trim(),
53
+ severity: normalizeSeverity(v.Severity),
54
+ location: { file: r.Target },
55
+ package: v.PkgName,
56
+ cwe: Array.isArray(v.CweIDs) && v.CweIDs.length ? v.CweIDs : undefined,
57
+ references: dedupeRefs(v.PrimaryURL, v.References),
58
+ metadata: {
59
+ installed_version: v.InstalledVersion,
60
+ fixed_version: v.FixedVersion,
61
+ },
62
+ }));
63
+ }
64
+ for (const s of asArray(r.Secrets)) {
65
+ out.push(lead({
66
+ source: "trivy",
67
+ rule_id: s.RuleID ?? "trivy-secret",
68
+ title: s.Title ?? "Potential secret",
69
+ severity: normalizeSeverity(s.Severity ?? "high"),
70
+ location: { file: r.Target, start_line: s.StartLine, end_line: s.EndLine },
71
+ excerpt: redact(s.Match),
72
+ }));
73
+ }
74
+ for (const m of asArray(r.Misconfigurations)) {
75
+ out.push(lead({
76
+ source: "trivy",
77
+ rule_id: m.ID ?? "trivy-misconfig",
78
+ title: m.Title ?? m.ID ?? "Misconfiguration",
79
+ severity: normalizeSeverity(m.Severity),
80
+ location: { file: r.Target },
81
+ references: dedupeRefs(m.PrimaryURL, m.References),
82
+ }));
83
+ }
84
+ }
85
+ return out;
86
+ }
87
+ export function normalizeSemgrep(parsed) {
88
+ const results = asArray(parsed?.results);
89
+ return results.map((r) => {
90
+ const meta = r.extra?.metadata ?? {};
91
+ return lead({
92
+ source: "semgrep",
93
+ rule_id: r.check_id ?? "semgrep-rule",
94
+ title: r.extra?.message ?? r.check_id ?? "Semgrep match",
95
+ severity: normalizeSeverity(r.extra?.severity),
96
+ location: { file: r.path, start_line: r.start?.line, end_line: r.end?.line },
97
+ cwe: toStringArray(meta.cwe),
98
+ owasp: toStringArray(meta.owasp),
99
+ references: Array.isArray(meta.references) ? meta.references : undefined,
100
+ excerpt: redact(r.extra?.lines),
101
+ });
102
+ });
103
+ }
104
+ export function normalizeOsvScanner(parsed) {
105
+ const results = asArray(parsed?.results);
106
+ const out = [];
107
+ for (const src of results) {
108
+ const file = src.source?.path;
109
+ for (const pkg of asArray(src.packages)) {
110
+ const pkgName = pkg.package?.name;
111
+ const groupSeverity = new Map();
112
+ for (const g of asArray(pkg.groups)) {
113
+ for (const id of asArray(g.ids)) {
114
+ if (g.max_severity)
115
+ groupSeverity.set(id, g.max_severity);
116
+ }
117
+ }
118
+ for (const v of asArray(pkg.vulnerabilities)) {
119
+ out.push(lead({
120
+ source: "osv-scanner",
121
+ rule_id: v.id ?? "osv-vuln",
122
+ title: v.summary ?? `${pkgName ?? "dependency"} ${v.id ?? ""}`.trim(),
123
+ severity: cvssScoreToSeverity(groupSeverity.get(v.id ?? "")),
124
+ location: { file },
125
+ package: pkgName,
126
+ references: asArray(v.references)
127
+ .map((r) => r.url)
128
+ .filter((u) => typeof u === "string"),
129
+ metadata: { version: pkg.package?.version, ecosystem: pkg.package?.ecosystem },
130
+ }));
131
+ }
132
+ }
133
+ }
134
+ return out;
135
+ }
136
+ function toStringArray(v) {
137
+ if (v === undefined || v === null)
138
+ return undefined;
139
+ const arr = Array.isArray(v) ? v : [v];
140
+ const out = arr.map((x) => String(x)).filter(Boolean);
141
+ return out.length ? out : undefined;
142
+ }
143
+ function dedupeRefs(primary, refs) {
144
+ const set = new Set();
145
+ if (primary)
146
+ set.add(primary);
147
+ for (const r of Array.isArray(refs) ? refs : [])
148
+ if (r)
149
+ set.add(r);
150
+ return set.size ? [...set] : undefined;
151
+ }
152
+ function cvssScoreToSeverity(score) {
153
+ if (!score)
154
+ return "unknown";
155
+ const n = Number.parseFloat(score);
156
+ if (Number.isNaN(n))
157
+ return "unknown";
158
+ if (n >= 9.0)
159
+ return "critical";
160
+ if (n >= 7.0)
161
+ return "high";
162
+ if (n >= 4.0)
163
+ return "medium";
164
+ if (n > 0)
165
+ return "low";
166
+ return "unknown";
167
+ }
168
+ export function normalizeScannerOutput(source, parsed) {
169
+ switch (source) {
170
+ case "gitleaks":
171
+ return normalizeGitleaks(parsed);
172
+ case "trivy":
173
+ return normalizeTrivy(parsed);
174
+ case "semgrep":
175
+ return normalizeSemgrep(parsed);
176
+ case "osv-scanner":
177
+ return normalizeOsvScanner(parsed);
178
+ default:
179
+ return [];
180
+ }
181
+ }
@@ -0,0 +1,27 @@
1
+ import type { LocalScanReport, ScannerId, ScannerRunResult } from "./types.js";
2
+ export declare const SUPPORTED_SCANNERS: readonly ScannerId[];
3
+ export declare const SCANNER_BINARIES: Record<ScannerId, string>;
4
+ export interface ScannerCommand {
5
+ bin: string;
6
+ args: string[];
7
+ }
8
+ export declare function scannerCommand(source: ScannerId, dir: string, outPath: string): ScannerCommand;
9
+ export interface ScannerExecEnv {
10
+ isAvailable(bin: string): Promise<boolean>;
11
+ execute(cmd: ScannerCommand, opts: {
12
+ timeoutMs: number;
13
+ }): Promise<{
14
+ reportText: string | null;
15
+ error?: string;
16
+ }>;
17
+ }
18
+ export declare const defaultExecEnv: ScannerExecEnv;
19
+ export interface RunLocalScannersOptions {
20
+ sources?: ScannerId[];
21
+ timeoutMs?: number;
22
+ env?: ScannerExecEnv;
23
+ tmpDir?: string;
24
+ }
25
+ export declare function runOneScanner(source: ScannerId, dir: string, opts?: RunLocalScannersOptions): Promise<ScannerRunResult>;
26
+ export declare function runLocalScanners(dir: string, opts?: RunLocalScannersOptions): Promise<LocalScanReport>;
27
+ export declare function aggregate(results: ScannerRunResult[]): LocalScanReport;
@@ -0,0 +1,142 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import * as fs from "node:fs/promises";
4
+ import * as crypto from "node:crypto";
5
+ import { execFile } from "node:child_process";
6
+ import { normalizeScannerOutput } from "./normalize.js";
7
+ export const SUPPORTED_SCANNERS = ["gitleaks", "trivy", "semgrep", "osv-scanner"];
8
+ export const SCANNER_BINARIES = {
9
+ gitleaks: "gitleaks",
10
+ trivy: "trivy",
11
+ semgrep: "semgrep",
12
+ "osv-scanner": "osv-scanner",
13
+ };
14
+ export function scannerCommand(source, dir, outPath) {
15
+ switch (source) {
16
+ case "gitleaks":
17
+ return {
18
+ bin: "gitleaks",
19
+ args: ["detect", "--source", dir, "--no-banner", "--report-format", "json", "--report-path", outPath, "--exit-code", "0"],
20
+ };
21
+ case "trivy":
22
+ return {
23
+ bin: "trivy",
24
+ args: ["fs", "--quiet", "--format", "json", "--output", outPath, "--scanners", "vuln,secret,misconfig", dir],
25
+ };
26
+ case "semgrep":
27
+ return {
28
+ bin: "semgrep",
29
+ args: ["scan", "--json", "--output", outPath, "--quiet", "--config", process.env.ZIVIS_SEMGREP_CONFIG || "auto", dir],
30
+ };
31
+ case "osv-scanner":
32
+ return {
33
+ bin: "osv-scanner",
34
+ args: ["--format", "json", "--output", outPath, "--recursive", dir],
35
+ };
36
+ default:
37
+ throw new Error(`unknown scanner: ${source}`);
38
+ }
39
+ }
40
+ const execFileAsync = (bin, args, timeoutMs) => new Promise((resolve) => {
41
+ execFile(bin, args, { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 }, (err) => {
42
+ const code = err && typeof err.code === "number" ? (err.code) : err ? 1 : 0;
43
+ resolve({ code, stderr: err ? String(err.message ?? "") : "" });
44
+ });
45
+ });
46
+ export const defaultExecEnv = {
47
+ async isAvailable(bin) {
48
+ return new Promise((resolve) => {
49
+ execFile(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [bin] : ["-v", bin], { shell: true, timeout: 5000 }, (err) => {
50
+ resolve(!err);
51
+ });
52
+ });
53
+ },
54
+ async execute(cmd, opts) {
55
+ const outIdx = cmd.args.findIndex((a) => a === "--output" || a === "--report-path");
56
+ const outPath = outIdx >= 0 ? cmd.args[outIdx + 1] : undefined;
57
+ if (!outPath)
58
+ return { reportText: null, error: "no output path in command" };
59
+ try {
60
+ await execFileAsync(cmd.bin, cmd.args, opts.timeoutMs);
61
+ const text = await fs.readFile(outPath, "utf8").catch(() => null);
62
+ return { reportText: text };
63
+ }
64
+ catch (err) {
65
+ return { reportText: null, error: err instanceof Error ? err.message : String(err) };
66
+ }
67
+ finally {
68
+ if (outPath)
69
+ await fs.rm(outPath, { force: true }).catch(() => { });
70
+ }
71
+ },
72
+ };
73
+ export async function runOneScanner(source, dir, opts = {}) {
74
+ const env = opts.env ?? defaultExecEnv;
75
+ const bin = SCANNER_BINARIES[source];
76
+ const started = Date.now();
77
+ if (!(await env.isAvailable(bin))) {
78
+ return { source, status: "not_installed", leads: [] };
79
+ }
80
+ const tmpDir = opts.tmpDir ?? os.tmpdir();
81
+ const outPath = path.join(tmpDir, `zivis-${source}-${crypto.randomUUID()}.json`);
82
+ const cmd = scannerCommand(source, dir, outPath);
83
+ try {
84
+ const { reportText, error } = await env.execute(cmd, { timeoutMs: opts.timeoutMs ?? 120_000 });
85
+ if (error && reportText === null) {
86
+ return { source, status: "error", leads: [], error, duration_ms: Date.now() - started };
87
+ }
88
+ if (reportText === null || reportText.trim() === "") {
89
+ return { source, status: "ok", leads: [], duration_ms: Date.now() - started };
90
+ }
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(reportText);
94
+ }
95
+ catch (e) {
96
+ return { source, status: "error", leads: [], error: `unparseable ${source} JSON: ${e instanceof Error ? e.message : String(e)}`, duration_ms: Date.now() - started };
97
+ }
98
+ const leads = normalizeScannerOutput(source, parsed);
99
+ return { source, status: "ok", leads, duration_ms: Date.now() - started };
100
+ }
101
+ catch (err) {
102
+ return { source, status: "error", leads: [], error: err instanceof Error ? err.message : String(err), duration_ms: Date.now() - started };
103
+ }
104
+ }
105
+ export async function runLocalScanners(dir, opts = {}) {
106
+ const sources = opts.sources ?? [...SUPPORTED_SCANNERS];
107
+ const results = [];
108
+ for (const source of sources) {
109
+ results.push(await runOneScanner(source, dir, opts));
110
+ }
111
+ return aggregate(results);
112
+ }
113
+ export function aggregate(results) {
114
+ const leads = [];
115
+ const bySource = {};
116
+ const bySeverity = {};
117
+ const scannersRun = [];
118
+ const scannersMissing = [];
119
+ for (const r of results) {
120
+ if (r.status === "not_installed")
121
+ scannersMissing.push(r.source);
122
+ else
123
+ scannersRun.push(r.source);
124
+ for (const l of r.leads) {
125
+ leads.push(l);
126
+ bySource[l.source] = (bySource[l.source] ?? 0) + 1;
127
+ bySeverity[l.severity] = (bySeverity[l.severity] ?? 0) + 1;
128
+ }
129
+ }
130
+ return {
131
+ ran_at: new Date().toISOString(),
132
+ results,
133
+ leads,
134
+ summary: {
135
+ total_leads: leads.length,
136
+ by_source: bySource,
137
+ by_severity: bySeverity,
138
+ scanners_run: scannersRun,
139
+ scanners_missing: scannersMissing,
140
+ },
141
+ };
142
+ }
@@ -0,0 +1,40 @@
1
+ export type ScannerId = "gitleaks" | "trivy" | "semgrep" | "osv-scanner";
2
+ export type LeadSeverity = "critical" | "high" | "medium" | "low" | "info" | "unknown";
3
+ export interface LeadLocation {
4
+ file?: string;
5
+ start_line?: number;
6
+ end_line?: number;
7
+ }
8
+ export interface NormalizedLead {
9
+ lane: "lead";
10
+ source: ScannerId;
11
+ rule_id: string;
12
+ title: string;
13
+ severity: LeadSeverity;
14
+ location?: LeadLocation;
15
+ package?: string;
16
+ cwe?: string[];
17
+ owasp?: string[];
18
+ references?: string[];
19
+ excerpt?: string;
20
+ metadata?: Record<string, unknown>;
21
+ }
22
+ export interface ScannerRunResult {
23
+ source: ScannerId;
24
+ status: "ok" | "not_installed" | "error";
25
+ leads: NormalizedLead[];
26
+ error?: string;
27
+ duration_ms?: number;
28
+ }
29
+ export interface LocalScanReport {
30
+ ran_at: string;
31
+ results: ScannerRunResult[];
32
+ leads: NormalizedLead[];
33
+ summary: {
34
+ total_leads: number;
35
+ by_source: Record<string, number>;
36
+ by_severity: Record<string, number>;
37
+ scanners_run: ScannerId[];
38
+ scanners_missing: ScannerId[];
39
+ };
40
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.js CHANGED
@@ -73,7 +73,6 @@ import { UPDATE_ENDPOINT_NAME, UPDATE_ENDPOINT_DESCRIPTION, UPDATE_ENDPOINT_SCHE
73
73
  import { MANAGE_APPLICATION_NAME, MANAGE_APPLICATION_DESCRIPTION, MANAGE_APPLICATION_SCHEMA, createManageApplicationHandler, } from "./tools/manage-application.js";
74
74
  import { MANAGE_ENDPOINT_LIFECYCLE_NAME, MANAGE_ENDPOINT_LIFECYCLE_DESCRIPTION, MANAGE_ENDPOINT_LIFECYCLE_SCHEMA, createManageEndpointLifecycleHandler, } from "./tools/manage-endpoint-lifecycle.js";
75
75
  import { DISCOVER_LOCAL_INFRA_NAME, DISCOVER_LOCAL_INFRA_DESCRIPTION, DISCOVER_LOCAL_INFRA_SCHEMA, createDiscoverLocalInfraHandler, } from "./tools/discover-local-infra.js";
76
- import { GENERATE_DIAGRAM_NAME, GENERATE_DIAGRAM_DESCRIPTION, GENERATE_DIAGRAM_SCHEMA, createGenerateDiagramHandler, } from "./tools/generate-diagram.js";
77
76
  import { LIST_DIAGRAMS_NAME, LIST_DIAGRAMS_DESCRIPTION, LIST_DIAGRAMS_SCHEMA, createListDiagramsHandler, } from "./tools/list-diagrams.js";
78
77
  import { GET_DIAGRAM_NAME, GET_DIAGRAM_DESCRIPTION, GET_DIAGRAM_SCHEMA, createGetDiagramHandler, } from "./tools/get-diagram.js";
79
78
  import { CREATE_DIAGRAM_NAME, CREATE_DIAGRAM_DESCRIPTION, CREATE_DIAGRAM_SCHEMA, createCreateDiagramHandler, } from "./tools/create-diagram.js";
@@ -215,10 +214,6 @@ export async function startServer(incoming = DEFAULT_CONFIG) {
215
214
  description: DISCOVER_LOCAL_INFRA_DESCRIPTION,
216
215
  inputSchema: DISCOVER_LOCAL_INFRA_SCHEMA,
217
216
  }, createDiscoverLocalInfraHandler());
218
- server.registerTool(GENERATE_DIAGRAM_NAME, {
219
- description: GENERATE_DIAGRAM_DESCRIPTION,
220
- inputSchema: GENERATE_DIAGRAM_SCHEMA,
221
- }, createGenerateDiagramHandler(apiClient));
222
217
  server.registerTool(LIST_DIAGRAMS_NAME, {
223
218
  description: LIST_DIAGRAMS_DESCRIPTION,
224
219
  inputSchema: LIST_DIAGRAMS_SCHEMA,
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { ApiClient } from "../api-client.js";
3
3
  export declare const CREATE_DIAGRAM_NAME = "zivis_create_diagram";
4
- export declare const CREATE_DIAGRAM_DESCRIPTION = "Create a new canvas diagram with nodes, connections, and boundaries in a single call.\n\nUse this to build system architecture diagrams, data flow diagrams, attack chains, etc. from code analysis.\n\n**Temp IDs:** When creating nodes and connections together, assign each node a temp_id (e.g., \"node_0\", \"node_1\") and reference them in connections via source_temp_id / target_temp_id. Similarly, assign boundaries a temp_id and reference them in nodes via boundary_temp_id.\n\n**Positioning:** Nodes default to (0,0). For readable layouts, space nodes apart (e.g., 250px horizontal, 150px vertical gaps). Default node size is 200x100px. Boundaries should be sized to contain their child nodes with padding.\n\n**Tags:** Use tags to annotate nodes with metadata (e.g., [\"database\", \"postgres\", \"port:5432\", \"pii\"]). These are searchable and displayed in the UI.\n\n**Colors:** Use hex colors (e.g., \"#336791\" for databases, \"#DC382D\" for caches, \"#009639\" for proxies).";
4
+ export declare const CREATE_DIAGRAM_DESCRIPTION = "Create a new diagram from Mermaid source.\n\nUse this to record system architecture diagrams, data flow diagrams, attack chains, sequence diagrams, etc. from code analysis. Content is Mermaid syntax (flowchart, sequenceDiagram, etc.) \u2014 see zivis_update_mermaid_source for editing an existing diagram's source.";
5
5
  export declare const CREATE_DIAGRAM_SCHEMA: {
6
6
  name: z.ZodString;
7
7
  description: z.ZodOptional<z.ZodString>;
@@ -15,84 +15,14 @@ export declare const CREATE_DIAGRAM_SCHEMA: {
15
15
  trust_boundary: "trust_boundary";
16
16
  network: "network";
17
17
  }>;
18
- nodes: z.ZodOptional<z.ZodArray<z.ZodObject<{
19
- temp_id: z.ZodOptional<z.ZodString>;
20
- name: z.ZodString;
21
- description: z.ZodOptional<z.ZodString>;
22
- tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
23
- icon: z.ZodOptional<z.ZodString>;
24
- color: z.ZodOptional<z.ZodString>;
25
- position_x: z.ZodOptional<z.ZodNumber>;
26
- position_y: z.ZodOptional<z.ZodNumber>;
27
- width: z.ZodOptional<z.ZodNumber>;
28
- height: z.ZodOptional<z.ZodNumber>;
29
- step_order: z.ZodOptional<z.ZodNumber>;
30
- boundary_temp_id: z.ZodOptional<z.ZodString>;
31
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
32
- }, z.core.$strip>>>;
33
- connections: z.ZodOptional<z.ZodArray<z.ZodObject<{
34
- source_temp_id: z.ZodOptional<z.ZodString>;
35
- target_temp_id: z.ZodOptional<z.ZodString>;
36
- source_node_id: z.ZodOptional<z.ZodString>;
37
- target_node_id: z.ZodOptional<z.ZodString>;
38
- label: z.ZodOptional<z.ZodString>;
39
- description: z.ZodOptional<z.ZodString>;
40
- tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
- bidirectional: z.ZodOptional<z.ZodBoolean>;
42
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
43
- }, z.core.$strip>>>;
44
- boundaries: z.ZodOptional<z.ZodArray<z.ZodObject<{
45
- temp_id: z.ZodOptional<z.ZodString>;
46
- label: z.ZodString;
47
- description: z.ZodOptional<z.ZodString>;
48
- color: z.ZodOptional<z.ZodString>;
49
- position_x: z.ZodOptional<z.ZodNumber>;
50
- position_y: z.ZodOptional<z.ZodNumber>;
51
- width: z.ZodOptional<z.ZodNumber>;
52
- height: z.ZodOptional<z.ZodNumber>;
53
- }, z.core.$strip>>>;
18
+ content: z.ZodString;
54
19
  linked_threat_model_id: z.ZodOptional<z.ZodString>;
55
20
  };
56
21
  export declare function createCreateDiagramHandler(apiClient: ApiClient): (params: {
57
22
  name: string;
58
23
  description?: string;
59
24
  diagram_type: string;
60
- nodes?: Array<{
61
- temp_id?: string;
62
- name: string;
63
- description?: string;
64
- tags?: string[];
65
- icon?: string;
66
- color?: string;
67
- position_x?: number;
68
- position_y?: number;
69
- width?: number;
70
- height?: number;
71
- step_order?: number;
72
- boundary_temp_id?: string;
73
- metadata?: Record<string, unknown>;
74
- }>;
75
- connections?: Array<{
76
- source_temp_id?: string;
77
- target_temp_id?: string;
78
- source_node_id?: string;
79
- target_node_id?: string;
80
- label?: string;
81
- description?: string;
82
- tags?: string[];
83
- bidirectional?: boolean;
84
- metadata?: Record<string, unknown>;
85
- }>;
86
- boundaries?: Array<{
87
- temp_id?: string;
88
- label: string;
89
- description?: string;
90
- color?: string;
91
- position_x?: number;
92
- position_y?: number;
93
- width?: number;
94
- height?: number;
95
- }>;
25
+ content: string;
96
26
  linked_threat_model_id?: string;
97
27
  }) => Promise<{
98
28
  content: {
@@ -1,17 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { sanitizeResponse } from "../sanitize.js";
3
3
  export const CREATE_DIAGRAM_NAME = "zivis_create_diagram";
4
- export const CREATE_DIAGRAM_DESCRIPTION = `Create a new canvas diagram with nodes, connections, and boundaries in a single call.
4
+ export const CREATE_DIAGRAM_DESCRIPTION = `Create a new diagram from Mermaid source.
5
5
 
6
- Use this to build system architecture diagrams, data flow diagrams, attack chains, etc. from code analysis.
7
-
8
- **Temp IDs:** When creating nodes and connections together, assign each node a temp_id (e.g., "node_0", "node_1") and reference them in connections via source_temp_id / target_temp_id. Similarly, assign boundaries a temp_id and reference them in nodes via boundary_temp_id.
9
-
10
- **Positioning:** Nodes default to (0,0). For readable layouts, space nodes apart (e.g., 250px horizontal, 150px vertical gaps). Default node size is 200x100px. Boundaries should be sized to contain their child nodes with padding.
11
-
12
- **Tags:** Use tags to annotate nodes with metadata (e.g., ["database", "postgres", "port:5432", "pii"]). These are searchable and displayed in the UI.
13
-
14
- **Colors:** Use hex colors (e.g., "#336791" for databases, "#DC382D" for caches, "#009639" for proxies).`;
6
+ Use this to record system architecture diagrams, data flow diagrams, attack chains, sequence diagrams, etc. from code analysis. Content is Mermaid syntax (flowchart, sequenceDiagram, etc.) — see zivis_update_mermaid_source for editing an existing diagram's source.`;
15
7
  export const CREATE_DIAGRAM_SCHEMA = {
16
8
  name: z
17
9
  .string()
@@ -23,51 +15,9 @@ export const CREATE_DIAGRAM_SCHEMA = {
23
15
  diagram_type: z
24
16
  .enum(["architecture", "data_flow", "attack_chain", "sequence", "trust_boundary", "network", "deployment", "custom"])
25
17
  .describe("Type of diagram"),
26
- nodes: z
27
- .array(z.object({
28
- temp_id: z.string().optional().describe("Temporary ID for referencing in connections (e.g., 'node_0')"),
29
- name: z.string().describe("Node name (e.g., 'PostgreSQL', 'API Gateway')"),
30
- description: z.string().optional().describe("Node description"),
31
- tags: z.array(z.string()).optional().describe("Tags (e.g., ['database', 'port:5432'])"),
32
- icon: z.string().optional().describe("Icon name (e.g., 'database', 'server', 'shield', 'globe')"),
33
- color: z.string().optional().describe("Hex color (e.g., '#336791')"),
34
- position_x: z.number().optional().describe("X position on canvas (default 0)"),
35
- position_y: z.number().optional().describe("Y position on canvas (default 0)"),
36
- width: z.number().optional().describe("Node width (default 200)"),
37
- height: z.number().optional().describe("Node height (default 100)"),
38
- step_order: z.number().optional().describe("Step order for attack chain diagrams"),
39
- boundary_temp_id: z.string().optional().describe("Temp ID of boundary this node belongs to"),
40
- metadata: z.record(z.string(), z.unknown()).optional().describe("Custom metadata"),
41
- }))
42
- .optional()
43
- .describe("Nodes to create"),
44
- connections: z
45
- .array(z.object({
46
- source_temp_id: z.string().optional().describe("Source node temp ID"),
47
- target_temp_id: z.string().optional().describe("Target node temp ID"),
48
- source_node_id: z.string().optional().describe("Source node UUID (if referencing existing node)"),
49
- target_node_id: z.string().optional().describe("Target node UUID (if referencing existing node)"),
50
- label: z.string().optional().describe("Connection label (e.g., 'HTTPS', 'depends on')"),
51
- description: z.string().optional().describe("Connection description"),
52
- tags: z.array(z.string()).optional().describe("Connection tags"),
53
- bidirectional: z.boolean().optional().describe("Whether connection goes both ways (default false)"),
54
- metadata: z.record(z.string(), z.unknown()).optional().describe("Custom metadata"),
55
- }))
56
- .optional()
57
- .describe("Connections between nodes"),
58
- boundaries: z
59
- .array(z.object({
60
- temp_id: z.string().optional().describe("Temporary ID for referencing in nodes (e.g., 'boundary_0')"),
61
- label: z.string().describe("Boundary label (e.g., 'DMZ', 'Internal Network')"),
62
- description: z.string().optional().describe("Boundary description"),
63
- color: z.string().optional().describe("Hex color"),
64
- position_x: z.number().optional().describe("X position (default 0)"),
65
- position_y: z.number().optional().describe("Y position (default 0)"),
66
- width: z.number().optional().describe("Boundary width (default 400)"),
67
- height: z.number().optional().describe("Boundary height (default 300)"),
68
- }))
69
- .optional()
70
- .describe("Boundary groups (trust boundaries, network segments)"),
18
+ content: z
19
+ .string()
20
+ .describe("Mermaid diagram source (e.g., 'flowchart TD\\n A[Client] --> B[Server]')"),
71
21
  linked_threat_model_id: z
72
22
  .string()
73
23
  .optional()
@@ -76,51 +26,13 @@ export const CREATE_DIAGRAM_SCHEMA = {
76
26
  export function createCreateDiagramHandler(apiClient) {
77
27
  return async (params) => {
78
28
  try {
79
- const apiNodes = params.nodes?.map((n) => ({
80
- tempId: n.temp_id,
81
- name: n.name,
82
- description: n.description,
83
- tags: n.tags,
84
- icon: n.icon,
85
- color: n.color,
86
- positionX: n.position_x,
87
- positionY: n.position_y,
88
- width: n.width,
89
- height: n.height,
90
- stepOrder: n.step_order,
91
- boundaryTempId: n.boundary_temp_id,
92
- metadata: n.metadata,
93
- }));
94
- const apiConnections = params.connections?.map((c) => ({
95
- sourceTempId: c.source_temp_id,
96
- targetTempId: c.target_temp_id,
97
- sourceNodeId: c.source_node_id,
98
- targetNodeId: c.target_node_id,
99
- label: c.label,
100
- description: c.description,
101
- tags: c.tags,
102
- bidirectional: c.bidirectional,
103
- metadata: c.metadata,
104
- }));
105
- const apiBoundaries = params.boundaries?.map((b) => ({
106
- tempId: b.temp_id,
107
- label: b.label,
108
- description: b.description,
109
- color: b.color,
110
- positionX: b.position_x,
111
- positionY: b.position_y,
112
- width: b.width,
113
- height: b.height,
114
- }));
115
29
  const data = await apiClient.post("/api/diagrams", {
116
30
  name: params.name,
117
31
  description: params.description,
118
32
  diagramType: params.diagram_type,
119
- contentType: "canvas",
33
+ contentType: "mermaid",
34
+ content: params.content,
120
35
  sourceType: "manual",
121
- nodes: apiNodes,
122
- connections: apiConnections,
123
- boundaries: apiBoundaries,
124
36
  });
125
37
  const sanitized = sanitizeResponse(data);
126
38
  const diagramId = sanitized.id;
@@ -135,16 +47,12 @@ export function createCreateDiagramHandler(apiClient) {
135
47
  catch {
136
48
  }
137
49
  }
138
- const nodeCount = Array.isArray(sanitized.nodes) ? sanitized.nodes.length : 0;
139
- const connCount = Array.isArray(sanitized.connections) ? sanitized.connections.length : 0;
140
- const boundaryCount = Array.isArray(sanitized.boundaries) ? sanitized.boundaries.length : 0;
141
50
  const result = {
142
51
  diagram_id: diagramId,
143
52
  name: sanitized.name,
144
53
  diagram_type: sanitized.diagramType,
145
- stats: { nodes: nodeCount, connections: connCount, boundaries: boundaryCount },
146
54
  linked_threat_model_id: linkedThreatModelId,
147
- _instruction: "Diagram created. Use zivis_get_diagram to see full content, or zivis_manage_diagram to modify.",
55
+ _instruction: "Diagram created. Use zivis_get_diagram to see full content, or zivis_manage_diagram to modify metadata/links, or zivis_update_mermaid_source to change the diagram source.",
148
56
  };
149
57
  return {
150
58
  content: [
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { ApiClient } from "../api-client.js";
3
3
  export declare const GET_DIAGRAM_NAME = "zivis_get_diagram";
4
- export declare const GET_DIAGRAM_DESCRIPTION = "Get the full content of a diagram including all nodes, connections, boundaries, and Mermaid source.\n\nReturns node IDs, names, tags, positions, boundary assignments, and metadata. Connection IDs include source/target node references and labels. Boundary IDs include labels and positions.\n\nFor mermaid-type diagrams, the `content` field contains the full Mermaid source string \u2014 use it to read or render the diagram. For canvas-type diagrams, `content` is null and the structure is expressed via nodes/connections/boundaries.\n\nUse this before modifying a diagram \u2014 you need node/connection/boundary IDs for zivis_manage_diagram (canvas) or the Mermaid source for zivis_update_mermaid_source (mermaid).";
4
+ export declare const GET_DIAGRAM_DESCRIPTION = "Get the full content of a diagram, including its Mermaid source.\n\nUse this before modifying a diagram \u2014 you need the current Mermaid source for zivis_update_mermaid_source.";
5
5
  export declare const GET_DIAGRAM_SCHEMA: {
6
6
  diagram_id: z.ZodString;
7
7
  };