@eclipse-glsp/server-mcp 2.7.0-next.10

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 (278) hide show
  1. package/LICENSE +642 -0
  2. package/README.md +57 -0
  3. package/lib/index.d.ts +66 -0
  4. package/lib/index.d.ts.map +1 -0
  5. package/lib/index.js +82 -0
  6. package/lib/index.js.map +1 -0
  7. package/lib/modules/mcp-diagram-module.d.ts +123 -0
  8. package/lib/modules/mcp-diagram-module.d.ts.map +1 -0
  9. package/lib/modules/mcp-diagram-module.js +202 -0
  10. package/lib/modules/mcp-diagram-module.js.map +1 -0
  11. package/lib/modules/mcp-server-module.d.ts +143 -0
  12. package/lib/modules/mcp-server-module.d.ts.map +1 -0
  13. package/lib/modules/mcp-server-module.js +251 -0
  14. package/lib/modules/mcp-server-module.js.map +1 -0
  15. package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.d.ts +44 -0
  16. package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.d.ts.map +1 -0
  17. package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.js +102 -0
  18. package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.js.map +1 -0
  19. package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.d.ts +44 -0
  20. package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.d.ts.map +1 -0
  21. package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.js +99 -0
  22. package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.js.map +1 -0
  23. package/lib/resources/handlers/diagram-png-mcp-resource-handler.d.ts +84 -0
  24. package/lib/resources/handlers/diagram-png-mcp-resource-handler.d.ts.map +1 -0
  25. package/lib/resources/handlers/diagram-png-mcp-resource-handler.js +177 -0
  26. package/lib/resources/handlers/diagram-png-mcp-resource-handler.js.map +1 -0
  27. package/lib/resources/handlers/diagram-svg-mcp-resource-handler.d.ts +55 -0
  28. package/lib/resources/handlers/diagram-svg-mcp-resource-handler.d.ts.map +1 -0
  29. package/lib/resources/handlers/diagram-svg-mcp-resource-handler.js +99 -0
  30. package/lib/resources/handlers/diagram-svg-mcp-resource-handler.js.map +1 -0
  31. package/lib/resources/services/element-types-provider.d.ts +65 -0
  32. package/lib/resources/services/element-types-provider.d.ts.map +1 -0
  33. package/lib/resources/services/element-types-provider.js +81 -0
  34. package/lib/resources/services/element-types-provider.js.map +1 -0
  35. package/lib/resources/services/mcp-model-serializer.d.ts +78 -0
  36. package/lib/resources/services/mcp-model-serializer.d.ts.map +1 -0
  37. package/lib/resources/services/mcp-model-serializer.js +188 -0
  38. package/lib/resources/services/mcp-model-serializer.js.map +1 -0
  39. package/lib/server/glsp-mcp-server.d.ts +82 -0
  40. package/lib/server/glsp-mcp-server.d.ts.map +1 -0
  41. package/lib/server/glsp-mcp-server.js +140 -0
  42. package/lib/server/glsp-mcp-server.js.map +1 -0
  43. package/lib/server/lru-event-store.d.ts +53 -0
  44. package/lib/server/lru-event-store.d.ts.map +1 -0
  45. package/lib/server/lru-event-store.js +100 -0
  46. package/lib/server/lru-event-store.js.map +1 -0
  47. package/lib/server/mcp-diagram-handler-dispatcher.d.ts +144 -0
  48. package/lib/server/mcp-diagram-handler-dispatcher.d.ts.map +1 -0
  49. package/lib/server/mcp-diagram-handler-dispatcher.js +382 -0
  50. package/lib/server/mcp-diagram-handler-dispatcher.js.map +1 -0
  51. package/lib/server/mcp-diagram-prompt-handler-registry.d.ts +33 -0
  52. package/lib/server/mcp-diagram-prompt-handler-registry.d.ts.map +1 -0
  53. package/lib/server/mcp-diagram-prompt-handler-registry.js +76 -0
  54. package/lib/server/mcp-diagram-prompt-handler-registry.js.map +1 -0
  55. package/lib/server/mcp-diagram-resource-handler-registry.d.ts +35 -0
  56. package/lib/server/mcp-diagram-resource-handler-registry.d.ts.map +1 -0
  57. package/lib/server/mcp-diagram-resource-handler-registry.js +94 -0
  58. package/lib/server/mcp-diagram-resource-handler-registry.js.map +1 -0
  59. package/lib/server/mcp-diagram-tool-handler-registry.d.ts +57 -0
  60. package/lib/server/mcp-diagram-tool-handler-registry.d.ts.map +1 -0
  61. package/lib/server/mcp-diagram-tool-handler-registry.js +111 -0
  62. package/lib/server/mcp-diagram-tool-handler-registry.js.map +1 -0
  63. package/lib/server/mcp-handler-shared.d.ts +142 -0
  64. package/lib/server/mcp-handler-shared.d.ts.map +1 -0
  65. package/lib/server/mcp-handler-shared.js +199 -0
  66. package/lib/server/mcp-handler-shared.js.map +1 -0
  67. package/lib/server/mcp-http-transport.d.ts +93 -0
  68. package/lib/server/mcp-http-transport.d.ts.map +1 -0
  69. package/lib/server/mcp-http-transport.js +350 -0
  70. package/lib/server/mcp-http-transport.js.map +1 -0
  71. package/lib/server/mcp-id-alias-service.d.ts +70 -0
  72. package/lib/server/mcp-id-alias-service.d.ts.map +1 -0
  73. package/lib/server/mcp-id-alias-service.js +85 -0
  74. package/lib/server/mcp-id-alias-service.js.map +1 -0
  75. package/lib/server/mcp-input-schemas.d.ts +73 -0
  76. package/lib/server/mcp-input-schemas.d.ts.map +1 -0
  77. package/lib/server/mcp-input-schemas.js +67 -0
  78. package/lib/server/mcp-input-schemas.js.map +1 -0
  79. package/lib/server/mcp-label-provider.d.ts +45 -0
  80. package/lib/server/mcp-label-provider.d.ts.map +1 -0
  81. package/lib/server/mcp-label-provider.js +42 -0
  82. package/lib/server/mcp-label-provider.js.map +1 -0
  83. package/lib/server/mcp-log-level-registry.d.ts +54 -0
  84. package/lib/server/mcp-log-level-registry.d.ts.map +1 -0
  85. package/lib/server/mcp-log-level-registry.js +80 -0
  86. package/lib/server/mcp-log-level-registry.js.map +1 -0
  87. package/lib/server/mcp-logger.d.ts +59 -0
  88. package/lib/server/mcp-logger.d.ts.map +1 -0
  89. package/lib/server/mcp-logger.js +104 -0
  90. package/lib/server/mcp-logger.js.map +1 -0
  91. package/lib/server/mcp-mime-types.d.ts +28 -0
  92. package/lib/server/mcp-mime-types.d.ts.map +1 -0
  93. package/lib/server/mcp-mime-types.js +18 -0
  94. package/lib/server/mcp-mime-types.js.map +1 -0
  95. package/lib/server/mcp-options.d.ts +39 -0
  96. package/lib/server/mcp-options.d.ts.map +1 -0
  97. package/lib/server/mcp-options.js +53 -0
  98. package/lib/server/mcp-options.js.map +1 -0
  99. package/lib/server/mcp-progress-reporter.d.ts +48 -0
  100. package/lib/server/mcp-progress-reporter.d.ts.map +1 -0
  101. package/lib/server/mcp-progress-reporter.js +66 -0
  102. package/lib/server/mcp-progress-reporter.js.map +1 -0
  103. package/lib/server/mcp-prompt-handler.d.ts +120 -0
  104. package/lib/server/mcp-prompt-handler.d.ts.map +1 -0
  105. package/lib/server/mcp-prompt-handler.js +131 -0
  106. package/lib/server/mcp-prompt-handler.js.map +1 -0
  107. package/lib/server/mcp-request-context.d.ts +37 -0
  108. package/lib/server/mcp-request-context.d.ts.map +1 -0
  109. package/lib/server/mcp-request-context.js +37 -0
  110. package/lib/server/mcp-request-context.js.map +1 -0
  111. package/lib/server/mcp-resource-handler.d.ts +212 -0
  112. package/lib/server/mcp-resource-handler.d.ts.map +1 -0
  113. package/lib/server/mcp-resource-handler.js +298 -0
  114. package/lib/server/mcp-resource-handler.js.map +1 -0
  115. package/lib/server/mcp-server-launcher.d.ts +143 -0
  116. package/lib/server/mcp-server-launcher.d.ts.map +1 -0
  117. package/lib/server/mcp-server-launcher.js +355 -0
  118. package/lib/server/mcp-server-launcher.js.map +1 -0
  119. package/lib/server/mcp-session.d.ts +44 -0
  120. package/lib/server/mcp-session.d.ts.map +1 -0
  121. package/lib/server/mcp-session.js +18 -0
  122. package/lib/server/mcp-session.js.map +1 -0
  123. package/lib/server/mcp-tool-handler.d.ts +259 -0
  124. package/lib/server/mcp-tool-handler.d.ts.map +1 -0
  125. package/lib/server/mcp-tool-handler.js +355 -0
  126. package/lib/server/mcp-tool-handler.js.map +1 -0
  127. package/lib/tools/handlers/count-elements-mcp-tool-handler.d.ts +47 -0
  128. package/lib/tools/handlers/count-elements-mcp-tool-handler.d.ts.map +1 -0
  129. package/lib/tools/handlers/count-elements-mcp-tool-handler.js +77 -0
  130. package/lib/tools/handlers/count-elements-mcp-tool-handler.js.map +1 -0
  131. package/lib/tools/handlers/create-edges-mcp-tool-handler.d.ts +113 -0
  132. package/lib/tools/handlers/create-edges-mcp-tool-handler.d.ts.map +1 -0
  133. package/lib/tools/handlers/create-edges-mcp-tool-handler.js +191 -0
  134. package/lib/tools/handlers/create-edges-mcp-tool-handler.js.map +1 -0
  135. package/lib/tools/handlers/create-nodes-mcp-tool-handler.d.ts +82 -0
  136. package/lib/tools/handlers/create-nodes-mcp-tool-handler.d.ts.map +1 -0
  137. package/lib/tools/handlers/create-nodes-mcp-tool-handler.js +124 -0
  138. package/lib/tools/handlers/create-nodes-mcp-tool-handler.js.map +1 -0
  139. package/lib/tools/handlers/delete-elements-mcp-tool-handler.d.ts +53 -0
  140. package/lib/tools/handlers/delete-elements-mcp-tool-handler.d.ts.map +1 -0
  141. package/lib/tools/handlers/delete-elements-mcp-tool-handler.js +74 -0
  142. package/lib/tools/handlers/delete-elements-mcp-tool-handler.js.map +1 -0
  143. package/lib/tools/handlers/diagram-model-mcp-tool-handler.d.ts +60 -0
  144. package/lib/tools/handlers/diagram-model-mcp-tool-handler.d.ts.map +1 -0
  145. package/lib/tools/handlers/diagram-model-mcp-tool-handler.js +79 -0
  146. package/lib/tools/handlers/diagram-model-mcp-tool-handler.js.map +1 -0
  147. package/lib/tools/handlers/element-types-mcp-tool-handler.d.ts +98 -0
  148. package/lib/tools/handlers/element-types-mcp-tool-handler.d.ts.map +1 -0
  149. package/lib/tools/handlers/element-types-mcp-tool-handler.js +156 -0
  150. package/lib/tools/handlers/element-types-mcp-tool-handler.js.map +1 -0
  151. package/lib/tools/handlers/get-selection-mcp-tool-handler.d.ts +44 -0
  152. package/lib/tools/handlers/get-selection-mcp-tool-handler.d.ts.map +1 -0
  153. package/lib/tools/handlers/get-selection-mcp-tool-handler.js +70 -0
  154. package/lib/tools/handlers/get-selection-mcp-tool-handler.js.map +1 -0
  155. package/lib/tools/handlers/layout-mcp-tool-handler.d.ts +44 -0
  156. package/lib/tools/handlers/layout-mcp-tool-handler.d.ts.map +1 -0
  157. package/lib/tools/handlers/layout-mcp-tool-handler.js +72 -0
  158. package/lib/tools/handlers/layout-mcp-tool-handler.js.map +1 -0
  159. package/lib/tools/handlers/modify-edges-mcp-tool-handler.d.ts +79 -0
  160. package/lib/tools/handlers/modify-edges-mcp-tool-handler.d.ts.map +1 -0
  161. package/lib/tools/handlers/modify-edges-mcp-tool-handler.js +138 -0
  162. package/lib/tools/handlers/modify-edges-mcp-tool-handler.js.map +1 -0
  163. package/lib/tools/handlers/modify-nodes-mcp-tool-handler.d.ts +93 -0
  164. package/lib/tools/handlers/modify-nodes-mcp-tool-handler.d.ts.map +1 -0
  165. package/lib/tools/handlers/modify-nodes-mcp-tool-handler.js +127 -0
  166. package/lib/tools/handlers/modify-nodes-mcp-tool-handler.js.map +1 -0
  167. package/lib/tools/handlers/query-elements-mcp-tool-handler.d.ts +103 -0
  168. package/lib/tools/handlers/query-elements-mcp-tool-handler.d.ts.map +1 -0
  169. package/lib/tools/handlers/query-elements-mcp-tool-handler.js +160 -0
  170. package/lib/tools/handlers/query-elements-mcp-tool-handler.js.map +1 -0
  171. package/lib/tools/handlers/redo-mcp-tool-handler.d.ts +46 -0
  172. package/lib/tools/handlers/redo-mcp-tool-handler.d.ts.map +1 -0
  173. package/lib/tools/handlers/redo-mcp-tool-handler.js +75 -0
  174. package/lib/tools/handlers/redo-mcp-tool-handler.js.map +1 -0
  175. package/lib/tools/handlers/save-model-mcp-tool-handler.d.ts +57 -0
  176. package/lib/tools/handlers/save-model-mcp-tool-handler.d.ts.map +1 -0
  177. package/lib/tools/handlers/save-model-mcp-tool-handler.js +93 -0
  178. package/lib/tools/handlers/save-model-mcp-tool-handler.js.map +1 -0
  179. package/lib/tools/handlers/session-info-mcp-tool-handler.d.ts +66 -0
  180. package/lib/tools/handlers/session-info-mcp-tool-handler.d.ts.map +1 -0
  181. package/lib/tools/handlers/session-info-mcp-tool-handler.js +109 -0
  182. package/lib/tools/handlers/session-info-mcp-tool-handler.js.map +1 -0
  183. package/lib/tools/handlers/set-selection-mcp-tool-handler.d.ts +61 -0
  184. package/lib/tools/handlers/set-selection-mcp-tool-handler.d.ts.map +1 -0
  185. package/lib/tools/handlers/set-selection-mcp-tool-handler.js +104 -0
  186. package/lib/tools/handlers/set-selection-mcp-tool-handler.js.map +1 -0
  187. package/lib/tools/handlers/set-view-mcp-tool-handler.d.ts +111 -0
  188. package/lib/tools/handlers/set-view-mcp-tool-handler.d.ts.map +1 -0
  189. package/lib/tools/handlers/set-view-mcp-tool-handler.js +144 -0
  190. package/lib/tools/handlers/set-view-mcp-tool-handler.js.map +1 -0
  191. package/lib/tools/handlers/undo-mcp-tool-handler.d.ts +46 -0
  192. package/lib/tools/handlers/undo-mcp-tool-handler.d.ts.map +1 -0
  193. package/lib/tools/handlers/undo-mcp-tool-handler.js +76 -0
  194. package/lib/tools/handlers/undo-mcp-tool-handler.js.map +1 -0
  195. package/lib/tools/handlers/validate-diagram-mcp-tool-handler.d.ts +67 -0
  196. package/lib/tools/handlers/validate-diagram-mcp-tool-handler.d.ts.map +1 -0
  197. package/lib/tools/handlers/validate-diagram-mcp-tool-handler.js +0 -0
  198. package/lib/tools/handlers/validate-diagram-mcp-tool-handler.js.map +1 -0
  199. package/lib/util/markdown-util.d.ts +20 -0
  200. package/lib/util/markdown-util.d.ts.map +1 -0
  201. package/lib/util/markdown-util.js +45 -0
  202. package/lib/util/markdown-util.js.map +1 -0
  203. package/lib/util/mcp-util.d.ts +22 -0
  204. package/lib/util/mcp-util.d.ts.map +1 -0
  205. package/lib/util/mcp-util.js +29 -0
  206. package/lib/util/mcp-util.js.map +1 -0
  207. package/package.json +63 -0
  208. package/src/index.ts +65 -0
  209. package/src/modules/mcp-diagram-module.ts +247 -0
  210. package/src/modules/mcp-server-module.ts +289 -0
  211. package/src/prompts/handlers/describe-diagram-mcp-prompt-handler.ts +88 -0
  212. package/src/prompts/handlers/suggest-improvements-mcp-prompt-handler.ts +85 -0
  213. package/src/resources/handlers/diagram-png-mcp-resource-handler.ts +177 -0
  214. package/src/resources/handlers/diagram-svg-mcp-resource-handler.ts +85 -0
  215. package/src/resources/services/element-types-provider.ts +105 -0
  216. package/src/resources/services/mcp-model-serializer.ts +211 -0
  217. package/src/server/glsp-mcp-server.spec.ts +73 -0
  218. package/src/server/glsp-mcp-server.ts +196 -0
  219. package/src/server/lru-event-store.spec.ts +121 -0
  220. package/src/server/lru-event-store.ts +112 -0
  221. package/src/server/mcp-diagram-handler-dispatcher.spec.ts +231 -0
  222. package/src/server/mcp-diagram-handler-dispatcher.ts +459 -0
  223. package/src/server/mcp-diagram-prompt-handler-registry.ts +59 -0
  224. package/src/server/mcp-diagram-resource-handler-registry.ts +73 -0
  225. package/src/server/mcp-diagram-tool-handler-registry.ts +97 -0
  226. package/src/server/mcp-handler-shared.spec.ts +53 -0
  227. package/src/server/mcp-handler-shared.ts +247 -0
  228. package/src/server/mcp-http-transport-e2e.spec.ts +151 -0
  229. package/src/server/mcp-http-transport.spec.ts +385 -0
  230. package/src/server/mcp-http-transport.ts +368 -0
  231. package/src/server/mcp-id-alias-service.spec.ts +106 -0
  232. package/src/server/mcp-id-alias-service.ts +104 -0
  233. package/src/server/mcp-input-schemas.ts +82 -0
  234. package/src/server/mcp-label-provider.ts +52 -0
  235. package/src/server/mcp-log-level-registry.spec.ts +75 -0
  236. package/src/server/mcp-log-level-registry.ts +90 -0
  237. package/src/server/mcp-logger.spec.ts +227 -0
  238. package/src/server/mcp-logger.ts +91 -0
  239. package/src/server/mcp-mime-types.ts +31 -0
  240. package/src/server/mcp-options.ts +43 -0
  241. package/src/server/mcp-progress-reporter.spec.ts +93 -0
  242. package/src/server/mcp-progress-reporter.ts +67 -0
  243. package/src/server/mcp-prompt-handler.ts +157 -0
  244. package/src/server/mcp-request-context.ts +39 -0
  245. package/src/server/mcp-resource-handler.ts +389 -0
  246. package/src/server/mcp-server-launcher.spec.ts +173 -0
  247. package/src/server/mcp-server-launcher.ts +369 -0
  248. package/src/server/mcp-session.ts +45 -0
  249. package/src/server/mcp-tool-handler.spec.ts +182 -0
  250. package/src/server/mcp-tool-handler.ts +431 -0
  251. package/src/server/raw-http.spec.ts +59 -0
  252. package/src/tools/handlers/count-elements-mcp-tool-handler.spec.ts +101 -0
  253. package/src/tools/handlers/count-elements-mcp-tool-handler.ts +68 -0
  254. package/src/tools/handlers/create-edges-mcp-tool-handler.spec.ts +198 -0
  255. package/src/tools/handlers/create-edges-mcp-tool-handler.ts +200 -0
  256. package/src/tools/handlers/create-nodes-mcp-tool-handler.spec.ts +199 -0
  257. package/src/tools/handlers/create-nodes-mcp-tool-handler.ts +126 -0
  258. package/src/tools/handlers/delete-elements-mcp-tool-handler.ts +69 -0
  259. package/src/tools/handlers/diagram-model-mcp-tool-handler.ts +68 -0
  260. package/src/tools/handlers/element-types-mcp-tool-handler.ts +152 -0
  261. package/src/tools/handlers/get-selection-mcp-tool-handler.ts +56 -0
  262. package/src/tools/handlers/layout-mcp-tool-handler.ts +58 -0
  263. package/src/tools/handlers/modify-edges-mcp-tool-handler.ts +142 -0
  264. package/src/tools/handlers/modify-nodes-mcp-tool-handler.ts +134 -0
  265. package/src/tools/handlers/query-elements-mcp-tool-handler.spec.ts +212 -0
  266. package/src/tools/handlers/query-elements-mcp-tool-handler.ts +157 -0
  267. package/src/tools/handlers/redo-mcp-tool-handler.ts +64 -0
  268. package/src/tools/handlers/save-model-mcp-tool-handler.ts +74 -0
  269. package/src/tools/handlers/session-info-mcp-tool-handler.spec.ts +152 -0
  270. package/src/tools/handlers/session-info-mcp-tool-handler.ts +98 -0
  271. package/src/tools/handlers/set-selection-mcp-tool-handler.spec.ts +120 -0
  272. package/src/tools/handlers/set-selection-mcp-tool-handler.ts +92 -0
  273. package/src/tools/handlers/set-view-mcp-tool-handler.ts +156 -0
  274. package/src/tools/handlers/undo-mcp-tool-handler.ts +63 -0
  275. package/src/tools/handlers/validate-diagram-mcp-tool-handler.ts +0 -0
  276. package/src/tools/tool-annotations.spec.ts +141 -0
  277. package/src/util/markdown-util.ts +44 -0
  278. package/src/util/mcp-util.ts +25 -0
@@ -0,0 +1,198 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import {
18
+ Action,
19
+ ActionDispatcher,
20
+ ClientId,
21
+ DiagramConfiguration,
22
+ EdgeCreationChecker,
23
+ EdgeTypeHint,
24
+ GModelElement,
25
+ Logger,
26
+ ModelState,
27
+ NullLogger
28
+ } from '@eclipse-glsp/server';
29
+ import { expect } from 'chai';
30
+ import { Container, ContainerModule } from 'inversify';
31
+ import { McpToolResult } from '../../server/mcp-handler-shared';
32
+ import { McpIdAliasService } from '../../server/mcp-id-alias-service';
33
+ import { DefaultMcpLabelProvider, McpLabelProvider } from '../../server/mcp-label-provider';
34
+ import { CreateEdgesInput, CreateEdgesMcpToolHandler } from './create-edges-mcp-tool-handler';
35
+
36
+ function makeElement(id: string, type: string): GModelElement {
37
+ return { id, type, children: [] } as unknown as GModelElement;
38
+ }
39
+
40
+ function makeModelState(elements: GModelElement[]): ModelState {
41
+ const byId = new Map(elements.map(el => [el.id, el]));
42
+ return {
43
+ index: {
44
+ allIds: () => [...byId.keys()],
45
+ get: (id: string) => byId.get(id),
46
+ find: (id: string) => byId.get(id)
47
+ },
48
+ isReadonly: false
49
+ } as unknown as ModelState;
50
+ }
51
+
52
+ class StubEdgeCreationChecker implements EdgeCreationChecker {
53
+ public lastTargetCall?: { edgeType: string; source: GModelElement; target: GModelElement };
54
+ constructor(private readonly result: boolean) {}
55
+ isValidSource(): boolean {
56
+ return this.result;
57
+ }
58
+ isValidTarget(edgeType: string, source: GModelElement, target: GModelElement): boolean {
59
+ this.lastTargetCall = { edgeType, source, target };
60
+ return this.result;
61
+ }
62
+ }
63
+
64
+ interface BuildArgs {
65
+ elements: GModelElement[];
66
+ edgeTypeHints: Pick<EdgeTypeHint, 'elementTypeId' | 'dynamic'>[];
67
+ checker?: EdgeCreationChecker;
68
+ }
69
+
70
+ /** Records dispatched actions so create-mode tests can assert no dispatch happened. */
71
+ class RecordingDispatcher {
72
+ public dispatched: Action[] = [];
73
+ async dispatch(action: Action): Promise<void> {
74
+ this.dispatched.push(action);
75
+ }
76
+ async dispatchAll(): Promise<void> {
77
+ // not used by create-edges
78
+ }
79
+ dispatchAfterNextUpdate(): void {
80
+ // not used
81
+ }
82
+ }
83
+
84
+ function buildHandler({ elements, edgeTypeHints, checker }: BuildArgs): {
85
+ handler: CreateEdgesMcpToolHandler;
86
+ dispatcher: RecordingDispatcher;
87
+ } {
88
+ const dispatcher = new RecordingDispatcher();
89
+ const container = new Container();
90
+ container.load(
91
+ new ContainerModule(bind => {
92
+ bind(Logger).toConstantValue(new NullLogger());
93
+ bind(ClientId).toConstantValue('test-session');
94
+ bind(ModelState).toConstantValue(makeModelState(elements));
95
+ bind(McpIdAliasService).toConstantValue({
96
+ lookup: (id: string) => id,
97
+ alias: (id: string) => id
98
+ } as McpIdAliasService);
99
+ bind(DiagramConfiguration).toConstantValue({ edgeTypeHints } as unknown as DiagramConfiguration);
100
+ bind(ActionDispatcher).toConstantValue(dispatcher as unknown as ActionDispatcher);
101
+ if (checker) {
102
+ bind(EdgeCreationChecker).toConstantValue(checker);
103
+ }
104
+ bind(McpLabelProvider).to(DefaultMcpLabelProvider);
105
+ bind(CreateEdgesMcpToolHandler).toSelf();
106
+ })
107
+ );
108
+ return { handler: container.get(CreateEdgesMcpToolHandler), dispatcher };
109
+ }
110
+
111
+ function callCreateResult(handler: CreateEdgesMcpToolHandler, params: CreateEdgesInput): Promise<McpToolResult> {
112
+ return (handler as unknown as { createResult: (p: CreateEdgesInput) => Promise<McpToolResult> }).createResult(params);
113
+ }
114
+
115
+ interface DryRunStructured {
116
+ createdEdges: { id: string; elementTypeId: string; label?: string }[];
117
+ errors: string[];
118
+ validationResults: { edgeType: string; sourceElementId: string; targetElementId: string; isValid: boolean; reason?: string }[];
119
+ }
120
+
121
+ describe('CreateEdgesMcpToolHandler · dryRun', () => {
122
+ const baseInput = (overrides: Partial<CreateEdgesInput> = {}): CreateEdgesInput => ({
123
+ sessionId: 's',
124
+ dryRun: true,
125
+ edges: [{ elementTypeId: 'edge:dynamic', sourceElementId: 's', targetElementId: 't' }],
126
+ ...overrides
127
+ });
128
+
129
+ it('returns isValid:true with a "no dynamic hint" reason when the edgeType has no dynamic hint', async () => {
130
+ const { handler, dispatcher } = buildHandler({
131
+ elements: [makeElement('s', 'task'), makeElement('t', 'task')],
132
+ edgeTypeHints: [{ elementTypeId: 'edge:static', dynamic: false }]
133
+ });
134
+
135
+ const result = await callCreateResult(
136
+ handler,
137
+ baseInput({ edges: [{ elementTypeId: 'edge:static', sourceElementId: 's', targetElementId: 't' }] })
138
+ );
139
+
140
+ const structured = result.structuredContent as unknown as DryRunStructured;
141
+ expect(structured.validationResults).to.have.lengthOf(1);
142
+ expect(structured.validationResults[0].isValid).to.equal(true);
143
+ expect(structured.validationResults[0].reason).to.match(/no dynamic edge-type hint/i);
144
+ expect(structured.createdEdges).to.deep.equal([]);
145
+ expect(dispatcher.dispatched).to.have.lengthOf(0);
146
+ });
147
+
148
+ it('flags isValid:false with a config-gap reason when edgeType declares a dynamic hint but no checker is bound', async () => {
149
+ const { handler, dispatcher } = buildHandler({
150
+ elements: [makeElement('s', 'task'), makeElement('t', 'task')],
151
+ edgeTypeHints: [{ elementTypeId: 'edge:dynamic', dynamic: true }]
152
+ });
153
+
154
+ const result = await callCreateResult(handler, baseInput());
155
+
156
+ const structured = result.structuredContent as unknown as DryRunStructured;
157
+ expect(structured.validationResults[0].isValid).to.equal(false);
158
+ expect(structured.validationResults[0].reason).to.include('EdgeCreationChecker is not bound');
159
+ expect(dispatcher.dispatched).to.have.lengthOf(0);
160
+ });
161
+
162
+ it('delegates to EdgeCreationChecker.isValidTarget when target + dynamic hint + checker are present', async () => {
163
+ const sourceElement = makeElement('s', 'task');
164
+ const targetElement = makeElement('t', 'task');
165
+ const checker = new StubEdgeCreationChecker(false);
166
+ const { handler, dispatcher } = buildHandler({
167
+ elements: [sourceElement, targetElement],
168
+ edgeTypeHints: [{ elementTypeId: 'edge:dynamic', dynamic: true }],
169
+ checker
170
+ });
171
+
172
+ const result = await callCreateResult(handler, baseInput());
173
+
174
+ expect(checker.lastTargetCall).to.deep.equal({
175
+ edgeType: 'edge:dynamic',
176
+ source: sourceElement,
177
+ target: targetElement
178
+ });
179
+ const structured = result.structuredContent as unknown as DryRunStructured;
180
+ expect(structured.validationResults[0].isValid).to.equal(false);
181
+ expect(dispatcher.dispatched).to.have.lengthOf(0);
182
+ });
183
+
184
+ it('reports source/target not-found inline as a per-edge validation failure (does not throw)', async () => {
185
+ const { handler } = buildHandler({
186
+ elements: [makeElement('s', 'task')],
187
+ edgeTypeHints: [{ elementTypeId: 'edge:dynamic', dynamic: true }]
188
+ });
189
+
190
+ const result = await callCreateResult(
191
+ handler,
192
+ baseInput({ edges: [{ elementTypeId: 'edge:dynamic', sourceElementId: 's', targetElementId: 'missing' }] })
193
+ );
194
+ const structured = result.structuredContent as unknown as DryRunStructured;
195
+ expect(structured.validationResults[0].isValid).to.equal(false);
196
+ expect(structured.validationResults[0].reason).to.include('Target element not found');
197
+ });
198
+ });
@@ -0,0 +1,200 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { ChangeRoutingPointsOperation, CreateEdgeOperation, DiagramConfiguration, EdgeCreationChecker } from '@eclipse-glsp/server';
18
+ import { inject, injectable, optional } from 'inversify';
19
+ import * as z from 'zod/v4';
20
+ import { McpToolResult } from '../../server/mcp-handler-shared';
21
+ import { ElementIdentity, ElementIdentitySchema, McpDiagramScopedInputSchema, position } from '../../server/mcp-input-schemas';
22
+ import { OperationMcpDiagramToolHandler } from '../../server/mcp-tool-handler';
23
+ import { formatNoticeList } from '../../util/mcp-util';
24
+
25
+ /** Single edge-creation entry. Strict so an LLM-typoed field surfaces as a validation error instead of being silently dropped. */
26
+ export const CreateEdgeSpecSchema = z.strictObject({
27
+ elementTypeId: z.string().describe('Edge type ID (e.g., `edge`, `transition`). Use the `element-types` tool to discover valid IDs.'),
28
+ sourceElementId: z.string().describe('ID of the source element (must exist in the diagram)'),
29
+ targetElementId: z.string().describe('ID of the target element (must exist in the diagram)'),
30
+ routingPoints: z.array(position).optional().describe('Optional array of routing point coordinates that allow for a complex edge path.'),
31
+ // `args` stays open (`record(...)`) — adopter-specific extension surface for per-edge-type creation hints.
32
+ args: z.record(z.string(), z.any()).optional().describe('Additional type-specific arguments for edge creation (varies by edge type)')
33
+ });
34
+
35
+ export const CreateEdgesInputSchema = McpDiagramScopedInputSchema.extend({
36
+ edges: z.array(CreateEdgeSpecSchema).min(1).describe('Array of edges to create. Must include at least one edge.'),
37
+ dryRun: z
38
+ .boolean()
39
+ .optional()
40
+ .describe(
41
+ 'When true, validate each edge against the type-hint rules without creating anything; returns per-edge `validationResults`.'
42
+ )
43
+ });
44
+ export type CreateEdgesInput = z.infer<typeof CreateEdgesInputSchema>;
45
+
46
+ export const CreateEdgesValidationResultSchema = z.object({
47
+ edgeType: z.string(),
48
+ sourceElementId: z.string(),
49
+ targetElementId: z.string(),
50
+ isValid: z.boolean(),
51
+ reason: z.string().optional().describe('Brief reason explaining the verdict (always present when `isValid: false`).')
52
+ });
53
+
54
+ export const CreateEdgesOutputSchema = z.object({
55
+ createdEdges: z.array(ElementIdentitySchema).describe('Identity of each edge successfully created. Empty in `dryRun` mode.'),
56
+ errors: z.array(z.string()).describe('Per-input failure messages; absent or empty when every input succeeded.'),
57
+ validationResults: z
58
+ .array(CreateEdgesValidationResultSchema)
59
+ .optional()
60
+ .describe('Per-input validation results. Present only in `dryRun` mode.')
61
+ });
62
+
63
+ type EdgeInput = CreateEdgesInput['edges'][number];
64
+ type ValidationResult = z.infer<typeof CreateEdgesValidationResultSchema>;
65
+
66
+ @injectable()
67
+ export class CreateEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<CreateEdgesInput> {
68
+ static readonly NAME = 'create-edges';
69
+ readonly name = CreateEdgesMcpToolHandler.NAME;
70
+ override readonly title = 'Create Diagram Edges';
71
+ readonly description =
72
+ 'Create one or multiple new edges connecting two elements in the diagram. ' +
73
+ 'Set `dryRun: true` to validate proposed edges (per the diagram-type type-hint rules) ' +
74
+ 'without creating anything; the result then carries per-edge `validationResults`. ' +
75
+ 'Without `dryRun`, this operation modifies the diagram state and requires user approval. ' +
76
+ 'Use the `element-types` tool to discover valid edge type IDs.';
77
+ readonly inputSchema = CreateEdgesInputSchema;
78
+ override readonly outputSchema = CreateEdgesOutputSchema;
79
+
80
+ @inject(DiagramConfiguration) protected diagramConfiguration: DiagramConfiguration;
81
+ @inject(EdgeCreationChecker) @optional() protected edgeCreationChecker?: EdgeCreationChecker;
82
+
83
+ protected async createResult({ edges, dryRun }: CreateEdgesInput): Promise<McpToolResult> {
84
+ if (dryRun) {
85
+ return this.runDryRun(edges);
86
+ }
87
+ return this.runCreate(edges);
88
+ }
89
+
90
+ protected runDryRun(edges: EdgeInput[]): McpToolResult {
91
+ const validationResults: ValidationResult[] = edges.map(edge => this.validateEdge(edge));
92
+ const validCount = validationResults.filter(result => result.isValid).length;
93
+ const summary =
94
+ `Dry run: validated ${edges.length} edge(s); ${validCount} would be accepted, ${edges.length - validCount} rejected.\n` +
95
+ validationResults
96
+ .map(
97
+ result =>
98
+ `- ${result.edgeType} ${result.sourceElementId} → ${result.targetElementId}: ` +
99
+ `${result.isValid ? 'valid' : `invalid (${result.reason})`}`
100
+ )
101
+ .join('\n');
102
+ return this.success(summary, { createdEdges: [], errors: [], validationResults });
103
+ }
104
+
105
+ protected async runCreate(edges: EdgeInput[]): Promise<McpToolResult> {
106
+ let beforeIds = this.modelState.index.allIds();
107
+
108
+ const errors: string[] = [];
109
+ const createdEdges: ElementIdentity[] = [];
110
+ let dispatchedOperations = 0;
111
+ // Sequential — each iteration must isolate its own creation in the post-dispatch diff.
112
+ for (const edge of edges) {
113
+ const { elementTypeId, routingPoints, args } = edge;
114
+ const sourceElementId = this.aliasService.lookup(edge.sourceElementId);
115
+ const targetElementId = this.aliasService.lookup(edge.targetElementId);
116
+
117
+ const source = this.modelState.index.find(sourceElementId);
118
+ if (!source) {
119
+ errors.push(`Source element not found: ${edge.sourceElementId}`);
120
+ continue;
121
+ }
122
+ const target = this.modelState.index.find(targetElementId);
123
+ if (!target) {
124
+ errors.push(`Target element not found: ${edge.targetElementId}`);
125
+ continue;
126
+ }
127
+
128
+ const operation = CreateEdgeOperation.create({ elementTypeId, sourceElementId, targetElementId, args });
129
+ await this.actionDispatcher.dispatch(operation);
130
+ dispatchedOperations++;
131
+
132
+ const afterIds = this.modelState.index.allIds();
133
+ const newIds = afterIds.filter(id => !beforeIds.includes(id));
134
+ const newElements = newIds.map(id => this.modelState.index.find(id)).filter(element => element?.type === elementTypeId);
135
+ const newElement = newElements[0];
136
+ if (newElements.length > 1) {
137
+ this.logger.warn('More than 1 new element created');
138
+ }
139
+ beforeIds = afterIds;
140
+
141
+ // Operations don't surface failure directly — infer from absence of a new id of the requested type.
142
+ if (!newElement) {
143
+ errors.push(`Edge creation likely failed because no new element ID was found for input: ${JSON.stringify(edge)}`);
144
+ continue;
145
+ }
146
+
147
+ if (routingPoints) {
148
+ const routingPointsOperation = ChangeRoutingPointsOperation.create([
149
+ { elementId: newElement.id, newRoutingPoints: routingPoints }
150
+ ]);
151
+ await this.actionDispatcher.dispatch(routingPointsOperation);
152
+ dispatchedOperations++;
153
+ }
154
+
155
+ createdEdges.push(this.describeResolvedElement(newElement));
156
+ }
157
+
158
+ const successListStr = createdEdges.map(({ id, elementTypeId }) => `- ${elementTypeId} (#${id})`).join('\n');
159
+ // Per-input errors are surfaced in `errors`; the call itself still succeeds — rolling back partial creates would require operation-level transactions.
160
+ return this.success(
161
+ `Successfully created ${createdEdges.length} edge(s) (in ${dispatchedOperations} commands):\n${successListStr}${formatNoticeList('errors', errors)}`,
162
+ { createdEdges, errors }
163
+ );
164
+ }
165
+
166
+ /** Mirrors `RequestCheckEdgeAction`: existence check, then dynamic-hint → checker; static or unknown edgeType → valid. */
167
+ protected validateEdge(edge: EdgeInput): ValidationResult {
168
+ const { elementTypeId } = edge;
169
+ const sourceRealId = this.aliasService.lookup(edge.sourceElementId);
170
+ const targetRealId = this.aliasService.lookup(edge.targetElementId);
171
+ const echo: Pick<ValidationResult, 'edgeType' | 'sourceElementId' | 'targetElementId'> = {
172
+ edgeType: elementTypeId,
173
+ sourceElementId: edge.sourceElementId,
174
+ targetElementId: edge.targetElementId
175
+ };
176
+
177
+ const source = this.modelState.index.find(sourceRealId);
178
+ if (!source) {
179
+ return { ...echo, isValid: false, reason: `Source element not found: ${edge.sourceElementId}` };
180
+ }
181
+ const target = this.modelState.index.find(targetRealId);
182
+ if (!target) {
183
+ return { ...echo, isValid: false, reason: `Target element not found: ${edge.targetElementId}` };
184
+ }
185
+
186
+ const hasDynamicHint = this.diagramConfiguration.edgeTypeHints.some(hint => hint.elementTypeId === elementTypeId && hint.dynamic);
187
+ if (!hasDynamicHint) {
188
+ return { ...echo, isValid: true, reason: 'no dynamic edge-type hint — static hints apply' };
189
+ }
190
+ if (!this.edgeCreationChecker) {
191
+ return {
192
+ ...echo,
193
+ isValid: false,
194
+ reason: `EdgeCreationChecker is not bound although edge type '${elementTypeId}' declares a dynamic hint.`
195
+ };
196
+ }
197
+ const isValid = this.edgeCreationChecker.isValidTarget(elementTypeId, source, target);
198
+ return { ...echo, isValid, ...(isValid ? {} : { reason: 'rejected by EdgeCreationChecker' }) };
199
+ }
200
+ }
@@ -0,0 +1,199 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import {
18
+ Action,
19
+ ActionDispatcher,
20
+ ApplyLabelEditOperation,
21
+ ClientId,
22
+ CreateNodeOperation,
23
+ GLabel,
24
+ GModelElement,
25
+ Logger,
26
+ ModelState,
27
+ NullLogger
28
+ } from '@eclipse-glsp/server';
29
+ import { expect } from 'chai';
30
+ import { Container, ContainerModule } from 'inversify';
31
+ import { McpToolResult } from '../../server/mcp-handler-shared';
32
+ import { McpIdAliasService } from '../../server/mcp-id-alias-service';
33
+ import { McpLabelProvider } from '../../server/mcp-label-provider';
34
+ import { CreateNodesInput, CreateNodesMcpToolHandler } from './create-nodes-mcp-tool-handler';
35
+
36
+ interface CreatedElementsStructured {
37
+ createdNodes: { id: string; elementTypeId: string; label?: string }[];
38
+ errors: string[];
39
+ warnings: string[];
40
+ }
41
+
42
+ function makeElement(id: string, type: string, children: GModelElement[] = []): GModelElement {
43
+ return { id, type, children } as unknown as GModelElement;
44
+ }
45
+
46
+ /** Mutable model that drops in new elements as CreateNodeOperations are dispatched. */
47
+ class FakeModelState {
48
+ public elements = new Map<string, GModelElement>();
49
+ constructor(initial: GModelElement[]) {
50
+ initial.forEach(el => this.elements.set(el.id, el));
51
+ }
52
+ addElement(el: GModelElement): void {
53
+ this.elements.set(el.id, el);
54
+ }
55
+ get index(): ModelState['index'] {
56
+ return {
57
+ allIds: () => [...this.elements.keys()],
58
+ get: (id: string) => this.elements.get(id),
59
+ find: (id: string) => this.elements.get(id)
60
+ } as unknown as ModelState['index'];
61
+ }
62
+ get isReadonly(): boolean {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /** Dispatcher that materializes a new element per `CreateNodeOperation` and records the action. */
68
+ class CreatingDispatcher {
69
+ public dispatched: Action[] = [];
70
+ private idCounter = 0;
71
+ constructor(
72
+ private readonly model: FakeModelState,
73
+ private readonly labeled: Set<string>
74
+ ) {}
75
+
76
+ async dispatch(action: Action): Promise<void> {
77
+ this.dispatched.push(action);
78
+ if (CreateNodeOperation.is(action)) {
79
+ const id = `${action.elementTypeId}#${++this.idCounter}`;
80
+ const children: GModelElement[] = this.labeled.has(action.elementTypeId)
81
+ ? [Object.assign(new GLabel(), { id: `${id}_label`, type: 'label', children: [] })]
82
+ : [];
83
+ this.model.addElement(makeElement(id, action.elementTypeId, children));
84
+ }
85
+ }
86
+ }
87
+
88
+ interface BuildArgs {
89
+ /** Element types whose newly created instance includes a child GLabel. */
90
+ labeled?: string[];
91
+ /** Element types for which CreateNodeOperation should produce no new element (silent failure simulation). */
92
+ failed?: string[];
93
+ }
94
+
95
+ function buildHandler({ labeled = [], failed = [] }: BuildArgs = {}): {
96
+ handler: CreateNodesMcpToolHandler;
97
+ dispatcher: CreatingDispatcher;
98
+ } {
99
+ const model = new FakeModelState([]);
100
+ const failedSet = new Set(failed);
101
+ const labeledSet = new Set(labeled);
102
+ const dispatcher = new (class extends CreatingDispatcher {
103
+ override async dispatch(action: Action): Promise<void> {
104
+ if (CreateNodeOperation.is(action) && failedSet.has(action.elementTypeId)) {
105
+ this.dispatched.push(action);
106
+ return;
107
+ }
108
+ return super.dispatch(action);
109
+ }
110
+ })(model, labeledSet);
111
+
112
+ const container = new Container();
113
+ container.load(
114
+ new ContainerModule(bind => {
115
+ bind(Logger).toConstantValue(new NullLogger());
116
+ bind(ClientId).toConstantValue('test-session');
117
+ bind(ModelState).toConstantValue(model as unknown as ModelState);
118
+ bind(McpIdAliasService).toConstantValue({
119
+ lookup: (id: string) => id,
120
+ alias: (id: string) => id
121
+ } as McpIdAliasService);
122
+ bind(ActionDispatcher).toConstantValue(dispatcher as unknown as ActionDispatcher);
123
+ bind(McpLabelProvider).toConstantValue({
124
+ getLabel: (element: GModelElement) => element.children.find(c => c instanceof GLabel) as GLabel | undefined
125
+ } as McpLabelProvider);
126
+ bind(CreateNodesMcpToolHandler).toSelf();
127
+ })
128
+ );
129
+ return { handler: container.get(CreateNodesMcpToolHandler), dispatcher };
130
+ }
131
+
132
+ function callCreateResult(handler: CreateNodesMcpToolHandler, params: CreateNodesInput): Promise<McpToolResult> {
133
+ return (handler as unknown as { createResult: (p: CreateNodesInput) => Promise<McpToolResult> }).createResult(params);
134
+ }
135
+
136
+ describe('CreateNodesMcpToolHandler', () => {
137
+ it('dispatches one CreateNodeOperation per node and reports the new element ids', async () => {
138
+ const { handler, dispatcher } = buildHandler();
139
+
140
+ const result = await callCreateResult(handler, {
141
+ sessionId: 's',
142
+ nodes: [
143
+ { elementTypeId: 'task:manual', position: { x: 0, y: 0 } },
144
+ { elementTypeId: 'task:manual', position: { x: 100, y: 0 } }
145
+ ]
146
+ });
147
+
148
+ const structured = result.structuredContent as unknown as CreatedElementsStructured;
149
+ expect(structured.createdNodes).to.have.lengthOf(2);
150
+ expect(structured.createdNodes[0].id).to.equal('task:manual#1');
151
+ expect(structured.createdNodes[1].id).to.equal('task:manual#2');
152
+ expect(structured.errors).to.deep.equal([]);
153
+ expect(structured.warnings).to.deep.equal([]);
154
+ expect(dispatcher.dispatched.filter(CreateNodeOperation.is)).to.have.lengthOf(2);
155
+ });
156
+
157
+ it('dispatches ApplyLabelEditOperation when `text` is supplied and the new element has a label', async () => {
158
+ const { handler, dispatcher } = buildHandler({ labeled: ['task:manual'] });
159
+
160
+ await callCreateResult(handler, {
161
+ sessionId: 's',
162
+ nodes: [{ elementTypeId: 'task:manual', position: { x: 0, y: 0 }, text: 'Hello' }]
163
+ });
164
+
165
+ const edits = dispatcher.dispatched.filter(ApplyLabelEditOperation.is);
166
+ expect(edits).to.have.lengthOf(1);
167
+ expect(edits[0].text).to.equal('Hello');
168
+ expect(edits[0].labelId).to.equal('task:manual#1_label');
169
+ });
170
+
171
+ it('emits a warning (no ApplyLabelEditOperation) when `text` is supplied but the element type has no label', async () => {
172
+ const { handler, dispatcher } = buildHandler({ labeled: [] });
173
+
174
+ const result = await callCreateResult(handler, {
175
+ sessionId: 's',
176
+ nodes: [{ elementTypeId: 'decision', position: { x: 0, y: 0 }, text: 'Ignored' }]
177
+ });
178
+
179
+ const structured = result.structuredContent as unknown as CreatedElementsStructured;
180
+ expect(structured.warnings).to.have.lengthOf(1);
181
+ expect(structured.warnings[0]).to.match(/no editable label/i);
182
+ expect(dispatcher.dispatched.filter(ApplyLabelEditOperation.is)).to.have.lengthOf(0);
183
+ });
184
+
185
+ it('records an error and skips label-edit when the CreateNodeOperation produces no new element', async () => {
186
+ const { handler, dispatcher } = buildHandler({ failed: ['ghost'] });
187
+
188
+ const result = await callCreateResult(handler, {
189
+ sessionId: 's',
190
+ nodes: [{ elementTypeId: 'ghost', position: { x: 0, y: 0 }, text: 'Hello' }]
191
+ });
192
+
193
+ const structured = result.structuredContent as unknown as CreatedElementsStructured;
194
+ expect(structured.createdNodes).to.deep.equal([]);
195
+ expect(structured.errors).to.have.lengthOf(1);
196
+ expect(structured.errors[0]).to.include('ghost');
197
+ expect(dispatcher.dispatched.filter(ApplyLabelEditOperation.is)).to.have.lengthOf(0);
198
+ });
199
+ });