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.
Files changed (41) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +226 -0
  3. package/dist/database.d.ts +3 -0
  4. package/dist/database.js +25 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +68 -0
  7. package/dist/model-session.d.ts +29 -0
  8. package/dist/model-session.js +172 -0
  9. package/dist/package-path.d.ts +2 -0
  10. package/dist/package-path.js +36 -0
  11. package/dist/remembered-path.d.ts +5 -0
  12. package/dist/remembered-path.js +43 -0
  13. package/dist/resolve-qea-path.d.ts +26 -0
  14. package/dist/resolve-qea-path.js +58 -0
  15. package/dist/text.d.ts +37 -0
  16. package/dist/text.js +152 -0
  17. package/dist/tools/annotations.d.ts +3 -0
  18. package/dist/tools/annotations.js +5 -0
  19. package/dist/tools/connectors.d.ts +3 -0
  20. package/dist/tools/connectors.js +152 -0
  21. package/dist/tools/diagrams.d.ts +3 -0
  22. package/dist/tools/diagrams.js +225 -0
  23. package/dist/tools/elements.d.ts +3 -0
  24. package/dist/tools/elements.js +210 -0
  25. package/dist/tools/packages.d.ts +3 -0
  26. package/dist/tools/packages.js +80 -0
  27. package/dist/tools/resolve.d.ts +3 -0
  28. package/dist/tools/resolve.js +135 -0
  29. package/dist/tools/scenarios.d.ts +3 -0
  30. package/dist/tools/scenarios.js +106 -0
  31. package/dist/tools/schema.d.ts +3 -0
  32. package/dist/tools/schema.js +164 -0
  33. package/dist/tools/search.d.ts +3 -0
  34. package/dist/tools/search.js +217 -0
  35. package/dist/tools/windowing.d.ts +37 -0
  36. package/dist/tools/windowing.js +92 -0
  37. package/dist/tools.d.ts +3 -0
  38. package/dist/tools.js +18 -0
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +1 -0
  41. 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,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ModelAccess } from "../model-session.js";
3
+ export declare function configureDiagramTools(server: McpServer, model: ModelAccess): void;
@@ -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,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ModelAccess } from "../model-session.js";
3
+ export declare function configureElementTools(server: McpServer, model: ModelAccess): void;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ModelAccess } from "../model-session.js";
3
+ export declare function configurePackageTools(server: McpServer, model: ModelAccess): void;