callman-mcp 0.4.0 → 0.5.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/CHANGELOG.md +37 -27
- package/README.md +33 -11
- package/dist/client/api.d.ts +83 -15
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +106 -21
- package/dist/client/api.js.map +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +46 -11
- package/dist/client/errors.js.map +1 -1
- package/dist/client/types.d.ts +231 -14
- package/dist/client/types.d.ts.map +1 -1
- package/dist/domain/diagram/build.d.ts +17 -17
- package/dist/domain/diagram/build.d.ts.map +1 -1
- package/dist/domain/diagram/build.js +3 -3
- package/dist/domain/diagram/build.js.map +1 -1
- package/dist/domain/diagram/layout.d.ts +1 -3
- package/dist/domain/diagram/layout.d.ts.map +1 -1
- package/dist/domain/diagram/layout.js +3 -9
- package/dist/domain/diagram/layout.js.map +1 -1
- package/dist/domain/diagram/patch.d.ts +22 -723
- package/dist/domain/diagram/patch.d.ts.map +1 -1
- package/dist/domain/diagram/patch.js +0 -53
- package/dist/domain/diagram/patch.js.map +1 -1
- package/dist/domain/diagram/summarize.d.ts +0 -1
- package/dist/domain/diagram/summarize.d.ts.map +1 -1
- package/dist/domain/diagram/summarize.js +0 -1
- package/dist/domain/diagram/summarize.js.map +1 -1
- package/dist/domain/uiTest/compose.d.ts +55 -0
- package/dist/domain/uiTest/compose.d.ts.map +1 -0
- package/dist/domain/uiTest/compose.js +228 -0
- package/dist/domain/uiTest/compose.js.map +1 -0
- package/dist/domain/uiTest/summarize.d.ts +273 -0
- package/dist/domain/uiTest/summarize.d.ts.map +1 -0
- package/dist/domain/uiTest/summarize.js +145 -0
- package/dist/domain/uiTest/summarize.js.map +1 -0
- package/dist/lib/truncate.d.ts +1 -1
- package/dist/lib/truncate.d.ts.map +1 -1
- package/dist/lib/truncate.js.map +1 -1
- package/dist/resources/docs/diagramSchema.d.ts +1 -1
- package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
- package/dist/resources/docs/diagramSchema.js +1 -33
- package/dist/resources/docs/diagramSchema.js.map +1 -1
- package/dist/resources/docs/overview.d.ts +1 -1
- package/dist/resources/docs/overview.d.ts.map +1 -1
- package/dist/resources/docs/overview.js +13 -7
- package/dist/resources/docs/overview.js.map +1 -1
- package/dist/resources/docs/uiTests.d.ts +2 -0
- package/dist/resources/docs/uiTests.d.ts.map +1 -0
- package/dist/resources/docs/uiTests.js +101 -0
- package/dist/resources/docs/uiTests.js.map +1 -0
- package/dist/resources/generated/aiPacks.d.ts +1 -1
- package/dist/resources/generated/aiPacks.js +2 -2
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +13 -2
- package/dist/resources/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/catalog.d.ts +1 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +31 -5
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/diagrams.d.ts.map +1 -1
- package/dist/tools/diagrams.js +42 -328
- package/dist/tools/diagrams.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lookups.d.ts +19 -5
- package/dist/tools/lookups.d.ts.map +1 -1
- package/dist/tools/lookups.js +77 -41
- package/dist/tools/lookups.js.map +1 -1
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +10 -0
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/uiTestAdmin.d.ts +4 -0
- package/dist/tools/uiTestAdmin.d.ts.map +1 -0
- package/dist/tools/uiTestAdmin.js +509 -0
- package/dist/tools/uiTestAdmin.js.map +1 -0
- package/dist/tools/uiTestRuns.d.ts +24 -0
- package/dist/tools/uiTestRuns.d.ts.map +1 -0
- package/dist/tools/uiTestRuns.js +367 -0
- package/dist/tools/uiTestRuns.js.map +1 -0
- package/dist/tools/uiTestShared.d.ts +236 -0
- package/dist/tools/uiTestShared.d.ts.map +1 -0
- package/dist/tools/uiTestShared.js +187 -0
- package/dist/tools/uiTestShared.js.map +1 -0
- package/dist/tools/uiTests.d.ts +4 -0
- package/dist/tools/uiTests.d.ts.map +1 -0
- package/dist/tools/uiTests.js +572 -0
- package/dist/tools/uiTests.js.map +1 -0
- package/dist-cjs/client/api.js +106 -21
- package/dist-cjs/client/errors.js +46 -11
- package/dist-cjs/domain/diagram/build.js +3 -3
- package/dist-cjs/domain/diagram/layout.js +3 -10
- package/dist-cjs/domain/diagram/patch.js +1 -54
- package/dist-cjs/domain/diagram/summarize.js +0 -1
- package/dist-cjs/domain/uiTest/compose.js +242 -0
- package/dist-cjs/domain/uiTest/summarize.js +155 -0
- package/dist-cjs/resources/docs/diagramSchema.js +1 -33
- package/dist-cjs/resources/docs/overview.js +13 -7
- package/dist-cjs/resources/docs/uiTests.js +103 -0
- package/dist-cjs/resources/generated/aiPacks.js +2 -2
- package/dist-cjs/resources/index.js +11 -0
- package/dist-cjs/server.js +2 -2
- package/dist-cjs/tools/catalog.js +31 -5
- package/dist-cjs/tools/diagrams.js +37 -323
- package/dist-cjs/tools/index.js +6 -0
- package/dist-cjs/tools/lookups.js +83 -46
- package/dist-cjs/tools/shared.js +12 -1
- package/dist-cjs/tools/uiTestAdmin.js +512 -0
- package/dist-cjs/tools/uiTestRuns.js +372 -0
- package/dist-cjs/tools/uiTestShared.js +202 -0
- package/dist-cjs/tools/uiTests.js +575 -0
- package/package.json +3 -3
package/dist/tools/diagrams.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyDiagramOps, BPMN_GRAMMAR_VERSION, bpmnXmlFromDocument, createEmptyDocument, isMermaidFlowchart, looksLikeBpmn, looksLikeMermaid, MERMAID_GRAMMAR_VERSION, normalizeDocument, parseBpmn, parseMermaid, placeBpmnNodesOps, } from "callman-core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { CallmanApiError, ToolInputError } from "../client/errors.js";
|
|
4
4
|
import { buildDiagramDocument, diagramInputSchema, toToolError } from "../domain/diagram/build.js";
|
|
5
|
-
import { layoutDocument,
|
|
6
|
-
import { applyDiagramPatch,
|
|
5
|
+
import { layoutDocument, placeNewNodes } from "../domain/diagram/layout.js";
|
|
6
|
+
import { applyDiagramPatch, diagramPatchOpSchema } from "../domain/diagram/patch.js";
|
|
7
7
|
import { diagramFullView, diagramGraphView, diagramListView, diagramMermaidView, diagramSourceView } from "../domain/diagram/summarize.js";
|
|
8
8
|
import { resolvedSummary } from "../domain/resolve.js";
|
|
9
9
|
import { normalizeName } from "../lib/ids.js";
|
|
10
10
|
import { sliceWithCursor } from "../lib/pagination.js";
|
|
11
|
-
import {
|
|
12
|
-
import { confirmArg, cursorArgs, DESTRUCTIVE, dryRunArg, guarded, IDEMPOTENT_WRITE, ok, READ, refArg, requireConfirm,
|
|
11
|
+
import { loadDiagrams, resolveDiagram } from "./lookups.js";
|
|
12
|
+
import { confirmArg, cursorArgs, DESTRUCTIVE, dryRunArg, guarded, IDEMPOTENT_WRITE, ok, READ, refArg, requireConfirm, WRITE } from "./shared.js";
|
|
13
13
|
const directionArg = z.enum(["DOWN", "RIGHT", "UP", "LEFT"]).optional().describe("Layout flow direction (default DOWN)");
|
|
14
14
|
const sourceArg = z
|
|
15
15
|
.object({
|
|
16
|
-
format: z.enum(["mermaid", "bpmn"
|
|
16
|
+
format: z.enum(["mermaid", "bpmn"]).describe('"mermaid": Mermaid text; "bpmn": a BPMN 2.0 XML file'),
|
|
17
17
|
text: z
|
|
18
18
|
.string()
|
|
19
19
|
.min(1)
|
|
@@ -22,7 +22,7 @@ const sourceArg = z
|
|
|
22
22
|
})
|
|
23
23
|
.strict()
|
|
24
24
|
.optional()
|
|
25
|
-
.describe("Create the diagram from Mermaid text
|
|
25
|
+
.describe("Create the diagram from Mermaid text or BPMN 2.0 XML instead of nodes/edges");
|
|
26
26
|
// A BPMN diagram keeps its truth in BPMN 2.0 XML: the model is derived from it.
|
|
27
27
|
const bpmnSource = (text) => ({ format: "bpmn", text, diagramType: "bpmn", grammar: BPMN_GRAMMAR_VERSION });
|
|
28
28
|
// Typed bpmn.* nodes → a BPMN diagram: the model becomes XML, the XML becomes
|
|
@@ -33,83 +33,6 @@ const bpmnDocumentFromModel = (model, unpositionedNodeIds, base) => {
|
|
|
33
33
|
const document = applyDiagramOps({ ...base, nodes: [], edges: [], source: undefined }, [{ op: "setSource", source: bpmnSource(xml), resync: true }], { now: now() }).doc;
|
|
34
34
|
return { document, layoutNote: unpositionedNodeIds.length > 0 ? `Written as BPMN 2.0 XML; ${unpositionedNodeIds.length} node(s) placed left to right after their predecessors.` : "Written as BPMN 2.0 XML with the positions given." };
|
|
35
35
|
};
|
|
36
|
-
// A database diagram keeps its truth in the JSON database model.
|
|
37
|
-
const databaseSource = (text) => ({ format: "database", text, diagramType: "database", grammar: DATABASE_GRAMMAR_VERSION, valid: true });
|
|
38
|
-
const DATABASE_DIALECTS = ["postgres", "mysql", "mssql", "oracle", "mongo", "generic"];
|
|
39
|
-
const toolErrorFromDatabaseEdit = (error) => {
|
|
40
|
-
if (error instanceof DatabaseEditError)
|
|
41
|
-
throw new ToolInputError(error.code, error.message);
|
|
42
|
-
throw error;
|
|
43
|
-
};
|
|
44
|
-
// A database model → a database diagram: the model becomes the source, the
|
|
45
|
-
// tables are derived from it and laid out (unrelated tables packed in rows).
|
|
46
|
-
const databaseDocumentFromModel = async (model, base, keepPositions = false) => {
|
|
47
|
-
const applied = applyDiagramOps({ ...base, ...(keepPositions ? {} : { nodes: [], edges: [] }), source: undefined }, [{ op: "setSource", source: databaseSource(serializeDatabaseModel(model)), resync: true }], { now: now() });
|
|
48
|
-
let document = applied.doc;
|
|
49
|
-
if (keepPositions) {
|
|
50
|
-
const fresh = applied.added.nodes.filter((id) => document.nodes.some((node) => node.id === id));
|
|
51
|
-
if (fresh.length > 0)
|
|
52
|
-
document = placeDatabaseNodes(document, fresh);
|
|
53
|
-
return { document, layoutNote: fresh.length > 0 ? `${fresh.length} new table(s) placed to the right of the existing ones.` : "Positions kept." };
|
|
54
|
-
}
|
|
55
|
-
if (document.nodes.length === 0)
|
|
56
|
-
return { document, layoutNote: "An empty database diagram." };
|
|
57
|
-
const laid = await layoutDocument(document, { request: DATABASE_LAYOUT_REQUEST });
|
|
58
|
-
return { document: laid.document, layoutNote: laid.warning ?? `${document.nodes.length} table(s) laid out with ${laid.engine} (relationships left to right).` };
|
|
59
|
-
};
|
|
60
|
-
// Tables + relationships as the create tool takes them → a model.
|
|
61
|
-
const databaseModelFromInput = (tables, relationships, dialect) => {
|
|
62
|
-
const edits = [
|
|
63
|
-
...tables.map((table) => ({ kind: "addTable", table: { ...table, ...(table.columns ? { columns: table.columns.map((column) => ({ ...column, defaultValue: column.defaultValue ?? undefined, comment: column.comment ?? undefined })) } : {}) } })),
|
|
64
|
-
...relationships.map((relationship) => ({
|
|
65
|
-
kind: "addRelationship",
|
|
66
|
-
...(relationship.id ? { id: relationship.id } : {}),
|
|
67
|
-
source: relationship.source,
|
|
68
|
-
target: relationship.target,
|
|
69
|
-
...(relationship.cardinality ? { cardinality: relationship.cardinality } : {}),
|
|
70
|
-
...(relationship.kind ? { relationshipKind: relationship.kind } : {}),
|
|
71
|
-
...(relationship.name ? { name: relationship.name } : {}),
|
|
72
|
-
...(relationship.label ? { label: relationship.label } : {}),
|
|
73
|
-
})),
|
|
74
|
-
];
|
|
75
|
-
try {
|
|
76
|
-
return applyDatabaseEdits(emptyDatabaseModel(dialect), edits).model;
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
return toolErrorFromDatabaseEdit(error);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
// A database connection by id or name, in one workspace or across all.
|
|
83
|
-
const findDatabaseConnection = async (ctx, workspace, ref) => {
|
|
84
|
-
const wanted = ref.trim().toLowerCase();
|
|
85
|
-
const workspaces = await workspacesForListing(ctx, workspace);
|
|
86
|
-
const matches = [];
|
|
87
|
-
for (const ws of workspaces) {
|
|
88
|
-
for (const connection of await ctx.api.listConnections(ws.id, "database")) {
|
|
89
|
-
if (connection.id === ref.trim() || connection.name.trim().toLowerCase() === wanted)
|
|
90
|
-
matches.push({ ws, connection });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (matches.length === 0)
|
|
94
|
-
throw new ToolInputError("CONNECTION_NOT_FOUND", `No database connection "${ref}"${workspace ? ` in workspace ${workspace}` : " in your workspaces"}. list_connections shows them (type: database).`);
|
|
95
|
-
const exact = matches.filter((match) => match.connection.id === ref.trim());
|
|
96
|
-
if (exact.length === 1)
|
|
97
|
-
return exact[0];
|
|
98
|
-
if (matches.length > 1) {
|
|
99
|
-
throw new ToolInputError("AMBIGUOUS_CONNECTION", `"${ref}" matches ${matches.length} database connections; pass its id or a \`workspace\`.`, {
|
|
100
|
-
candidates: matches.map((match) => ({ id: match.connection.id, name: match.connection.name, workspace: workspaceTag(match.ws) })),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
return matches[0];
|
|
104
|
-
};
|
|
105
|
-
const schemaQuery = (options) => ({
|
|
106
|
-
...(options.schemas && options.schemas.length > 0 ? { schemas: options.schemas.join(",") } : {}),
|
|
107
|
-
...(options.tables && options.tables.length > 0 ? { tables: options.tables.join(",") } : {}),
|
|
108
|
-
...(options.includeViews !== undefined ? { includeViews: String(options.includeViews) } : {}),
|
|
109
|
-
...(options.objectsOnly ? { objectsOnly: "true" } : {}),
|
|
110
|
-
...(options.maxTables !== undefined ? { maxTables: options.maxTables } : {}),
|
|
111
|
-
});
|
|
112
|
-
const PROVIDER_LABEL = { postgresql: "PostgreSQL", mysql: "MySQL", mssql: "SQL Server", oracle: "Oracle", mongodb: "MongoDB" };
|
|
113
36
|
// Every document entering a tool goes through the shared gate (legacy fields
|
|
114
37
|
// stripped, ids deduped, parents healed) — the same one the studio applies.
|
|
115
38
|
const loadDocument = (diagram) => {
|
|
@@ -132,21 +55,6 @@ const assertEditableStructure = (doc) => {
|
|
|
132
55
|
// nodes/edges (positions come from layout); anything else is kept as
|
|
133
56
|
// source only (the desktop previews it, tools read it back).
|
|
134
57
|
const documentFromSource = async (title, text, existing, direction, format = "mermaid") => {
|
|
135
|
-
if (format === "database") {
|
|
136
|
-
const parsed = parseDatabaseModel(text);
|
|
137
|
-
if (!parsed.ok)
|
|
138
|
-
throw new ToolInputError("DIAGRAM_SOURCE_INVALID", "The text is not a readable database model.", { errors: parsed.errors.slice(0, 20) });
|
|
139
|
-
const base = existing ?? createEmptyDocument(title);
|
|
140
|
-
const keep = Boolean(existing && existing.source?.format === "database");
|
|
141
|
-
let built;
|
|
142
|
-
try {
|
|
143
|
-
built = await databaseDocumentFromModel(parsed.model, keep ? existing : { ...base, nodes: [], edges: [] }, keep);
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
return toToolError(error);
|
|
147
|
-
}
|
|
148
|
-
return { document: built.document, layoutNote: built.layoutNote, warnings: parsed.warnings };
|
|
149
|
-
}
|
|
150
58
|
if (format === "bpmn") {
|
|
151
59
|
if (!looksLikeBpmn(text))
|
|
152
60
|
throw new ToolInputError("DIAGRAM_SOURCE_INVALID", "The text is not a BPMN 2.0 document (no <definitions> root).");
|
|
@@ -216,59 +124,39 @@ const isConflict = (error) => error instanceof CallmanApiError && error.status =
|
|
|
216
124
|
export const registerDiagramTools = (server, ctx) => {
|
|
217
125
|
server.registerTool("list_diagrams", {
|
|
218
126
|
title: "List diagrams",
|
|
219
|
-
description: "List your system-design diagrams and the ones shared with you (owner, node/edge counts, whether you can edit)
|
|
220
|
-
inputSchema: {
|
|
221
|
-
query: z.string().max(200).optional(),
|
|
222
|
-
folder: z.string().max(300).optional().describe('Only the diagrams in this folder of your library (id, name or path such as "Architecture/Payments"); "/" = the top level only'),
|
|
223
|
-
...cursorArgs,
|
|
224
|
-
},
|
|
127
|
+
description: "List your system-design diagrams and the ones shared with you (owner, node/edge counts, whether you can edit). Diagrams belong to users and are shared into workspaces — not workspace-scoped.",
|
|
128
|
+
inputSchema: { query: z.string().max(200).optional(), ...cursorArgs },
|
|
225
129
|
annotations: READ,
|
|
226
|
-
}, guarded(async ({ query,
|
|
227
|
-
const
|
|
130
|
+
}, guarded(async ({ query, cursor, limit }) => {
|
|
131
|
+
const all = await loadDiagrams(ctx);
|
|
228
132
|
const needle = query?.trim().toLowerCase();
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
return false;
|
|
233
|
-
if (scope === undefined)
|
|
234
|
-
return true;
|
|
235
|
-
return (diagram.folderId ?? null) === (scope.item?.id ?? null);
|
|
236
|
-
});
|
|
237
|
-
const page = sliceWithCursor(filtered, cursor, limit, `${needle ?? ""}|${scope?.item?.id ?? (scope ? "/" : "")}`);
|
|
238
|
-
return ok({
|
|
239
|
-
diagrams: page.items.map((diagram) => ({ ...diagramListView(diagram), folder: diagramFolderPath(rows, diagram.folderId) })),
|
|
240
|
-
folders: rows.map((row) => ({ id: row.id, name: row.name, parentId: row.parentId, path: row.path })),
|
|
241
|
-
total: page.total,
|
|
242
|
-
nextCursor: page.nextCursor,
|
|
243
|
-
...(scope?.item && scope.annotation ? { resolved: [resolvedSummary(folder, scope)] } : {}),
|
|
244
|
-
});
|
|
133
|
+
const filtered = needle ? all.filter((diagram) => diagram.name.toLowerCase().includes(needle)) : all;
|
|
134
|
+
const page = sliceWithCursor(filtered, cursor, limit, needle ?? "");
|
|
135
|
+
return ok({ diagrams: page.items.map(diagramListView), total: page.total, nextCursor: page.nextCursor });
|
|
245
136
|
}));
|
|
246
137
|
server.registerTool("get_diagram", {
|
|
247
138
|
title: "Get a diagram",
|
|
248
|
-
description: "Read a diagram: `summary` (counts, owner, sharing), `graph` (default — nodes with type/label/parent/style and edges with semantic/color/routing/sides, in the same shape the write tools accept: enough to describe, restyle or edit it), `mermaid` (the diagram rendered as Mermaid flowchart text), `source` (the Mermaid text
|
|
249
|
-
inputSchema: { diagram: refArg("Diagram"), view: z.enum(["summary", "graph", "mermaid", "source", "
|
|
139
|
+
description: "Read a diagram: `summary` (counts, owner, sharing), `graph` (default — nodes with type/label/parent/style and edges with semantic/color/routing/sides, in the same shape the write tools accept: enough to describe, restyle or edit it), `mermaid` (the diagram rendered as Mermaid flowchart text), `source` (the Mermaid text or BPMN 2.0 XML a source-backed diagram keeps) or `full` (the complete stored DiagramDocument with positions, sizes and routes, up to 512 KB).",
|
|
140
|
+
inputSchema: { diagram: refArg("Diagram"), view: z.enum(["summary", "graph", "mermaid", "source", "full"]).default("graph") },
|
|
250
141
|
annotations: READ,
|
|
251
142
|
}, guarded(async ({ diagram, view }) => {
|
|
252
143
|
const resolved = await resolveDiagram(ctx, diagram);
|
|
253
144
|
const doc = loadDocument(resolved.item);
|
|
254
145
|
const summary = diagramListView(resolved.item);
|
|
255
|
-
const summaryWithFolder = { ...summary, folder: diagramFolderPath(await diagramFolderRows(ctx), resolved.item.folderId) };
|
|
256
146
|
const body = view === "summary"
|
|
257
|
-
?
|
|
147
|
+
? summary
|
|
258
148
|
: view === "graph"
|
|
259
149
|
? { ...summary, ...diagramGraphView(doc) }
|
|
260
150
|
: view === "mermaid"
|
|
261
151
|
? { ...summary, ...diagramMermaidView(doc) }
|
|
262
152
|
: view === "source"
|
|
263
153
|
? { ...summary, ...diagramSourceView(doc) }
|
|
264
|
-
:
|
|
265
|
-
? { ...summary, schema: databaseSchemaView(doc) }
|
|
266
|
-
: { ...summary, ...diagramFullView(doc) };
|
|
154
|
+
: { ...summary, ...diagramFullView(doc) };
|
|
267
155
|
return ok({ diagram: body, ...(resolved.annotation ? { resolved: [resolvedSummary(diagram, resolved)] } : {}) });
|
|
268
156
|
}));
|
|
269
157
|
server.registerTool("create_diagram", {
|
|
270
|
-
title: "Create a system-design
|
|
271
|
-
description: "Create a diagram from typed nodes and edges — or from Mermaid text / BPMN 2.0 XML
|
|
158
|
+
title: "Create a system-design diagram",
|
|
159
|
+
description: "Create a diagram from typed nodes and edges — or from Mermaid text / BPMN 2.0 XML (`source`). A BPMN process (`kind: \"bpmn\"`, or nodes typed bpmn.*) is stored as BPMN 2.0 XML the desktop opens in its BPMN editor. Node `type` must come from callman://diagram/components (e.g. net.service, net.api-gateway, db.postgres, mq.kafka, aws.lambda, flow.process); unknown types are rejected with suggestions unless strictTypes: false. Group nodes inside a container node — frame, pool.horizontal or pool.vertical — with `parent`. Style nodes with `style` (fill / stroke / textColor as CSS colors, strokeWidth, dashed, radius, bold, fontSize, opacity…) and edges with `semantic` (sync default, async, fallback, bus), an optional `color`, `routing` and fixed `sourceSide`/`targetSide`. Layout is automatic when nodes have no positions. Read callman://docs/diagram-schema for every field. Keep it readable: 8–25 nodes, short labels.",
|
|
272
160
|
inputSchema: {
|
|
273
161
|
name: z.string().min(1).max(200).describe("Diagram name (library entry)"),
|
|
274
162
|
title: diagramInputSchema.shape.title.optional().describe("Diagram title (defaults to the name)"),
|
|
@@ -276,73 +164,28 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
276
164
|
edges: diagramInputSchema.shape.edges.optional(),
|
|
277
165
|
canvas: diagramInputSchema.shape.canvas,
|
|
278
166
|
source: sourceArg,
|
|
279
|
-
kind: z.enum(["canvas", "bpmn"
|
|
280
|
-
tables: z.array(dbTableInputSchema).max(500).optional().describe("Database diagram: the tables (name, schema?, kind?, columns with dataType / primaryKey / nullable / unique / defaultValue / comment)"),
|
|
281
|
-
relationships: z.array(dbRelationshipInputSchema).max(2000).optional().describe('Database diagram: relationships between the tables — source is the child (foreign-key) side as "table.column", target the parent; cardinality source:target (default N:1)'),
|
|
282
|
-
dialect: z.enum(DATABASE_DIALECTS).optional().describe("Database diagram: which database the types are written for"),
|
|
283
|
-
fromConnection: z
|
|
284
|
-
.object({
|
|
285
|
-
connection: z.string().min(1).max(200).describe("A database connection by id or name (list_connections, type: database)"),
|
|
286
|
-
schemas: z.array(z.string().min(1).max(120)).max(50).optional().describe("Schemas to read (default: every user schema; MySQL: the connection's database; Oracle: the current schema)"),
|
|
287
|
-
tables: z.array(z.string().min(1).max(240)).max(2000).optional().describe('Only these tables ("schema.table" or "table"); default: all'),
|
|
288
|
-
includeViews: z.boolean().optional(),
|
|
289
|
-
})
|
|
290
|
-
.strict()
|
|
291
|
-
.optional()
|
|
292
|
-
.describe("Database diagram: read a real database's schema through one of the workspace's connections and draw it — tables, columns, keys and the relationships the database declares"),
|
|
293
|
-
workspace: workspaceArg,
|
|
294
|
-
folder: z.string().max(300).optional().describe('File it in a folder of your library (id, name or path such as "Architecture/Payments"; create one with create_diagram_folder). Default: the top level'),
|
|
167
|
+
kind: z.enum(["canvas", "bpmn"]).optional().describe('"bpmn": model a business process — nodes use bpmn.* types and the diagram is stored as BPMN 2.0 XML (default: canvas; inferred as bpmn when every node type starts with "bpmn.")'),
|
|
295
168
|
autoLayout: z.boolean().optional().describe("Force layout even when positions were given"),
|
|
296
169
|
direction: directionArg,
|
|
297
170
|
strictTypes: z.boolean().optional(),
|
|
298
171
|
dryRun: dryRunArg,
|
|
299
172
|
},
|
|
300
173
|
annotations: WRITE,
|
|
301
|
-
}, guarded(async ({ name, title, nodes, edges, canvas, source, kind,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
throw new ToolInputError("
|
|
306
|
-
if (inputs.length > 1)
|
|
307
|
-
throw new ToolInputError("CONFLICTING_INPUT", `Pass one of nodes, source, tables or fromConnection — not ${inputs.join(" + ")}.`);
|
|
308
|
-
if (relationships && !tables)
|
|
309
|
-
throw new ToolInputError("CONFLICTING_INPUT", "relationships go with tables.");
|
|
174
|
+
}, guarded(async ({ name, title, nodes, edges, canvas, source, kind, autoLayout, direction, strictTypes, dryRun }) => {
|
|
175
|
+
if (!nodes && !source)
|
|
176
|
+
throw new ToolInputError("NOTHING_TO_CREATE", "Pass nodes (+ edges) or source { format: \"mermaid\" | \"bpmn\", text }.");
|
|
177
|
+
if (nodes && source)
|
|
178
|
+
throw new ToolInputError("CONFLICTING_INPUT", "Pass either nodes or source, not both.");
|
|
310
179
|
let document;
|
|
311
180
|
let layoutNote;
|
|
312
181
|
let warnings = [];
|
|
313
182
|
const bpmnKind = kind === "bpmn" || (kind === undefined && Boolean(nodes && nodes.length > 0 && nodes.every((node) => node.type.startsWith("bpmn."))));
|
|
314
|
-
const databaseKind = kind === "database" || Boolean(tables) || Boolean(fromConnection) || (kind === undefined && Boolean(nodes && nodes.length > 0 && nodes.every((node) => isDatabaseType(node.type))));
|
|
315
|
-
let connectionNote = {};
|
|
316
183
|
if (source) {
|
|
317
184
|
const fromSource = await documentFromSource(title ?? name, source.text, null, direction, source.format);
|
|
318
185
|
document = canvas ? { ...fromSource.document, canvas: { ...fromSource.document.canvas, ...canvas } } : fromSource.document;
|
|
319
186
|
layoutNote = fromSource.layoutNote;
|
|
320
187
|
warnings = fromSource.warnings;
|
|
321
188
|
}
|
|
322
|
-
else if (fromConnection) {
|
|
323
|
-
const found = await findDatabaseConnection(ctx, workspace, fromConnection.connection);
|
|
324
|
-
const schema = await ctx.api.getConnectionSchema(found.ws.id, found.connection.id, schemaQuery(fromConnection));
|
|
325
|
-
const model = databaseModelFromSchema(schema, { connection: { connectionId: found.connection.id, name: found.connection.name, provider: found.connection.provider } });
|
|
326
|
-
const built = await databaseDocumentFromModel(model, createEmptyDocument(title ?? name));
|
|
327
|
-
document = built.document;
|
|
328
|
-
layoutNote = built.layoutNote;
|
|
329
|
-
warnings = [...schema.warnings, ...(schema.truncated ? ["The database has more tables than the cap; the list was cut."] : [])];
|
|
330
|
-
connectionNote = { connection: { id: found.connection.id, name: found.connection.name, provider: PROVIDER_LABEL[found.connection.provider] ?? found.connection.provider, workspace: workspaceTag(found.ws) }, tables: model.entities.length, relationships: model.relationships.length };
|
|
331
|
-
}
|
|
332
|
-
else if (tables) {
|
|
333
|
-
const model = databaseModelFromInput(tables, relationships ?? [], dialect);
|
|
334
|
-
const built = await databaseDocumentFromModel(model, createEmptyDocument(title ?? name));
|
|
335
|
-
document = built.document;
|
|
336
|
-
layoutNote = built.layoutNote;
|
|
337
|
-
}
|
|
338
|
-
else if (databaseKind) {
|
|
339
|
-
const built = buildDiagramDocument({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes, kind: "database" });
|
|
340
|
-
const model = databaseModelFromDocument({ ...built.document, source: undefined }, dialect);
|
|
341
|
-
const database = await databaseDocumentFromModel(model, createEmptyDocument(title ?? name));
|
|
342
|
-
document = database.document;
|
|
343
|
-
layoutNote = database.layoutNote;
|
|
344
|
-
warnings = built.warnings;
|
|
345
|
-
}
|
|
346
189
|
else if (bpmnKind) {
|
|
347
190
|
const built = buildDiagramDocument({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes, kind: "bpmn" });
|
|
348
191
|
const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, createEmptyDocument(title ?? name));
|
|
@@ -360,26 +203,21 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
360
203
|
layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
|
|
361
204
|
}
|
|
362
205
|
}
|
|
363
|
-
if (document.source?.format === "database" && canvas && !source)
|
|
364
|
-
document = { ...document, canvas: { ...document.canvas, ...canvas } };
|
|
365
|
-
const view = document.source?.format === "database" ? { ...diagramGraphView(document), schema: databaseSchemaView(document) } : diagramGraphView(document);
|
|
366
206
|
if (dryRun) {
|
|
367
|
-
return ok({ dryRun: true, diagram: { name,
|
|
207
|
+
return ok({ dryRun: true, diagram: { name, ...diagramGraphView(document) }, warnings, ...(layoutNote ? { layout: layoutNote } : {}) });
|
|
368
208
|
}
|
|
369
|
-
const created = await ctx.api.createDiagram({ name: name.trim(), document
|
|
209
|
+
const created = await ctx.api.createDiagram({ name: name.trim(), document });
|
|
370
210
|
ctx.invalidate("diagrams");
|
|
371
211
|
return ok({
|
|
372
|
-
diagram: { ...diagramListView(created),
|
|
212
|
+
diagram: { ...diagramListView(created), ...diagramGraphView(document) },
|
|
373
213
|
warnings,
|
|
374
|
-
...connectionNote,
|
|
375
214
|
...(layoutNote ? { layout: layoutNote } : {}),
|
|
376
|
-
|
|
377
|
-
note: `Saved${targetFolder ? ` in ${targetFolder.item.path}` : ""}. Open it in the desktop app (Diagrams → Refresh). Edit with patch_diagram; share with share_diagram.`,
|
|
215
|
+
note: "Saved. Open it in the desktop app (Diagrams → Refresh). Edit with patch_diagram; share with share_diagram.",
|
|
378
216
|
});
|
|
379
217
|
}));
|
|
380
218
|
server.registerTool("update_diagram", {
|
|
381
219
|
title: "Replace a diagram's content",
|
|
382
|
-
description: "Replace a diagram's whole document (nodes + edges, same fields as create_diagram), replace or attach its Mermaid text
|
|
220
|
+
description: "Replace a diagram's whole document (nodes + edges, same fields as create_diagram), replace or attach its Mermaid text or BPMN 2.0 XML `source`, and/or rename it, retitle it or change its canvas — keeping its identity and sharing. Passing nodes replaces ALL nodes and edges (omitted edges are removed). Passing `source` re-derives nodes and edges from the text (existing ones keep their place; new ones are placed). For small edits — including restyling — prefer patch_diagram.",
|
|
383
221
|
inputSchema: {
|
|
384
222
|
diagram: refArg("Diagram"),
|
|
385
223
|
name: z.string().min(1).max(200).optional(),
|
|
@@ -388,19 +226,17 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
388
226
|
edges: diagramInputSchema.shape.edges.optional(),
|
|
389
227
|
canvas: diagramInputSchema.shape.canvas,
|
|
390
228
|
source: sourceArg,
|
|
391
|
-
folder: z.string().max(300).nullable().optional().describe('Move the diagram into this folder of your library (id, name or path); null = the top level'),
|
|
392
229
|
autoLayout: z.boolean().optional(),
|
|
393
230
|
direction: directionArg,
|
|
394
231
|
strictTypes: z.boolean().optional(),
|
|
395
232
|
},
|
|
396
233
|
annotations: IDEMPOTENT_WRITE,
|
|
397
|
-
}, guarded(async ({ diagram, name, title, nodes, edges, canvas, source,
|
|
398
|
-
if (name === undefined && nodes === undefined && title === undefined && canvas === undefined && source === undefined
|
|
399
|
-
throw new ToolInputError("NOTHING_TO_UPDATE", "Pass name, title, canvas, source
|
|
234
|
+
}, guarded(async ({ diagram, name, title, nodes, edges, canvas, source, autoLayout, direction, strictTypes }) => {
|
|
235
|
+
if (name === undefined && nodes === undefined && title === undefined && canvas === undefined && source === undefined) {
|
|
236
|
+
throw new ToolInputError("NOTHING_TO_UPDATE", "Pass name, title, canvas, source and/or nodes (+ edges).");
|
|
400
237
|
}
|
|
401
238
|
if (nodes && source)
|
|
402
239
|
throw new ToolInputError("CONFLICTING_INPUT", "Pass either nodes or source, not both.");
|
|
403
|
-
const targetFolder = folder ? await resolveDiagramFolder(ctx, folder) : null;
|
|
404
240
|
const resolved = await resolveDiagram(ctx, diagram);
|
|
405
241
|
if (!resolved.item.canEdit)
|
|
406
242
|
throw new ToolInputError("DIAGRAM_READ_ONLY", "You have read-only access to this diagram.");
|
|
@@ -408,19 +244,7 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
408
244
|
let document;
|
|
409
245
|
let layoutNote;
|
|
410
246
|
let warnings = [];
|
|
411
|
-
if (nodes && existing.source?.format === "
|
|
412
|
-
// A database diagram stays one: the tables given become the new model.
|
|
413
|
-
const built = buildDiagramDocument({ title: title ?? existing.meta.title, nodes, edges: edges ?? [], canvas: canvas ?? existing.canvas }, { strictTypes, existingId: existing.id, createdAt: existing.meta.createdAt, kind: "database" });
|
|
414
|
-
const previous = parseDatabaseModel(existing.source.text).model;
|
|
415
|
-
const model = databaseModelFromDocument({ ...built.document, source: undefined }, previous.dialect);
|
|
416
|
-
if (previous.connection)
|
|
417
|
-
model.connection = previous.connection;
|
|
418
|
-
const database = await databaseDocumentFromModel(model, existing, true);
|
|
419
|
-
document = database.document;
|
|
420
|
-
layoutNote = database.layoutNote;
|
|
421
|
-
warnings = [...built.warnings, "The database model was rewritten from the tables given; tables not in the input are gone. Prefer patch_diagram (addTable, addColumn, addRelationship …) for edits."];
|
|
422
|
-
}
|
|
423
|
-
else if (nodes && existing.source?.format === "bpmn") {
|
|
247
|
+
if (nodes && existing.source?.format === "bpmn") {
|
|
424
248
|
// A BPMN diagram stays BPMN: the new model is written as fresh XML.
|
|
425
249
|
const built = buildDiagramDocument({ title: title ?? existing.meta.title, nodes, edges: edges ?? [], canvas: canvas ?? existing.canvas }, { strictTypes, existingId: existing.id, createdAt: existing.meta.createdAt, kind: "bpmn" });
|
|
426
250
|
const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, existing);
|
|
@@ -437,7 +261,7 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
437
261
|
warnings.push(`edges was omitted, so the ${existing.edges.length} existing edge(s) were removed. Pass edges to keep connections, or use patch_diagram for partial edits.`);
|
|
438
262
|
}
|
|
439
263
|
if (existing.source)
|
|
440
|
-
warnings.push(
|
|
264
|
+
warnings.push("The diagram's Mermaid source was detached: nodes and edges were replaced from the input.");
|
|
441
265
|
if (autoLayout || built.unpositionedNodeIds.length > 0) {
|
|
442
266
|
const laid = await layoutDocument(document, { direction });
|
|
443
267
|
document = laid.document;
|
|
@@ -467,20 +291,13 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
467
291
|
const updated = await ctx.api.updateDiagram(resolved.item.id, {
|
|
468
292
|
...(name !== undefined ? { name: name.trim() } : {}),
|
|
469
293
|
...(document ? { document, ...(typeof existing.meta.revision === "number" ? { baseRevision: existing.meta.revision } : {}) } : {}),
|
|
470
|
-
...(folder !== undefined ? { folderId: targetFolder?.item.id ?? null } : {}),
|
|
471
294
|
});
|
|
472
295
|
ctx.invalidate("diagrams");
|
|
473
|
-
|
|
474
|
-
return ok({
|
|
475
|
-
diagram: { ...diagramListView(updated), folder: diagramFolderPath(rows, updated.folderId), ...diagramGraphView(document ?? existing) },
|
|
476
|
-
warnings,
|
|
477
|
-
...(layoutNote ? { layout: layoutNote } : {}),
|
|
478
|
-
resolved: [resolvedSummary(diagram, resolved), ...(targetFolder?.annotation ? [resolvedSummary(folder, targetFolder)] : [])],
|
|
479
|
-
});
|
|
296
|
+
return ok({ diagram: { ...diagramListView(updated), ...diagramGraphView(document ?? existing) }, warnings, ...(layoutNote ? { layout: layoutNote } : {}), resolved: [resolvedSummary(diagram, resolved)] });
|
|
480
297
|
}));
|
|
481
298
|
server.registerTool("patch_diagram", {
|
|
482
299
|
title: "Edit a diagram incrementally",
|
|
483
|
-
description: "Apply operations to a diagram without resending it: addNode, updateNode, removeNode (drops its edges, lifts its children out), addEdge, updateEdge, removeEdge, setTitle, moveNode, reparent, setEdgeRoute (routing, fixed sides, bends), reorder (stacking), setCanvas, group (wrap nodes in a container), ungroup, relayout (a hint: new | all).
|
|
300
|
+
description: "Apply operations to a diagram without resending it: addNode, updateNode, removeNode (drops its edges, lifts its children out), addEdge, updateEdge, removeEdge, setTitle, moveNode, reparent, setEdgeRoute (routing, fixed sides, bends), reorder (stacking), setCanvas, group (wrap nodes in a container), ungroup, relayout (a hint: new | all). updateNode `changes` MERGE: style and data keys you pass change, a key set to null is removed. updateEdge can change semantic, color, label, arrows, sides and ends. Moving a node re-routes its edges. `relayout`: none | new (place only new nodes, default) | all (re-run automatic layout). Call get_diagram (view: graph) first to see node ids and current styles. Ops are documented in callman://docs/diagram-schema.",
|
|
484
301
|
inputSchema: {
|
|
485
302
|
diagram: refArg("Diagram"),
|
|
486
303
|
ops: z.array(diagramPatchOpSchema).min(1).max(200),
|
|
@@ -504,21 +321,15 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
504
321
|
const wantsAll = relayout === "all" || patched.relayout?.scope === "all";
|
|
505
322
|
const layoutDirection = direction ?? patched.relayout?.direction;
|
|
506
323
|
const bpmn = document.source?.format === "bpmn";
|
|
507
|
-
const database = document.source?.format === "database";
|
|
508
324
|
if (wantsAll) {
|
|
509
|
-
// A process reads left to right; the BPMN XML follows the moves.
|
|
510
|
-
|
|
511
|
-
const laid = await layoutDocument(document, { direction: layoutDirection ?? (bpmn || database ? "RIGHT" : undefined), ...(database ? { request: DATABASE_LAYOUT_REQUEST } : {}) });
|
|
325
|
+
// A process reads left to right; the BPMN XML follows the moves.
|
|
326
|
+
const laid = await layoutDocument(document, { direction: layoutDirection ?? (bpmn ? "RIGHT" : undefined) });
|
|
512
327
|
document = laid.document;
|
|
513
328
|
layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
|
|
514
329
|
}
|
|
515
330
|
else if (relayout === "new" && patched.addedNodeIds.length > 0) {
|
|
516
331
|
const ids = patched.addedNodeIds.filter((id) => document.nodes.some((node) => node.id === id));
|
|
517
|
-
if (
|
|
518
|
-
document = placeDatabaseNodes(document, ids);
|
|
519
|
-
layoutNote = `Placed ${ids.length} new table(s) to the right of the existing ones.`;
|
|
520
|
-
}
|
|
521
|
-
else if (bpmn) {
|
|
332
|
+
if (bpmn) {
|
|
522
333
|
document = applyDiagramOps(document, placeBpmnNodesOps(document, ids), { now: now() }).doc;
|
|
523
334
|
layoutNote = `Placed ${ids.length} new node(s) after their predecessors.`;
|
|
524
335
|
}
|
|
@@ -547,7 +358,7 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
547
358
|
ctx.invalidate("diagrams");
|
|
548
359
|
const { updated, document, patched, layoutNote } = outcome;
|
|
549
360
|
return ok({
|
|
550
|
-
diagram: { ...diagramListView(updated), ...diagramGraphView(document)
|
|
361
|
+
diagram: { ...diagramListView(updated), ...diagramGraphView(document) },
|
|
551
362
|
applied: ops.length,
|
|
552
363
|
added: patched.addedNodeIds,
|
|
553
364
|
removedNodes: patched.removedNodeIds,
|
|
@@ -559,103 +370,6 @@ export const registerDiagramTools = (server, ctx) => {
|
|
|
559
370
|
resolved: [resolvedSummary(diagram, resolved)],
|
|
560
371
|
});
|
|
561
372
|
}));
|
|
562
|
-
server.registerTool("introspect_database", {
|
|
563
|
-
title: "Read a database's schema through a connection",
|
|
564
|
-
description: "Read the database behind one of the workspace's database connections (list_connections, type: database): schemas, tables and views (MongoDB: collections with fields inferred from a sample), columns with types, nullability, defaults and comments, primary and unique keys, foreign keys with referential actions, indexes. Metadata only — never credentials, never data. Use `objectsOnly` to list names first, then read the tables you need; `create_diagram` with `fromConnection` draws the result as a Database Diagram in one step. Runs on the Callman backend, which must be able to reach the database.",
|
|
565
|
-
inputSchema: {
|
|
566
|
-
connection: z.string().min(1).max(200).describe("Connection id or name"),
|
|
567
|
-
workspace: workspaceArg,
|
|
568
|
-
schemas: z.array(z.string().min(1).max(120)).max(50).optional(),
|
|
569
|
-
tables: z.array(z.string().min(1).max(240)).max(2000).optional().describe('"schema.table" or "table"'),
|
|
570
|
-
includeViews: z.boolean().optional(),
|
|
571
|
-
objectsOnly: z.boolean().optional().describe("Names only (no columns / keys) — for a first look at a large database"),
|
|
572
|
-
maxTables: z.number().int().min(1).max(20000).optional(),
|
|
573
|
-
},
|
|
574
|
-
annotations: READ,
|
|
575
|
-
}, guarded(async ({ connection, workspace, schemas, tables, includeViews, objectsOnly, maxTables }) => {
|
|
576
|
-
const found = await findDatabaseConnection(ctx, workspace, connection);
|
|
577
|
-
const schema = await ctx.api.getConnectionSchema(found.ws.id, found.connection.id, schemaQuery({ schemas, tables, includeViews, objectsOnly, maxTables }));
|
|
578
|
-
return ok({
|
|
579
|
-
connection: { id: found.connection.id, name: found.connection.name, provider: PROVIDER_LABEL[found.connection.provider] ?? found.connection.provider, workspace: workspaceTag(found.ws) },
|
|
580
|
-
schema,
|
|
581
|
-
...(schema.truncated ? { note: "The database has more tables than the cap; pass schemas/tables to narrow the read or raise maxTables." } : {}),
|
|
582
|
-
});
|
|
583
|
-
}));
|
|
584
|
-
// ── library folders (private to the token's owner) ──────────────────
|
|
585
|
-
const folderView = (row) => ({ id: row.id, name: row.name, parentId: row.parentId, path: row.path });
|
|
586
|
-
server.registerTool("create_diagram_folder", {
|
|
587
|
-
title: "Create a diagram folder",
|
|
588
|
-
description: "Create a folder in your diagram library, optionally inside a parent folder (id, name or path). Folders are yours alone (nobody else sees them; a diagram you share shows at the top level for others), nest up to 8 levels, and sibling names are unique — an existing folder with that name is returned instead of a duplicate. File diagrams with create_diagram / update_diagram `folder`.",
|
|
589
|
-
inputSchema: {
|
|
590
|
-
name: z.string().min(1).max(120),
|
|
591
|
-
parent: z.string().max(300).optional().describe("Parent folder id, name or path; omit for the top level"),
|
|
592
|
-
},
|
|
593
|
-
annotations: WRITE,
|
|
594
|
-
}, guarded(async ({ name, parent }) => {
|
|
595
|
-
const parentFolder = parent ? await resolveDiagramFolder(ctx, parent) : null;
|
|
596
|
-
const rows = await diagramFolderRows(ctx);
|
|
597
|
-
const trimmed = name.trim();
|
|
598
|
-
const sibling = rows.find((row) => row.parentId === (parentFolder?.item.id ?? null) && normalizeName(row.name) === normalizeName(trimmed));
|
|
599
|
-
if (sibling) {
|
|
600
|
-
return ok({ folder: folderView(sibling), existed: true, note: "A folder with that name already exists there — returned instead of creating a duplicate." });
|
|
601
|
-
}
|
|
602
|
-
const created = await ctx.api.createDiagramFolder({ name: trimmed, parentId: parentFolder?.item.id ?? null });
|
|
603
|
-
ctx.invalidate("diagram-folders");
|
|
604
|
-
const path = parentFolder ? `${parentFolder.item.path}/${created.name}` : created.name;
|
|
605
|
-
return ok({
|
|
606
|
-
folder: { id: created.id, name: created.name, parentId: created.parentId, path },
|
|
607
|
-
...(parentFolder?.annotation ? { resolved: [resolvedSummary(parent, parentFolder)] } : {}),
|
|
608
|
-
});
|
|
609
|
-
}));
|
|
610
|
-
server.registerTool("update_diagram_folder", {
|
|
611
|
-
title: "Rename or move a diagram folder",
|
|
612
|
-
description: "Rename a folder of your diagram library and/or move it under another folder (parent: null = the top level). A folder cannot be moved into itself or one of its subfolders.",
|
|
613
|
-
inputSchema: {
|
|
614
|
-
folder: refArg("Diagram folder", "(or path)"),
|
|
615
|
-
name: z.string().min(1).max(120).optional(),
|
|
616
|
-
parent: z.string().max(300).nullable().optional().describe("New parent folder (id, name or path); null = the top level"),
|
|
617
|
-
},
|
|
618
|
-
annotations: IDEMPOTENT_WRITE,
|
|
619
|
-
}, guarded(async ({ folder, name, parent }) => {
|
|
620
|
-
if (name === undefined && parent === undefined)
|
|
621
|
-
throw new ToolInputError("NOTHING_TO_UPDATE", "Pass name and/or parent.");
|
|
622
|
-
const target = await resolveDiagramFolder(ctx, folder);
|
|
623
|
-
const parentFolder = parent ? await resolveDiagramFolder(ctx, parent) : null;
|
|
624
|
-
if (parentFolder && (parentFolder.item.id === target.item.id || parentFolder.item.path.startsWith(`${target.item.path}/`))) {
|
|
625
|
-
throw new ToolInputError("FOLDER_CYCLE", "A folder cannot be moved into itself or one of its subfolders.");
|
|
626
|
-
}
|
|
627
|
-
const updated = await ctx.api.updateDiagramFolder(target.item.id, {
|
|
628
|
-
...(name !== undefined ? { name: name.trim() } : {}),
|
|
629
|
-
...(parent !== undefined ? { parentId: parentFolder?.item.id ?? null } : {}),
|
|
630
|
-
});
|
|
631
|
-
ctx.invalidate("diagram-folders");
|
|
632
|
-
const rows = await diagramFolderRows(ctx);
|
|
633
|
-
const row = rows.find((entry) => entry.id === updated.id);
|
|
634
|
-
return ok({
|
|
635
|
-
folder: row ? folderView(row) : { id: updated.id, name: updated.name, parentId: updated.parentId, path: updated.name },
|
|
636
|
-
resolved: [resolvedSummary(folder, target), ...(parentFolder?.annotation ? [resolvedSummary(parent, parentFolder)] : [])],
|
|
637
|
-
});
|
|
638
|
-
}));
|
|
639
|
-
server.registerTool("delete_diagram_folder", {
|
|
640
|
-
title: "Delete a diagram folder",
|
|
641
|
-
description: "Delete a folder of your diagram library and its subfolders. No diagram is deleted: everything inside moves to the deleted folder's parent. Confirm with the user first.",
|
|
642
|
-
inputSchema: { folder: refArg("Diagram folder", "(or path)"), confirm: confirmArg },
|
|
643
|
-
annotations: DESTRUCTIVE,
|
|
644
|
-
}, guarded(async ({ folder, confirm }) => {
|
|
645
|
-
requireConfirm(confirm, "a diagram folder");
|
|
646
|
-
const target = await resolveDiagramFolder(ctx, folder, { exact: true });
|
|
647
|
-
const result = await ctx.api.deleteDiagramFolder(target.item.id);
|
|
648
|
-
ctx.invalidate("diagram-folders");
|
|
649
|
-
ctx.invalidate("diagrams");
|
|
650
|
-
const rows = await diagramFolderRows(ctx);
|
|
651
|
-
return ok({
|
|
652
|
-
deleted: folderView(target.item),
|
|
653
|
-
deletedFolderCount: result.deletedFolderIds.length,
|
|
654
|
-
movedDiagramCount: result.movedDiagramCount,
|
|
655
|
-
movedTo: diagramFolderPath(rows, result.movedToFolderId) ?? "the top level",
|
|
656
|
-
resolved: [resolvedSummary(folder, target)],
|
|
657
|
-
});
|
|
658
|
-
}));
|
|
659
373
|
server.registerTool("delete_diagram", {
|
|
660
374
|
title: "Delete a diagram",
|
|
661
375
|
description: "Delete a diagram you own. Permanent — confirm with the user first.",
|