enterprise-architect-mcp 2.1.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/LICENSE +190 -0
- package/README.md +226 -0
- package/dist/database.d.ts +3 -0
- package/dist/database.js +25 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +68 -0
- package/dist/model-session.d.ts +29 -0
- package/dist/model-session.js +172 -0
- package/dist/package-path.d.ts +2 -0
- package/dist/package-path.js +36 -0
- package/dist/remembered-path.d.ts +5 -0
- package/dist/remembered-path.js +43 -0
- package/dist/resolve-qea-path.d.ts +26 -0
- package/dist/resolve-qea-path.js +58 -0
- package/dist/text.d.ts +37 -0
- package/dist/text.js +152 -0
- package/dist/tools/annotations.d.ts +3 -0
- package/dist/tools/annotations.js +5 -0
- package/dist/tools/connectors.d.ts +3 -0
- package/dist/tools/connectors.js +152 -0
- package/dist/tools/diagrams.d.ts +3 -0
- package/dist/tools/diagrams.js +225 -0
- package/dist/tools/elements.d.ts +3 -0
- package/dist/tools/elements.js +210 -0
- package/dist/tools/packages.d.ts +3 -0
- package/dist/tools/packages.js +80 -0
- package/dist/tools/resolve.d.ts +3 -0
- package/dist/tools/resolve.js +135 -0
- package/dist/tools/scenarios.d.ts +3 -0
- package/dist/tools/scenarios.js +106 -0
- package/dist/tools/schema.d.ts +3 -0
- package/dist/tools/schema.js +164 -0
- package/dist/tools/search.d.ts +3 -0
- package/dist/tools/search.js +217 -0
- package/dist/tools/windowing.d.ts +37 -0
- package/dist/tools/windowing.js +92 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.js +18 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { READ_ONLY } from "./annotations.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { decodeEntities } from "../text.js";
|
|
4
|
+
// Extract braced GUID from LFSP/LFEP tokens, discarding trailing anchor letter
|
|
5
|
+
const FEATURE_LINK_RE = /LF([SE])P=\{([^}]+)\}[^;]*/g;
|
|
6
|
+
function resolveFeatureGuid(db, guid) {
|
|
7
|
+
// Try attribute first
|
|
8
|
+
const attr = db
|
|
9
|
+
.prepare(`SELECT a.Name, a.Notes, o.Name as ElementName
|
|
10
|
+
FROM t_attribute a LEFT JOIN t_object o ON a.Object_ID = o.Object_ID
|
|
11
|
+
WHERE a.ea_guid = ? COLLATE NOCASE`)
|
|
12
|
+
.get(guid);
|
|
13
|
+
if (attr) {
|
|
14
|
+
return { name: attr.Name, owningElementName: attr.ElementName, notes: decodeEntities(attr.Notes), type: "attribute" };
|
|
15
|
+
}
|
|
16
|
+
// Try operation
|
|
17
|
+
const op = db
|
|
18
|
+
.prepare(`SELECT p.Name, p.Notes, o.Name as ElementName
|
|
19
|
+
FROM t_operation p LEFT JOIN t_object o ON p.Object_ID = o.Object_ID
|
|
20
|
+
WHERE p.ea_guid = ? COLLATE NOCASE`)
|
|
21
|
+
.get(guid);
|
|
22
|
+
if (op) {
|
|
23
|
+
return { name: op.Name, owningElementName: op.ElementName, notes: decodeEntities(op.Notes), type: "operation" };
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function parseFeatureLinks(db, styleEx) {
|
|
28
|
+
if (!styleEx)
|
|
29
|
+
return { sourceFeature: null, targetFeature: null };
|
|
30
|
+
let sourceFeature = null;
|
|
31
|
+
let targetFeature = null;
|
|
32
|
+
let m;
|
|
33
|
+
FEATURE_LINK_RE.lastIndex = 0;
|
|
34
|
+
while ((m = FEATURE_LINK_RE.exec(styleEx)) !== null) {
|
|
35
|
+
const side = m[1]; // S = source, E = target
|
|
36
|
+
const guid = `{${m[2]}}`;
|
|
37
|
+
const resolved = resolveFeatureGuid(db, guid);
|
|
38
|
+
const feature = resolved
|
|
39
|
+
? { resolved: true, ...resolved }
|
|
40
|
+
: { resolved: false, present: true, guid };
|
|
41
|
+
if (side === "S")
|
|
42
|
+
sourceFeature = feature;
|
|
43
|
+
else
|
|
44
|
+
targetFeature = feature;
|
|
45
|
+
}
|
|
46
|
+
return { sourceFeature, targetFeature };
|
|
47
|
+
}
|
|
48
|
+
export function configureConnectorTools(server, model) {
|
|
49
|
+
server.tool("ea_get_connectors", "Get all relationships (connectors) for a given element. `connectors` lists what the element is connected to and how (Realisation, Dependency, Association, etc.), each entry naming its `source` and `dest` ends. Feature links show which specific attribute or operation each end attaches to.", {
|
|
50
|
+
elementId: z.coerce.number().describe("The Object_ID of the element to get connectors for"),
|
|
51
|
+
connectorType: z
|
|
52
|
+
.string()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Filter by connector type (e.g., Realisation, Dependency, Association, InformationFlow, Generalization)"),
|
|
55
|
+
direction: z
|
|
56
|
+
.enum(["both", "outgoing", "incoming"])
|
|
57
|
+
.default("both")
|
|
58
|
+
.describe("Filter direction: outgoing (element is source), incoming (element is target), or both"),
|
|
59
|
+
}, READ_ONLY, async ({ elementId, connectorType, direction }) => {
|
|
60
|
+
const db = await model.database();
|
|
61
|
+
try {
|
|
62
|
+
// Verify element exists
|
|
63
|
+
const elExists = db.prepare("SELECT Object_ID FROM t_object WHERE Object_ID = ?").get(elementId);
|
|
64
|
+
if (!elExists) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Element with ID ${elementId} not found`, elementId }, null, 2) }],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
let conditions = [];
|
|
71
|
+
const params = [];
|
|
72
|
+
if (direction === "outgoing") {
|
|
73
|
+
conditions.push("c.Start_Object_ID = ?");
|
|
74
|
+
params.push(elementId);
|
|
75
|
+
}
|
|
76
|
+
else if (direction === "incoming") {
|
|
77
|
+
conditions.push("c.End_Object_ID = ?");
|
|
78
|
+
params.push(elementId);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
conditions.push("(c.Start_Object_ID = ? OR c.End_Object_ID = ?)");
|
|
82
|
+
params.push(elementId, elementId);
|
|
83
|
+
}
|
|
84
|
+
if (connectorType) {
|
|
85
|
+
conditions.push("c.Connector_Type = ?");
|
|
86
|
+
params.push(connectorType);
|
|
87
|
+
}
|
|
88
|
+
const sql = `
|
|
89
|
+
SELECT c.Connector_ID, c.Connector_Type, c.SubType, c.Name, c.Direction,
|
|
90
|
+
c.Stereotype, c.Notes, c.SourceCard, c.DestCard,
|
|
91
|
+
c.Start_Object_ID, c.End_Object_ID,
|
|
92
|
+
c.SourceRole, c.DestRole, c.StyleEx,
|
|
93
|
+
src.Name as SourceName, src.Object_Type as SourceType, src.Stereotype as SourceStereotype,
|
|
94
|
+
dst.Name as DestName, dst.Object_Type as DestType, dst.Stereotype as DestStereotype
|
|
95
|
+
FROM t_connector c
|
|
96
|
+
LEFT JOIN t_object src ON c.Start_Object_ID = src.Object_ID
|
|
97
|
+
LEFT JOIN t_object dst ON c.End_Object_ID = dst.Object_ID
|
|
98
|
+
WHERE ${conditions.join(" AND ")}
|
|
99
|
+
ORDER BY c.Connector_Type, c.Name
|
|
100
|
+
`;
|
|
101
|
+
const rows = db.prepare(sql).all(...params);
|
|
102
|
+
const connectors = rows.map((r) => {
|
|
103
|
+
const { sourceFeature, targetFeature } = parseFeatureLinks(db, r.StyleEx);
|
|
104
|
+
return {
|
|
105
|
+
id: r.Connector_ID,
|
|
106
|
+
type: r.Connector_Type,
|
|
107
|
+
subType: r.SubType,
|
|
108
|
+
name: r.Name,
|
|
109
|
+
direction: r.Start_Object_ID === elementId ? "outgoing" : "incoming",
|
|
110
|
+
stereotype: r.Stereotype,
|
|
111
|
+
notes: decodeEntities(r.Notes),
|
|
112
|
+
sourceCard: r.SourceCard,
|
|
113
|
+
destCard: r.DestCard,
|
|
114
|
+
sourceRole: r.SourceRole || null,
|
|
115
|
+
destRole: r.DestRole || null,
|
|
116
|
+
source: { id: r.Start_Object_ID, name: r.SourceName, type: r.SourceType, stereotype: r.SourceStereotype },
|
|
117
|
+
dest: { id: r.End_Object_ID, name: r.DestName, type: r.DestType, stereotype: r.DestStereotype },
|
|
118
|
+
sourceFeature,
|
|
119
|
+
targetFeature,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
if (connectors.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
125
|
+
connectors: [],
|
|
126
|
+
totalMatched: 0,
|
|
127
|
+
returned: 0,
|
|
128
|
+
truncated: false,
|
|
129
|
+
_meta: { sourceTables: ["t_connector", "t_object", "t_attribute", "t_operation"] },
|
|
130
|
+
}, null, 2) }],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const response = {
|
|
134
|
+
connectors,
|
|
135
|
+
totalMatched: connectors.length,
|
|
136
|
+
returned: connectors.length,
|
|
137
|
+
truncated: false,
|
|
138
|
+
_meta: { sourceTables: ["t_connector", "t_object", "t_attribute", "t_operation"] },
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: `Error retrieving connectors: ${msg}` }],
|
|
148
|
+
isError: true,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { READ_ONLY } from "./annotations.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { decodeEntities, foldText } from "../text.js";
|
|
4
|
+
import { buildPackagePath } from "../package-path.js";
|
|
5
|
+
import { breakdownApplies, buildBreakdown, buildContinuation, countBy, isTruncated, limitParam, offsetParam } from "./windowing.js";
|
|
6
|
+
// Reuse feature link parsing from connectors — import the module's export
|
|
7
|
+
// Since the feature link logic is internal to connectors, we inline a lightweight version here
|
|
8
|
+
const FEATURE_LINK_RE = /LF([SE])P=\{([^}]+)\}[^;]*/g;
|
|
9
|
+
function resolveFeatureGuid(db, guid) {
|
|
10
|
+
const attr = db
|
|
11
|
+
.prepare(`SELECT a.Name, a.Notes, o.Name as ElementName
|
|
12
|
+
FROM t_attribute a LEFT JOIN t_object o ON a.Object_ID = o.Object_ID
|
|
13
|
+
WHERE a.ea_guid = ? COLLATE NOCASE`)
|
|
14
|
+
.get(guid);
|
|
15
|
+
if (attr) {
|
|
16
|
+
return { resolved: true, name: attr.Name, owningElementName: attr.ElementName, notes: decodeEntities(attr.Notes), type: "attribute" };
|
|
17
|
+
}
|
|
18
|
+
const op = db
|
|
19
|
+
.prepare(`SELECT p.Name, p.Notes, o.Name as ElementName
|
|
20
|
+
FROM t_operation p LEFT JOIN t_object o ON p.Object_ID = o.Object_ID
|
|
21
|
+
WHERE p.ea_guid = ? COLLATE NOCASE`)
|
|
22
|
+
.get(guid);
|
|
23
|
+
if (op) {
|
|
24
|
+
return { resolved: true, name: op.Name, owningElementName: op.ElementName, notes: decodeEntities(op.Notes), type: "operation" };
|
|
25
|
+
}
|
|
26
|
+
return { resolved: false, present: true, guid };
|
|
27
|
+
}
|
|
28
|
+
function parseFeatureLinks(db, styleEx) {
|
|
29
|
+
if (!styleEx)
|
|
30
|
+
return { sourceFeature: null, targetFeature: null };
|
|
31
|
+
let sourceFeature = null;
|
|
32
|
+
let targetFeature = null;
|
|
33
|
+
let m;
|
|
34
|
+
FEATURE_LINK_RE.lastIndex = 0;
|
|
35
|
+
while ((m = FEATURE_LINK_RE.exec(styleEx)) !== null) {
|
|
36
|
+
const side = m[1];
|
|
37
|
+
const guid = `{${m[2]}}`;
|
|
38
|
+
const feature = resolveFeatureGuid(db, guid);
|
|
39
|
+
if (side === "S")
|
|
40
|
+
sourceFeature = feature;
|
|
41
|
+
else
|
|
42
|
+
targetFeature = feature;
|
|
43
|
+
}
|
|
44
|
+
return { sourceFeature, targetFeature };
|
|
45
|
+
}
|
|
46
|
+
export function configureDiagramTools(server, model) {
|
|
47
|
+
server.tool("ea_get_diagram_elements", "Get all elements and connectors placed on a specific diagram: the `diagram` itself, plus `elements` and `connectors`. Connectors include feature-link resolution showing which attribute or operation each end attaches to. The connector list is the union of explicit t_diagramlinks rows and implied connectors (both ends on the diagram).", {
|
|
48
|
+
diagramId: z.coerce.number().describe("The Diagram_ID to get elements for"),
|
|
49
|
+
}, READ_ONLY, async ({ diagramId }) => {
|
|
50
|
+
const db = await model.database();
|
|
51
|
+
try {
|
|
52
|
+
const diagram = db.prepare(`
|
|
53
|
+
SELECT d.Diagram_ID, d.Name, d.Diagram_Type, d.Package_ID, d.Notes,
|
|
54
|
+
p.Name as PackageName
|
|
55
|
+
FROM t_diagram d
|
|
56
|
+
LEFT JOIN t_package p ON d.Package_ID = p.Package_ID
|
|
57
|
+
WHERE d.Diagram_ID = ?
|
|
58
|
+
`).get(diagramId);
|
|
59
|
+
if (!diagram) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Diagram with ID ${diagramId} not found`, diagramId }, null, 2) }],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const elements = db.prepare(`
|
|
66
|
+
SELECT o.Object_ID, o.Object_Type, o.Name, o.Alias, o.Stereotype, o.Note
|
|
67
|
+
FROM t_diagramobjects do_
|
|
68
|
+
JOIN t_object o ON do_.Object_ID = o.Object_ID
|
|
69
|
+
WHERE do_.Diagram_ID = ?
|
|
70
|
+
ORDER BY do_.Sequence
|
|
71
|
+
`).all(diagramId);
|
|
72
|
+
const elementResults = elements.map((e) => {
|
|
73
|
+
const result = { Object_ID: e.Object_ID, Object_Type: e.Object_Type, Name: e.Name, Alias: e.Alias, Stereotype: e.Stereotype };
|
|
74
|
+
if (e.Object_Type === "Note")
|
|
75
|
+
result.Note = decodeEntities(e.Note);
|
|
76
|
+
return result;
|
|
77
|
+
});
|
|
78
|
+
// R2: Connectors on diagram — union of explicit links and implied connectors
|
|
79
|
+
// Explicit: rows in t_diagramlinks for this diagram
|
|
80
|
+
// Implied: connectors whose both Start_Object_ID and End_Object_ID appear in t_diagramobjects for this diagram
|
|
81
|
+
const connectorRows = db.prepare(`
|
|
82
|
+
SELECT DISTINCT c.Connector_ID, c.Connector_Type, c.SubType, c.Name, c.Direction,
|
|
83
|
+
c.Stereotype, c.Notes, c.SourceCard, c.DestCard,
|
|
84
|
+
c.Start_Object_ID, c.End_Object_ID,
|
|
85
|
+
c.SourceRole, c.DestRole, c.StyleEx,
|
|
86
|
+
src.Name as SourceName, src.Object_Type as SourceType,
|
|
87
|
+
dst.Name as DestName, dst.Object_Type as DestType,
|
|
88
|
+
dl.Hidden
|
|
89
|
+
FROM t_connector c
|
|
90
|
+
LEFT JOIN t_object src ON c.Start_Object_ID = src.Object_ID
|
|
91
|
+
LEFT JOIN t_object dst ON c.End_Object_ID = dst.Object_ID
|
|
92
|
+
LEFT JOIN t_diagramlinks dl ON dl.ConnectorID = c.Connector_ID AND dl.DiagramID = ?
|
|
93
|
+
WHERE
|
|
94
|
+
dl.ConnectorID IS NOT NULL
|
|
95
|
+
OR (
|
|
96
|
+
c.Start_Object_ID IN (SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID = ?)
|
|
97
|
+
AND c.End_Object_ID IN (SELECT Object_ID FROM t_diagramobjects WHERE Diagram_ID = ?)
|
|
98
|
+
)
|
|
99
|
+
`).all(diagramId, diagramId, diagramId);
|
|
100
|
+
const connectors = connectorRows.map((r) => {
|
|
101
|
+
const { sourceFeature, targetFeature } = parseFeatureLinks(db, r.StyleEx);
|
|
102
|
+
return {
|
|
103
|
+
id: r.Connector_ID,
|
|
104
|
+
type: r.Connector_Type,
|
|
105
|
+
subType: r.SubType,
|
|
106
|
+
name: r.Name,
|
|
107
|
+
stereotype: r.Stereotype,
|
|
108
|
+
notes: decodeEntities(r.Notes),
|
|
109
|
+
sourceCard: r.SourceCard,
|
|
110
|
+
destCard: r.DestCard,
|
|
111
|
+
sourceRole: r.SourceRole || null,
|
|
112
|
+
destRole: r.DestRole || null,
|
|
113
|
+
hidden: r.Hidden === 1,
|
|
114
|
+
source: { id: r.Start_Object_ID, name: r.SourceName, type: r.SourceType },
|
|
115
|
+
dest: { id: r.End_Object_ID, name: r.DestName, type: r.DestType },
|
|
116
|
+
sourceFeature,
|
|
117
|
+
targetFeature,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
content: [{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: JSON.stringify({
|
|
124
|
+
diagram: {
|
|
125
|
+
id: diagram.Diagram_ID,
|
|
126
|
+
name: diagram.Name,
|
|
127
|
+
type: diagram.Diagram_Type,
|
|
128
|
+
packageId: diagram.Package_ID,
|
|
129
|
+
packageName: diagram.PackageName,
|
|
130
|
+
notes: decodeEntities(diagram.Notes),
|
|
131
|
+
},
|
|
132
|
+
elements: elementResults,
|
|
133
|
+
connectors,
|
|
134
|
+
_meta: {
|
|
135
|
+
sourceTables: ["t_diagram", "t_package", "t_diagramobjects", "t_object", "t_connector", "t_diagramlinks", "t_attribute", "t_operation"],
|
|
136
|
+
elements: { totalMatched: elementResults.length, returned: elementResults.length, truncated: false },
|
|
137
|
+
connectors: { totalMatched: connectors.length, returned: connectors.length, truncated: false },
|
|
138
|
+
},
|
|
139
|
+
}, null, 2),
|
|
140
|
+
}],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: "text", text: `Error retrieving diagram elements: ${msg}` }],
|
|
147
|
+
isError: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// R7: List and search diagrams
|
|
152
|
+
server.tool("ea_list_diagrams", "List diagrams in the model, optionally filtered by package, diagram type, and/or name substring. Each entry in `results` carries `diagramId`, `name`, `type`, `packagePath`, and `eaGuid`. Diagrams are ordered by the model's internal identity \u2014 stable but artificial, neither alphabetical nor the analyst's tree order \u2014 so adjacency carries no meaning. Walk a large result set with `offset` rather than a larger `limit`; while rows remain, `continuation` names the next call. When far more diagrams match than one window can hold, `breakdown` reports how many each type holds, so the next call can narrow by `diagramType` instead of paging.", {
|
|
153
|
+
packageId: z.coerce.number().optional().describe("Filter to diagrams in this package"),
|
|
154
|
+
diagramType: z.string().optional().describe("Filter by diagram type (e.g., Logical, Use Case, Sequence, Activity, Component)"),
|
|
155
|
+
nameContains: z.string().optional().describe("Filter to diagrams whose name contains this substring (case- and diacritic-insensitive across European Latin alphabets)"),
|
|
156
|
+
limit: limitParam(50),
|
|
157
|
+
offset: offsetParam,
|
|
158
|
+
}, READ_ONLY, async ({ packageId, diagramType, nameContains, limit, offset }) => {
|
|
159
|
+
const db = await model.database();
|
|
160
|
+
try {
|
|
161
|
+
if (packageId != null) {
|
|
162
|
+
const pkgExists = db.prepare("SELECT Package_ID FROM t_package WHERE Package_ID = ?").get(packageId);
|
|
163
|
+
if (!pkgExists) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Package with ID ${packageId} not found`, packageId }, null, 2) }],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let sql = "SELECT Diagram_ID, Name, Diagram_Type, Package_ID, ea_guid FROM t_diagram WHERE 1=1";
|
|
171
|
+
const params = [];
|
|
172
|
+
if (packageId != null) {
|
|
173
|
+
sql += " AND Package_ID = ?";
|
|
174
|
+
params.push(packageId);
|
|
175
|
+
}
|
|
176
|
+
if (diagramType) {
|
|
177
|
+
sql += " AND Diagram_Type = ?";
|
|
178
|
+
params.push(diagramType);
|
|
179
|
+
}
|
|
180
|
+
// nameContains folds text, which SQLite cannot do, so the window is applied
|
|
181
|
+
// in JS below; the ORDER BY is what makes that window repeatable.
|
|
182
|
+
sql += " ORDER BY Diagram_ID";
|
|
183
|
+
const allRows = db.prepare(sql).all(...params);
|
|
184
|
+
let filtered = allRows;
|
|
185
|
+
if (nameContains) {
|
|
186
|
+
const folded = foldText(nameContains);
|
|
187
|
+
filtered = allRows.filter((r) => foldText(r.Name || "").includes(folded));
|
|
188
|
+
}
|
|
189
|
+
const totalMatched = filtered.length;
|
|
190
|
+
const window = filtered.slice(offset, offset + limit);
|
|
191
|
+
const truncated = isTruncated(offset, window.length, totalMatched);
|
|
192
|
+
const results = window.map((r) => ({
|
|
193
|
+
diagramId: r.Diagram_ID,
|
|
194
|
+
name: r.Name,
|
|
195
|
+
type: r.Diagram_Type,
|
|
196
|
+
packagePath: buildPackagePath(db, r.Package_ID),
|
|
197
|
+
eaGuid: r.ea_guid,
|
|
198
|
+
}));
|
|
199
|
+
const breakdown = !diagramType && breakdownApplies(totalMatched, limit)
|
|
200
|
+
? buildBreakdown({ diagramType: countBy(filtered, (r) => r.Diagram_Type) })
|
|
201
|
+
: undefined;
|
|
202
|
+
const continuation = buildContinuation("ea_list_diagrams", { packageId, diagramType, nameContains, limit }, offset, results.length, totalMatched);
|
|
203
|
+
const response = {
|
|
204
|
+
results,
|
|
205
|
+
totalMatched,
|
|
206
|
+
returned: results.length,
|
|
207
|
+
offset,
|
|
208
|
+
truncated,
|
|
209
|
+
...(breakdown ? { breakdown } : {}),
|
|
210
|
+
...(continuation ? { continuation } : {}),
|
|
211
|
+
_meta: { sourceTables: ["t_diagram", "t_package"] },
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `Error listing diagrams: ${msg}` }],
|
|
221
|
+
isError: true,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { READ_ONLY } from "./annotations.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { buildPackagePath } from "../package-path.js";
|
|
4
|
+
import { decodeEntities } from "../text.js";
|
|
5
|
+
import { breakdownApplies, buildBreakdown, buildContinuation, isTruncated, limitParam, offsetParam } from "./windowing.js";
|
|
6
|
+
const MAX_INLINE_ITEMS = 50;
|
|
7
|
+
const formatMultiplicity = (a) => a.LowerBound && a.UpperBound ? `${a.LowerBound}..${a.UpperBound}` : undefined;
|
|
8
|
+
export function configureElementTools(server, model) {
|
|
9
|
+
server.tool("ea_get_element", "Get full details of an Enterprise Architect element by its ID, including its `Note`, `attributes`, `operations`, the `diagrams` it appears on, and its `constraints`. Attribute multiplicity supports a requiredness inference only when the element uses multiplicities contrastively; read `_meta.attributes.multiplicityIsUniform` before making that inference — when it is true the element's attributes carry no multiplicity contrast, so a value like 1..1 is not evidence of requiredness. Attributes and operations are capped inline: `attributesTruncated`/`operationsTruncated` say whether the returned list is partial, and `attributesTotal`/`operationsTotal` give the full counts, so never infer a count from the inline list alone.", {
|
|
10
|
+
elementId: z.coerce.number().describe("The Object_ID of the element to retrieve"),
|
|
11
|
+
}, READ_ONLY, async ({ elementId }) => {
|
|
12
|
+
const db = await model.database();
|
|
13
|
+
try {
|
|
14
|
+
const element = db.prepare(`
|
|
15
|
+
SELECT o.Object_ID, o.Object_Type, o.Name, o.Alias, o.Stereotype,
|
|
16
|
+
o.Package_ID, p.Name as PackageName, o.Note, o.Status,
|
|
17
|
+
o.Author, o.CreatedDate, o.ModifiedDate, o.Phase, o.Complexity
|
|
18
|
+
FROM t_object o
|
|
19
|
+
LEFT JOIN t_package p ON o.Package_ID = p.Package_ID
|
|
20
|
+
WHERE o.Object_ID = ?
|
|
21
|
+
`).get(elementId);
|
|
22
|
+
if (!element) {
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Element with ID ${elementId} not found`, elementId }, null, 2) }],
|
|
25
|
+
isError: true,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// Get attributes
|
|
29
|
+
const allAttributes = db.prepare(`
|
|
30
|
+
SELECT ID, Name, Type, Scope, Stereotype, Notes, LowerBound, UpperBound, "Default"
|
|
31
|
+
FROM t_attribute
|
|
32
|
+
WHERE Object_ID = ?
|
|
33
|
+
ORDER BY Pos
|
|
34
|
+
`).all(elementId);
|
|
35
|
+
const attributesTruncated = allAttributes.length > MAX_INLINE_ITEMS;
|
|
36
|
+
const attributes = allAttributes.slice(0, MAX_INLINE_ITEMS).map((a) => ({
|
|
37
|
+
id: a.ID,
|
|
38
|
+
name: a.Name,
|
|
39
|
+
type: a.Type,
|
|
40
|
+
scope: a.Scope,
|
|
41
|
+
stereotype: a.Stereotype,
|
|
42
|
+
notes: a.Notes,
|
|
43
|
+
multiplicity: formatMultiplicity(a),
|
|
44
|
+
default: a.Default,
|
|
45
|
+
}));
|
|
46
|
+
// Computed over every attribute, not the inline slice, so truncation cannot flip the flag.
|
|
47
|
+
const multiplicityIsUniform = new Set(allAttributes.map(formatMultiplicity).filter((m) => m !== undefined)).size < 2;
|
|
48
|
+
// Get operations
|
|
49
|
+
const allOperations = db.prepare(`
|
|
50
|
+
SELECT OperationID, Name, Type, Scope, Stereotype, Notes
|
|
51
|
+
FROM t_operation
|
|
52
|
+
WHERE Object_ID = ?
|
|
53
|
+
ORDER BY Pos
|
|
54
|
+
`).all(elementId);
|
|
55
|
+
const operationsTruncated = allOperations.length > MAX_INLINE_ITEMS;
|
|
56
|
+
const operations = allOperations.slice(0, MAX_INLINE_ITEMS).map((op) => {
|
|
57
|
+
const params = db.prepare(`
|
|
58
|
+
SELECT Name, Type, Kind, Notes
|
|
59
|
+
FROM t_operationparams
|
|
60
|
+
WHERE OperationID = ?
|
|
61
|
+
ORDER BY Pos
|
|
62
|
+
`).all(op.OperationID);
|
|
63
|
+
return {
|
|
64
|
+
id: op.OperationID,
|
|
65
|
+
name: op.Name,
|
|
66
|
+
returnType: op.Type,
|
|
67
|
+
scope: op.Scope,
|
|
68
|
+
stereotype: op.Stereotype,
|
|
69
|
+
notes: op.Notes,
|
|
70
|
+
parameters: params.map((p) => ({
|
|
71
|
+
name: p.Name,
|
|
72
|
+
type: p.Type,
|
|
73
|
+
kind: p.Kind,
|
|
74
|
+
notes: p.Notes,
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
// R4: Diagrams this element appears on
|
|
79
|
+
const diagramRows = db.prepare(`
|
|
80
|
+
SELECT d.Diagram_ID, d.Name, d.Diagram_Type, d.Package_ID
|
|
81
|
+
FROM t_diagramobjects do_
|
|
82
|
+
JOIN t_diagram d ON do_.Diagram_ID = d.Diagram_ID
|
|
83
|
+
WHERE do_.Object_ID = ?
|
|
84
|
+
`).all(elementId);
|
|
85
|
+
const diagrams = diagramRows.map((d) => ({
|
|
86
|
+
diagramId: d.Diagram_ID,
|
|
87
|
+
name: d.Name,
|
|
88
|
+
type: d.Diagram_Type,
|
|
89
|
+
packagePath: buildPackagePath(db, d.Package_ID),
|
|
90
|
+
}));
|
|
91
|
+
// R10: Constraints (pre-conditions, post-conditions, invariants, process rules)
|
|
92
|
+
const constraintRows = db.prepare(`
|
|
93
|
+
SELECT "Constraint" as name, ConstraintType, Notes, Status
|
|
94
|
+
FROM t_objectconstraint
|
|
95
|
+
WHERE Object_ID = ?
|
|
96
|
+
ORDER BY ConstraintType, "Constraint"
|
|
97
|
+
`).all(elementId);
|
|
98
|
+
const constraints = constraintRows.map((c) => ({
|
|
99
|
+
name: c.name,
|
|
100
|
+
type: c.ConstraintType,
|
|
101
|
+
notes: decodeEntities(c.Notes),
|
|
102
|
+
status: c.Status || null,
|
|
103
|
+
}));
|
|
104
|
+
const result = {
|
|
105
|
+
...element,
|
|
106
|
+
Note: decodeEntities(element.Note),
|
|
107
|
+
attributes,
|
|
108
|
+
attributesTruncated,
|
|
109
|
+
attributesTotal: allAttributes.length,
|
|
110
|
+
operations,
|
|
111
|
+
operationsTruncated,
|
|
112
|
+
operationsTotal: allOperations.length,
|
|
113
|
+
diagrams,
|
|
114
|
+
constraints,
|
|
115
|
+
_meta: {
|
|
116
|
+
sourceTables: ["t_object", "t_package", "t_attribute", "t_operation", "t_operationparams", "t_diagramobjects", "t_diagram", "t_objectconstraint"],
|
|
117
|
+
attributes: { totalMatched: allAttributes.length, returned: attributes.length, truncated: attributesTruncated, multiplicityIsUniform },
|
|
118
|
+
operations: { totalMatched: allOperations.length, returned: operations.length, truncated: operationsTruncated },
|
|
119
|
+
diagrams: { totalMatched: diagramRows.length, returned: diagrams.length, truncated: false },
|
|
120
|
+
constraints: { totalMatched: constraintRows.length, returned: constraints.length, truncated: false },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: `Error retrieving element: ${msg}` }],
|
|
131
|
+
isError: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
server.tool("ea_list_elements", "List elements within a package, optionally filtered by object type. `elements` is a lightweight list (ID, type, name, alias, stereotype), grouped by element type and then ordered by the model's internal identity. That order is stable but artificial \u2014 neither alphabetical nor the analyst's tree order \u2014 so adjacency carries no meaning. Walk a large package with `offset` rather than a larger `limit`; while rows remain, `continuation` names the next call. When far more elements match than one window can hold, `breakdown` reports how many each type holds, so the next call can narrow by `objectType` instead of paging.", {
|
|
136
|
+
packageId: z.coerce.number().describe("The Package_ID to list elements from"),
|
|
137
|
+
objectType: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Filter by object type (e.g., Class, UseCase, Activity, Screen)"),
|
|
141
|
+
limit: limitParam(50),
|
|
142
|
+
offset: offsetParam,
|
|
143
|
+
}, READ_ONLY, async ({ packageId, objectType, limit, offset }) => {
|
|
144
|
+
const db = await model.database();
|
|
145
|
+
try {
|
|
146
|
+
// Verify package exists
|
|
147
|
+
const pkgExists = db.prepare("SELECT Package_ID FROM t_package WHERE Package_ID = ?").get(packageId);
|
|
148
|
+
if (!pkgExists) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: JSON.stringify({ error: "not_found", message: `Package with ID ${packageId} not found`, packageId }, null, 2) }],
|
|
151
|
+
isError: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
let sql = `
|
|
155
|
+
SELECT Object_ID, Object_Type, Name, Alias, Stereotype
|
|
156
|
+
FROM t_object
|
|
157
|
+
WHERE Package_ID = ?
|
|
158
|
+
`;
|
|
159
|
+
const params = [packageId];
|
|
160
|
+
if (objectType) {
|
|
161
|
+
sql += " AND Object_Type = ?";
|
|
162
|
+
params.push(objectType);
|
|
163
|
+
}
|
|
164
|
+
// Identity, not Name: SQLite's binary collation sorts every accented initial
|
|
165
|
+
// past Z, which systematically exiles them from a truncated window.
|
|
166
|
+
sql += " ORDER BY Object_Type, Object_ID LIMIT ? OFFSET ?";
|
|
167
|
+
params.push(limit, offset);
|
|
168
|
+
const rows = db.prepare(sql).all(...params);
|
|
169
|
+
// Count total without limit for truncation reporting
|
|
170
|
+
let countSql = "SELECT COUNT(*) as cnt FROM t_object WHERE Package_ID = ?";
|
|
171
|
+
const countParams = [packageId];
|
|
172
|
+
if (objectType) {
|
|
173
|
+
countSql += " AND Object_Type = ?";
|
|
174
|
+
countParams.push(objectType);
|
|
175
|
+
}
|
|
176
|
+
const totalMatched = db.prepare(countSql).get(...countParams).cnt;
|
|
177
|
+
const truncated = isTruncated(offset, rows.length, totalMatched);
|
|
178
|
+
let breakdown;
|
|
179
|
+
if (!objectType && breakdownApplies(totalMatched, limit)) {
|
|
180
|
+
const typeRows = db
|
|
181
|
+
.prepare("SELECT Object_Type, COUNT(*) as cnt FROM t_object WHERE Package_ID = ? GROUP BY Object_Type")
|
|
182
|
+
.all(packageId);
|
|
183
|
+
breakdown = buildBreakdown({
|
|
184
|
+
objectType: new Map(typeRows.filter((r) => r.Object_Type).map((r) => [String(r.Object_Type), r.cnt])),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const continuation = buildContinuation("ea_list_elements", { packageId, objectType, limit }, offset, rows.length, totalMatched);
|
|
188
|
+
const response = {
|
|
189
|
+
elements: rows,
|
|
190
|
+
totalMatched,
|
|
191
|
+
returned: rows.length,
|
|
192
|
+
offset,
|
|
193
|
+
truncated,
|
|
194
|
+
...(breakdown ? { breakdown } : {}),
|
|
195
|
+
...(continuation ? { continuation } : {}),
|
|
196
|
+
_meta: { sourceTables: ["t_object"] },
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: `Error listing elements: ${msg}` }],
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|