@eclipse-glsp/server-mcp 2.8.0-next.7 → 2.8.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 (109) hide show
  1. package/lib/common/modules/mcp-diagram-module.js +1 -1
  2. package/lib/common/modules/mcp-diagram-module.js.map +1 -1
  3. package/lib/common/server/mcp-diagram-handler-dispatcher.d.ts +20 -4
  4. package/lib/common/server/mcp-diagram-handler-dispatcher.d.ts.map +1 -1
  5. package/lib/common/server/mcp-diagram-handler-dispatcher.js +36 -5
  6. package/lib/common/server/mcp-diagram-handler-dispatcher.js.map +1 -1
  7. package/lib/common/server/mcp-input-schemas.d.ts +2 -0
  8. package/lib/common/server/mcp-input-schemas.d.ts.map +1 -1
  9. package/lib/common/server/mcp-input-schemas.js +3 -1
  10. package/lib/common/server/mcp-input-schemas.js.map +1 -1
  11. package/lib/common/server/mcp-resource-handler.d.ts +10 -1
  12. package/lib/common/server/mcp-resource-handler.d.ts.map +1 -1
  13. package/lib/common/server/mcp-resource-handler.js +12 -1
  14. package/lib/common/server/mcp-resource-handler.js.map +1 -1
  15. package/lib/common/server/mcp-tool-handler.d.ts +33 -16
  16. package/lib/common/server/mcp-tool-handler.d.ts.map +1 -1
  17. package/lib/common/server/mcp-tool-handler.js +18 -1
  18. package/lib/common/server/mcp-tool-handler.js.map +1 -1
  19. package/lib/common/tools/handlers/count-elements-mcp-tool-handler.d.ts +2 -1
  20. package/lib/common/tools/handlers/count-elements-mcp-tool-handler.d.ts.map +1 -1
  21. package/lib/common/tools/handlers/count-elements-mcp-tool-handler.js.map +1 -1
  22. package/lib/common/tools/handlers/create-edges-mcp-tool-handler.d.ts +2 -1
  23. package/lib/common/tools/handlers/create-edges-mcp-tool-handler.d.ts.map +1 -1
  24. package/lib/common/tools/handlers/create-edges-mcp-tool-handler.js +11 -6
  25. package/lib/common/tools/handlers/create-edges-mcp-tool-handler.js.map +1 -1
  26. package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.d.ts +2 -1
  27. package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.d.ts.map +1 -1
  28. package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.js +14 -6
  29. package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.js.map +1 -1
  30. package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.d.ts +2 -1
  31. package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.d.ts.map +1 -1
  32. package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.js.map +1 -1
  33. package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.d.ts +2 -1
  34. package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.d.ts.map +1 -1
  35. package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.js +4 -2
  36. package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.js.map +1 -1
  37. package/lib/common/tools/handlers/element-types-mcp-tool-handler.d.ts +2 -1
  38. package/lib/common/tools/handlers/element-types-mcp-tool-handler.d.ts.map +1 -1
  39. package/lib/common/tools/handlers/element-types-mcp-tool-handler.js +4 -2
  40. package/lib/common/tools/handlers/element-types-mcp-tool-handler.js.map +1 -1
  41. package/lib/common/tools/handlers/get-selection-mcp-tool-handler.d.ts +2 -1
  42. package/lib/common/tools/handlers/get-selection-mcp-tool-handler.d.ts.map +1 -1
  43. package/lib/common/tools/handlers/get-selection-mcp-tool-handler.js.map +1 -1
  44. package/lib/common/tools/handlers/layout-mcp-tool-handler.d.ts +5 -2
  45. package/lib/common/tools/handlers/layout-mcp-tool-handler.d.ts.map +1 -1
  46. package/lib/common/tools/handlers/layout-mcp-tool-handler.js +5 -1
  47. package/lib/common/tools/handlers/layout-mcp-tool-handler.js.map +1 -1
  48. package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.d.ts +2 -1
  49. package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.d.ts.map +1 -1
  50. package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.js +10 -7
  51. package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.js.map +1 -1
  52. package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.d.ts +4 -1
  53. package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.d.ts.map +1 -1
  54. package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.js +54 -22
  55. package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.js.map +1 -1
  56. package/lib/common/tools/handlers/query-elements-mcp-tool-handler.d.ts +2 -1
  57. package/lib/common/tools/handlers/query-elements-mcp-tool-handler.d.ts.map +1 -1
  58. package/lib/common/tools/handlers/query-elements-mcp-tool-handler.js.map +1 -1
  59. package/lib/common/tools/handlers/redo-mcp-tool-handler.d.ts +2 -1
  60. package/lib/common/tools/handlers/redo-mcp-tool-handler.d.ts.map +1 -1
  61. package/lib/common/tools/handlers/redo-mcp-tool-handler.js +7 -3
  62. package/lib/common/tools/handlers/redo-mcp-tool-handler.js.map +1 -1
  63. package/lib/common/tools/handlers/save-model-mcp-tool-handler.d.ts +2 -1
  64. package/lib/common/tools/handlers/save-model-mcp-tool-handler.d.ts.map +1 -1
  65. package/lib/common/tools/handlers/save-model-mcp-tool-handler.js +3 -1
  66. package/lib/common/tools/handlers/save-model-mcp-tool-handler.js.map +1 -1
  67. package/lib/common/tools/handlers/session-info-mcp-tool-handler.d.ts +2 -1
  68. package/lib/common/tools/handlers/session-info-mcp-tool-handler.d.ts.map +1 -1
  69. package/lib/common/tools/handlers/session-info-mcp-tool-handler.js.map +1 -1
  70. package/lib/common/tools/handlers/set-selection-mcp-tool-handler.d.ts +2 -1
  71. package/lib/common/tools/handlers/set-selection-mcp-tool-handler.d.ts.map +1 -1
  72. package/lib/common/tools/handlers/set-selection-mcp-tool-handler.js +5 -3
  73. package/lib/common/tools/handlers/set-selection-mcp-tool-handler.js.map +1 -1
  74. package/lib/common/tools/handlers/set-view-mcp-tool-handler.d.ts +2 -1
  75. package/lib/common/tools/handlers/set-view-mcp-tool-handler.d.ts.map +1 -1
  76. package/lib/common/tools/handlers/set-view-mcp-tool-handler.js +1 -1
  77. package/lib/common/tools/handlers/set-view-mcp-tool-handler.js.map +1 -1
  78. package/lib/common/tools/handlers/undo-mcp-tool-handler.d.ts +2 -1
  79. package/lib/common/tools/handlers/undo-mcp-tool-handler.d.ts.map +1 -1
  80. package/lib/common/tools/handlers/undo-mcp-tool-handler.js +7 -3
  81. package/lib/common/tools/handlers/undo-mcp-tool-handler.js.map +1 -1
  82. package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.d.ts +2 -1
  83. package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.d.ts.map +1 -1
  84. package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.js +0 -0
  85. package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.js.map +1 -1
  86. package/package.json +2 -2
  87. package/src/common/modules/mcp-diagram-module.ts +1 -1
  88. package/src/common/server/mcp-diagram-handler-dispatcher.ts +56 -5
  89. package/src/common/server/mcp-input-schemas.ts +3 -0
  90. package/src/common/server/mcp-resource-handler.ts +13 -1
  91. package/src/common/server/mcp-tool-handler.ts +47 -19
  92. package/src/common/tools/handlers/count-elements-mcp-tool-handler.ts +2 -1
  93. package/src/common/tools/handlers/create-edges-mcp-tool-handler.ts +20 -8
  94. package/src/common/tools/handlers/create-nodes-mcp-tool-handler.ts +19 -8
  95. package/src/common/tools/handlers/delete-elements-mcp-tool-handler.ts +2 -1
  96. package/src/common/tools/handlers/diagram-model-mcp-tool-handler.ts +6 -3
  97. package/src/common/tools/handlers/element-types-mcp-tool-handler.ts +6 -3
  98. package/src/common/tools/handlers/get-selection-mcp-tool-handler.ts +2 -1
  99. package/src/common/tools/handlers/layout-mcp-tool-handler.ts +8 -2
  100. package/src/common/tools/handlers/modify-edges-mcp-tool-handler.ts +14 -8
  101. package/src/common/tools/handlers/modify-nodes-mcp-tool-handler.ts +63 -25
  102. package/src/common/tools/handlers/query-elements-mcp-tool-handler.ts +2 -1
  103. package/src/common/tools/handlers/redo-mcp-tool-handler.ts +9 -4
  104. package/src/common/tools/handlers/save-model-mcp-tool-handler.ts +5 -2
  105. package/src/common/tools/handlers/session-info-mcp-tool-handler.ts +2 -1
  106. package/src/common/tools/handlers/set-selection-mcp-tool-handler.ts +8 -5
  107. package/src/common/tools/handlers/set-view-mcp-tool-handler.ts +3 -2
  108. package/src/common/tools/handlers/undo-mcp-tool-handler.ts +9 -4
  109. package/src/common/tools/handlers/validate-diagram-mcp-tool-handler.ts +0 -0
@@ -37,6 +37,11 @@ import { McpRequestContext, NoopMcpRequestContext } from './mcp-request-context'
37
37
  import { AbstractMcpDiagramResourceHandler, McpDiagramResourceHandlerConstructor, toParams } from './mcp-resource-handler';
38
38
  import { BaseMcpDiagramToolHandler, McpDiagramToolHandlerConstructor } from './mcp-tool-handler';
39
39
 
40
+ /** The diagram-type support hook shared by the diagram-scope tool and resource handler bases. */
41
+ interface DiagramTypeSupportAware {
42
+ isSupportedByDiagramType(): boolean;
43
+ }
44
+
40
45
  /**
41
46
  * Per-diagram-type catalog of constructor lists, harvested at MCP-server start by loading each
42
47
  * diagram type's modules onto a temporary child container — same pattern as
@@ -97,11 +102,13 @@ export class DefaultMcpDiagramHandlerDispatcher implements McpDiagramHandlerDisp
97
102
 
98
103
  /**
99
104
  * Build the per-diagram-type catalog by inspecting each diagram type's module set. We don't
100
- * have a real GLSP session yet — and we don't want one, because we only need the bound
105
+ * have a real GLSP session yet — and we don't want one, because we mostly need the bound
101
106
  * constructor *lists*, not instances. So we spin up a throwaway child container per diagram
102
107
  * type, load its modules plus a placeholder session module, and read out the multi-binding
103
- * constants. No handler is instantiated; the temporary container is unbound immediately
104
- * after.
108
+ * constants. The temporary container is unbound immediately after.
109
+ *
110
+ * Only handlers that override `isSupportedByDiagramType` are instantiated, so a handler with
111
+ * a `@postConstruct` sees this probe just because it opted into the support gate.
105
112
  *
106
113
  * The placeholder session module is bound with the synthetic {@link TEMPORARY_CLIENT_ID} so
107
114
  * any session-scoped `@inject(ClientId)` in module-load wiring resolves cleanly. Diagram
@@ -123,8 +130,18 @@ export class DefaultMcpDiagramHandlerDispatcher implements McpDiagramHandlerDisp
123
130
  for (const [diagramType, modules] of this.diagramModules) {
124
131
  const tempContainer = this.serverContainer.createChild();
125
132
  tempContainer.load(...modules, placeholderSessionModule);
126
- const tools = getConstructorList<McpDiagramToolHandlerConstructor>(tempContainer, McpDiagramToolHandlerConstructor);
127
- const resources = getConstructorList<McpDiagramResourceHandlerConstructor>(tempContainer, McpDiagramResourceHandlerConstructor);
133
+ const tools = this.filterSupported(
134
+ tempContainer,
135
+ getConstructorList<McpDiagramToolHandlerConstructor>(tempContainer, McpDiagramToolHandlerConstructor),
136
+ diagramType,
137
+ BaseMcpDiagramToolHandler.prototype.isSupportedByDiagramType
138
+ );
139
+ const resources = this.filterSupported(
140
+ tempContainer,
141
+ getConstructorList<McpDiagramResourceHandlerConstructor>(tempContainer, McpDiagramResourceHandlerConstructor),
142
+ diagramType,
143
+ AbstractMcpDiagramResourceHandler.prototype.isSupportedByDiagramType
144
+ );
128
145
  const prompts = getConstructorList<McpDiagramPromptHandlerConstructor>(tempContainer, McpDiagramPromptHandlerConstructor);
129
146
  tempContainer.unbindAll();
130
147
  catalogs.push({ diagramType, toolConstructors: tools, resourceConstructors: resources, promptConstructors: prompts });
@@ -132,6 +149,40 @@ export class DefaultMcpDiagramHandlerDispatcher implements McpDiagramHandlerDisp
132
149
  this.diagramCatalogs = catalogs;
133
150
  }
134
151
 
152
+ /**
153
+ * Drop constructors whose handler reports the diagram type can't support it, so they never
154
+ * reach the MCP catalog. A handler that overrides the hook is resolved against the harvest
155
+ * container, where `@optional()` dependencies reflect the diagram type's real bindings; one
156
+ * that inherits `defaultHook` or doesn't declare the hook is kept without being instantiated.
157
+ *
158
+ * Fails open: a constructor that cannot be resolved here stays in the catalog.
159
+ */
160
+ protected filterSupported<C extends interfaces.Newable<DiagramTypeSupportAware>>(
161
+ container: Container,
162
+ constructors: C[],
163
+ diagramType: string,
164
+ defaultHook: DiagramTypeSupportAware['isSupportedByDiagramType']
165
+ ): C[] {
166
+ return constructors.filter(constructor => {
167
+ // A duck-typed handler may not carry the hook at all; skip it rather than resolving it
168
+ // only to call a method that isn't there.
169
+ const hook = constructor.prototype.isSupportedByDiagramType;
170
+ if (typeof hook !== 'function' || hook === defaultHook) {
171
+ return true;
172
+ }
173
+ try {
174
+ if (container.resolve(constructor).isSupportedByDiagramType()) {
175
+ return true;
176
+ }
177
+ this.logger.debug(`Diagram type '${diagramType}' does not support MCP handler '${constructor.name}'; not registering.`);
178
+ return false;
179
+ } catch (err: unknown) {
180
+ this.logger.debug(`Could not probe MCP handler '${constructor.name}' for diagram type '${diagramType}'; registering.`, err);
181
+ return true;
182
+ }
183
+ });
184
+ }
185
+
135
186
  /** True when at least one diagram type has at least one tool handler bound. */
136
187
  hasDiagramTools(): boolean {
137
188
  return this.diagramCatalogs.some(catalog => catalog.toolConstructors.length > 0);
@@ -45,6 +45,9 @@ export const elementId = z.string();
45
45
  /** One or more element ids. Empty arrays are rejected. */
46
46
  export const elementIds = z.array(z.string()).min(1);
47
47
 
48
+ /** Zero or more element ids, for tools where an empty array is itself meaningful (e.g. "select nothing"). */
49
+ export const elementIdsAllowingEmpty = z.array(z.string());
50
+
48
51
  /** Cartesian position used by node-creation / -modification tools. */
49
52
  export const position = z
50
53
  .object({
@@ -294,7 +294,19 @@ export abstract class AbstractMcpDiagramResourceHandler<
294
294
  return requestActionOrFail(this.actionDispatcher, request, timeoutMs, label);
295
295
  }
296
296
 
297
- /** Override to opt out of registration when a runtime dependency is missing. Default: `true`. */
297
+ /**
298
+ * Whether the diagram type can support this resource at all — see
299
+ * {@link BaseMcpDiagramToolHandler.isSupportedByDiagramType}. Returning `false` keeps the
300
+ * resource out of the MCP catalog.
301
+ */
302
+ isSupportedByDiagramType(): boolean {
303
+ return true;
304
+ }
305
+
306
+ /**
307
+ * Override to opt out when a per-session dependency is missing. Default: `true`. Gates the
308
+ * per-GLSP-session registry only — see {@link BaseMcpDiagramToolHandler.canRegister}.
309
+ */
298
310
  canRegister(): boolean {
299
311
  return true;
300
312
  }
@@ -26,7 +26,7 @@ import {
26
26
  } from '@eclipse-glsp/server';
27
27
  import { ToolAnnotations } from '@modelcontextprotocol/sdk/types';
28
28
  import { inject, injectable, interfaces, optional } from 'inversify';
29
- import { ZodObject, ZodRawShape } from 'zod/v4';
29
+ import { ZodObject, ZodRawShape, ZodType } from 'zod/v4';
30
30
  import { GLSPMcpServer } from './glsp-mcp-server';
31
31
  import {
32
32
  McpElementsNotFoundError,
@@ -74,7 +74,7 @@ export const McpToolHandler = Symbol('McpToolHandler');
74
74
  * @experimental
75
75
  */
76
76
  @injectable()
77
- export abstract class BaseMcpToolHandler {
77
+ export abstract class BaseMcpToolHandler<O extends McpStructuredContent = McpStructuredContent> {
78
78
  @inject(McpRequestContext)
79
79
  @optional()
80
80
  protected requestContext: McpRequestContext = new NoopMcpRequestContext();
@@ -95,9 +95,11 @@ export abstract class BaseMcpToolHandler {
95
95
  * Optional dual-emit schema. When set, pass the matching `structured` payload to
96
96
  * {@link success} so the framework forwards it as `structuredContent` alongside the
97
97
  * human-readable text. The MCP spec says clients SHOULD validate `structuredContent`
98
- * against the declared schema, so the two MUST stay in sync.
98
+ * against the declared schema, so the two MUST stay in sync. Bind the `O` type parameter to
99
+ * `z.infer<typeof MyOutputSchema>` to have the compiler enforce that — the type ties this
100
+ * field and {@link success}'s payload to the same shape.
99
101
  */
100
- readonly outputSchema?: ZodObject<ZodRawShape>;
102
+ readonly outputSchema?: ZodObject<ZodRawShape> & ZodType<O>;
101
103
  /** Optional human-friendly display name for UIs that render a friendlier label than `name`. */
102
104
  readonly title?: string;
103
105
 
@@ -187,7 +189,7 @@ export abstract class BaseMcpToolHandler {
187
189
  * Pass `structured` whenever {@link outputSchema} is declared (the spec says clients SHOULD
188
190
  * validate against the declared shape). Omit for plain text-only responses.
189
191
  */
190
- protected success(message: string, structured?: McpStructuredContent): McpToolResult {
192
+ protected success(message: string, structured?: O): McpToolResult {
191
193
  return { isError: false, content: [{ type: 'text', text: message }], structuredContent: structured };
192
194
  }
193
195
 
@@ -220,13 +222,16 @@ export abstract class BaseMcpToolHandler {
220
222
  * @experimental
221
223
  */
222
224
  @injectable()
223
- export abstract class AbstractMcpToolHandler<T = Record<string, unknown>> extends BaseMcpToolHandler implements McpToolHandler {
225
+ export abstract class AbstractMcpToolHandler<I = Record<string, unknown>, O extends McpStructuredContent = McpStructuredContent>
226
+ extends BaseMcpToolHandler<O>
227
+ implements McpToolHandler
228
+ {
224
229
  /** Throw {@link McpToolError} for expected, user-facing errors; the base wraps. */
225
- protected abstract createResult(params: T): MaybePromise<McpToolResult>;
230
+ protected abstract createResult(params: I): MaybePromise<McpToolResult>;
226
231
 
227
232
  registerTool(server: GLSPMcpServer): void {
228
233
  server.registerTool(this.name, this.toRegistrationConfig(), async (params, extra) =>
229
- this.requestContext.run(extra, () => this.execute(() => this.createResult(params as T)))
234
+ this.requestContext.run(extra, () => this.execute(() => this.createResult(params as I)))
230
235
  );
231
236
  }
232
237
  }
@@ -247,14 +252,17 @@ export abstract class AbstractMcpToolHandler<T = Record<string, unknown>> extend
247
252
  * @experimental
248
253
  */
249
254
  @injectable()
250
- export abstract class BaseMcpDiagramToolHandler<T extends McpDiagramScopedInput = McpDiagramScopedInput> extends BaseMcpToolHandler {
255
+ export abstract class BaseMcpDiagramToolHandler<
256
+ I extends McpDiagramScopedInput = McpDiagramScopedInput,
257
+ O extends McpStructuredContent = McpStructuredContent
258
+ > extends BaseMcpToolHandler<O> {
251
259
  @inject(ClientId) protected clientId: string;
252
260
  @inject(ModelState) protected modelState: ModelState;
253
261
  @inject(McpIdAliasService) protected aliasService: McpIdAliasService;
254
262
  @inject(McpLabelProvider) protected labelProvider: McpLabelProvider;
255
263
 
256
264
  /** Throw {@link McpToolError} for expected errors; the base wraps. */
257
- protected abstract createResult(params: T): MaybePromise<McpToolResult>;
265
+ protected abstract createResult(params: I): MaybePromise<McpToolResult>;
258
266
 
259
267
  /**
260
268
  * Public dispatch entry point invoked by {@link AbstractMcpServerLauncher}'s registered SDK
@@ -262,9 +270,27 @@ export abstract class BaseMcpDiagramToolHandler<T extends McpDiagramScopedInput
262
270
  * passes through; {@link OperationMcpDiagramToolHandler} enforces the readonly gate.
263
271
  * Adopters don't call this directly.
264
272
  */
265
- abstract handle(params: T): Promise<McpToolResult>;
273
+ abstract handle(params: I): Promise<McpToolResult>;
266
274
 
267
- /** Override to opt out of registration when a runtime dependency is missing. Default: `true`. */
275
+ /**
276
+ * Whether the diagram type can support this tool at all. Evaluated once per diagram type at
277
+ * MCP-server start against a container with that type's diagram modules loaded, so
278
+ * `@inject(...) @optional()` dependencies reflect its real bindings. When no registered
279
+ * diagram type supports the tool, it is left out of the MCP catalog entirely.
280
+ *
281
+ * Override this for statically bound dependencies (e.g. a `LayoutEngine`) and
282
+ * {@link canRegister} for capabilities of the connected GLSP client — the harvest container
283
+ * has no client, so client capabilities always read as absent here.
284
+ */
285
+ isSupportedByDiagramType(): boolean {
286
+ return true;
287
+ }
288
+
289
+ /**
290
+ * Override to opt out when a per-session dependency is missing. Default: `true`. Gates the
291
+ * per-GLSP-session registry only: the tool stays in the catalog (another session may support
292
+ * it) and calls against this session fail with "No tool handler '…' registered".
293
+ */
268
294
  canRegister(): boolean {
269
295
  return true;
270
296
  }
@@ -357,9 +383,10 @@ export abstract class BaseMcpDiagramToolHandler<T extends McpDiagramScopedInput
357
383
  */
358
384
  @injectable()
359
385
  export abstract class AbstractMcpDiagramToolHandler<
360
- T extends McpDiagramScopedInput = McpDiagramScopedInput
361
- > extends BaseMcpDiagramToolHandler<T> {
362
- handle(params: T): Promise<McpToolResult> {
386
+ I extends McpDiagramScopedInput = McpDiagramScopedInput,
387
+ O extends McpStructuredContent = McpStructuredContent
388
+ > extends BaseMcpDiagramToolHandler<I, O> {
389
+ handle(params: I): Promise<McpToolResult> {
363
390
  return this.execute(() => this.createResult(params));
364
391
  }
365
392
  }
@@ -388,8 +415,9 @@ export abstract class AbstractMcpDiagramToolHandler<
388
415
  */
389
416
  @injectable()
390
417
  export abstract class OperationMcpDiagramToolHandler<
391
- T extends McpDiagramScopedInput = McpDiagramScopedInput
392
- > extends BaseMcpDiagramToolHandler<T> {
418
+ I extends McpDiagramScopedInput = McpDiagramScopedInput,
419
+ O extends McpStructuredContent = McpStructuredContent
420
+ > extends BaseMcpDiagramToolHandler<I, O> {
393
421
  @inject(ActionDispatcher) protected actionDispatcher: ActionDispatcher;
394
422
 
395
423
  // Operation tools mutate the model — flip the read defaults. Concrete handlers override
@@ -400,7 +428,7 @@ export abstract class OperationMcpDiagramToolHandler<
400
428
  override readonly destructiveHint: boolean = false;
401
429
  override readonly idempotentHint: boolean = false;
402
430
 
403
- handle(params: T): Promise<McpToolResult> {
431
+ handle(params: I): Promise<McpToolResult> {
404
432
  return this.execute(() => {
405
433
  if (this.modelState.isReadonly) {
406
434
  throw new McpReadOnlyError();
@@ -431,5 +459,5 @@ export abstract class OperationMcpDiagramToolHandler<
431
459
  * are read off `new Constructor()` at MCP-session-init for SDK catalog registration, the same
432
460
  * trick `bindOperations` uses to read `operationType`.
433
461
  */
434
- export type McpDiagramToolHandlerConstructor = interfaces.Newable<BaseMcpDiagramToolHandler<any>>;
462
+ export type McpDiagramToolHandlerConstructor = interfaces.Newable<BaseMcpDiagramToolHandler<any, any>>;
435
463
  export const McpDiagramToolHandlerConstructor = Symbol('McpDiagramToolHandlerConstructor');
@@ -27,6 +27,7 @@ export const CountElementsOutputSchema = z.object({
27
27
  total: z.number().int().describe('Total element count across the diagram (root included).'),
28
28
  countsByType: z.record(z.string(), z.number().int()).describe('Element count grouped by `GModelElement.type`.')
29
29
  });
30
+ export type CountElementsOutput = z.infer<typeof CountElementsOutputSchema>;
30
31
 
31
32
  /**
32
33
  * Counts elements in the diagram, grouped by type. Cheap alternative to dumping the full
@@ -34,7 +35,7 @@ export const CountElementsOutputSchema = z.object({
34
35
  * elements of type X exist".
35
36
  */
36
37
  @injectable()
37
- export class CountElementsMcpToolHandler extends AbstractMcpDiagramToolHandler<CountElementsInput> {
38
+ export class CountElementsMcpToolHandler extends AbstractMcpDiagramToolHandler<CountElementsInput, CountElementsOutput> {
38
39
  static readonly NAME = 'count-elements';
39
40
  readonly name = CountElementsMcpToolHandler.NAME;
40
41
  override readonly title = 'Count Diagram Elements';
@@ -14,7 +14,13 @@
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
16
 
17
- import { ChangeRoutingPointsOperation, CreateEdgeOperation, DiagramConfiguration, EdgeCreationChecker } from '@eclipse-glsp/server';
17
+ import {
18
+ ChangeRoutingPointsOperation,
19
+ CreateEdgeOperation,
20
+ DiagramConfiguration,
21
+ EdgeCreationChecker,
22
+ GModelElement
23
+ } from '@eclipse-glsp/server';
18
24
  import { inject, injectable, optional } from 'inversify';
19
25
  import * as z from 'zod/v4';
20
26
  import { McpToolResult } from '../../server/mcp-handler-shared';
@@ -66,12 +72,13 @@ export const CreateEdgesOutputSchema = z.object({
66
72
  .optional()
67
73
  .describe('Per-input validation results. Present only in `dryRun` mode.')
68
74
  });
75
+ export type CreateEdgesOutput = z.infer<typeof CreateEdgesOutputSchema>;
69
76
 
70
77
  type EdgeInput = CreateEdgesInput['edges'][number];
71
78
  type ValidationResult = z.infer<typeof CreateEdgesValidationResultSchema>;
72
79
 
73
80
  @injectable()
74
- export class CreateEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<CreateEdgesInput> {
81
+ export class CreateEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<CreateEdgesInput, CreateEdgesOutput> {
75
82
  static readonly NAME = 'create-edges';
76
83
  readonly name = CreateEdgesMcpToolHandler.NAME;
77
84
  override readonly title = 'Create Diagram Edges';
@@ -106,7 +113,7 @@ export class CreateEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<Cr
106
113
  `${result.isValid ? 'valid' : `invalid (${result.reason})`}`
107
114
  )
108
115
  .join('\n');
109
- return this.success(summary, { createdEdges: [], errors: [], validationResults });
116
+ return this.success(summary, { createdEdges: [], dispatchedCommands: 0, errors: [], validationResults });
110
117
  }
111
118
 
112
119
  protected async runCreate(edges: EdgeInput[]): Promise<McpToolResult> {
@@ -137,11 +144,16 @@ export class CreateEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<Cr
137
144
  dispatchedOperations++;
138
145
 
139
146
  const afterIds = this.modelState.index.allIds();
140
- const newIds = afterIds.filter(id => !beforeIds.includes(id));
141
- const newElements = newIds.map(id => this.modelState.index.find(id)).filter(element => element?.type === elementTypeId);
142
- const newElement = newElements[0];
143
- if (newElements.length > 1) {
144
- this.logger.warn('More than 1 new element created');
147
+ const knownIds = new Set(beforeIds);
148
+ const newElements = afterIds
149
+ .filter(id => !knownIds.has(id))
150
+ .map(id => this.modelState.index.find(id))
151
+ .filter((element): element is GModelElement => element !== undefined);
152
+ // Fall back to the first new element — see `create-nodes`.
153
+ const typeMatches = newElements.filter(element => element.type === elementTypeId);
154
+ const newElement = typeMatches[0] ?? newElements[0];
155
+ if (newElements.length > 1 && typeMatches.length !== 1) {
156
+ this.logger.warn(`Ambiguous creation result for '${elementTypeId}': picked '${newElement?.id}' among new elements`);
145
157
  }
146
158
  beforeIds = afterIds;
147
159
 
@@ -14,7 +14,7 @@
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
16
 
17
- import { ApplyLabelEditOperation, CreateNodeOperation } from '@eclipse-glsp/server';
17
+ import { ApplyLabelEditOperation, CreateNodeOperation, GModelElement } from '@eclipse-glsp/server';
18
18
  import { injectable } from 'inversify';
19
19
  import * as z from 'zod/v4';
20
20
  import { McpToolResult } from '../../server/mcp-handler-shared';
@@ -33,7 +33,10 @@ export const CreateNodeSpecSchema = z.strictObject({
33
33
  elementTypeId: z
34
34
  .string()
35
35
  .describe('Element type ID (e.g., `task:manual`, `task:automated`). Use the `element-types` tool to discover valid IDs.'),
36
- position: position.describe('Position where the node should be created (absolute diagram coordinates)'),
36
+ position: position.describe(
37
+ 'Position where the node should be created, in absolute diagram coordinates — the server converts to ' +
38
+ 'parent-relative when `containerId` is set. Note `modify-nodes` takes parent-relative coordinates instead.'
39
+ ),
37
40
  text: z.string().optional().describe('Label text to use in case the given element type allows for labels.'),
38
41
  containerId: z.string().optional().describe('ID of the container element. If not provided, node is added to the root.'),
39
42
  // `args` stays open (`record(...)`) — adopter-specific extension surface for per-element-type creation hints.
@@ -57,9 +60,10 @@ export const CreateNodesOutputSchema = z.object({
57
60
  'Soft notices for inputs that succeeded with caveats (e.g. `text` supplied for a type whose elements have no editable label).'
58
61
  )
59
62
  });
63
+ export type CreateNodesOutput = z.infer<typeof CreateNodesOutputSchema>;
60
64
 
61
65
  @injectable()
62
- export class CreateNodesMcpToolHandler extends OperationMcpDiagramToolHandler<CreateNodesInput> {
66
+ export class CreateNodesMcpToolHandler extends OperationMcpDiagramToolHandler<CreateNodesInput, CreateNodesOutput> {
63
67
  static readonly NAME = 'create-nodes';
64
68
  readonly name = CreateNodesMcpToolHandler.NAME;
65
69
  override readonly title = 'Create Diagram Nodes';
@@ -91,11 +95,18 @@ export class CreateNodesMcpToolHandler extends OperationMcpDiagramToolHandler<Cr
91
95
  dispatchedOperations++;
92
96
 
93
97
  const afterIds = this.modelState.index.allIds();
94
- const newIds = afterIds.filter(id => !beforeIds.includes(id));
95
- const newElements = newIds.map(id => this.modelState.index.find(id)).filter(element => element?.type === elementTypeId);
96
- const newElement = newElements[0];
97
- if (newElements.length > 1) {
98
- this.logger.warn('More than 1 new element created');
98
+ const knownIds = new Set(beforeIds);
99
+ const newElements = afterIds
100
+ .filter(id => !knownIds.has(id))
101
+ .map(id => this.modelState.index.find(id))
102
+ .filter((element): element is GModelElement => element !== undefined);
103
+ // Fall back to the first new element: the built type may differ from the requested
104
+ // `elementTypeId` (type hint vs. concrete GModel type). `allIds()` is pre-order, so the
105
+ // first entry is the created element itself rather than one of its new children.
106
+ const typeMatches = newElements.filter(element => element.type === elementTypeId);
107
+ const newElement = typeMatches[0] ?? newElements[0];
108
+ if (newElements.length > 1 && typeMatches.length !== 1) {
109
+ this.logger.warn(`Ambiguous creation result for '${elementTypeId}': picked '${newElement?.id}' among new elements`);
99
110
  }
100
111
  beforeIds = afterIds;
101
112
 
@@ -38,9 +38,10 @@ export const DeleteElementsOutputSchema = z.object({
38
38
  ),
39
39
  dispatchedCommands
40
40
  });
41
+ export type DeleteElementsOutput = z.infer<typeof DeleteElementsOutputSchema>;
41
42
 
42
43
  @injectable()
43
- export class DeleteElementsMcpToolHandler extends OperationMcpDiagramToolHandler<DeleteElementsInput> {
44
+ export class DeleteElementsMcpToolHandler extends OperationMcpDiagramToolHandler<DeleteElementsInput, DeleteElementsOutput> {
44
45
  static readonly NAME = 'delete-elements';
45
46
  readonly name = DeleteElementsMcpToolHandler.NAME;
46
47
  override readonly title = 'Delete Diagram Elements';
@@ -38,9 +38,10 @@ export const DiagramModelOutputSchema = z.object({
38
38
  sessionId: z.string(),
39
39
  elements: z.array(DiagramModelElementSchema)
40
40
  });
41
+ export type DiagramModelOutput = z.infer<typeof DiagramModelOutputSchema>;
41
42
 
42
43
  @injectable()
43
- export class DiagramModelMcpToolHandler extends AbstractMcpDiagramToolHandler<DiagramModelInput> {
44
+ export class DiagramModelMcpToolHandler extends AbstractMcpDiagramToolHandler<DiagramModelInput, DiagramModelOutput> {
44
45
  static readonly NAME = 'diagram-model';
45
46
  readonly name = DiagramModelMcpToolHandler.NAME;
46
47
  override readonly title = 'Diagram Model Structure';
@@ -57,8 +58,10 @@ export class DiagramModelMcpToolHandler extends AbstractMcpDiagramToolHandler<Di
57
58
  protected createResult({ sessionId }: DiagramModelInput): McpToolResult {
58
59
  const root = this.modelState.root;
59
60
  const structured = this.serializer.serializeStructured(root);
60
- const count = Array.isArray(structured.elements) ? structured.elements.length : 0;
61
- return this.success(this.summarizeModel(root, count), { sessionId, ...structured });
61
+ const elements = (Array.isArray(structured.elements) ? structured.elements : []) as DiagramModelOutput['elements'];
62
+ // Spread first so extra top-level keys from an adopter serializer still reach the client;
63
+ // the narrowed `elements` then satisfies the declared output type.
64
+ return this.success(this.summarizeModel(root, elements.length), { sessionId, ...structured, elements });
62
65
  }
63
66
 
64
67
  /** Builds the LLM-facing summary line. Override to customize per-adopter wording. */
@@ -55,6 +55,7 @@ export const ElementTypesOutputSchema = z.object({
55
55
  nodeTypes: z.array(ElementTypeEntrySchema),
56
56
  edgeTypes: z.array(ElementTypeEntrySchema)
57
57
  });
58
+ export type ElementTypesOutput = z.infer<typeof ElementTypesOutputSchema>;
58
59
 
59
60
  /**
60
61
  * Server-scope so the LLM can discover types before opening a session. Lazy-harvests the
@@ -66,7 +67,7 @@ export const ElementTypesOutputSchema = z.object({
66
67
  * `bindElementTypesProvider()`; the harvest picks it up without further overrides.
67
68
  */
68
69
  @injectable()
69
- export class ElementTypesMcpToolHandler extends AbstractMcpToolHandler<ElementTypesInput> {
70
+ export class ElementTypesMcpToolHandler extends AbstractMcpToolHandler<ElementTypesInput, ElementTypesOutput> {
70
71
  @inject(DiagramModules) protected diagramModules: Map<string, ContainerModule[]>;
71
72
  @inject(InjectionContainer) protected serverContainer: Container;
72
73
  @inject(ClientSessionManager) protected clientSessionManager: ClientSessionManager;
@@ -89,8 +90,10 @@ export class ElementTypesMcpToolHandler extends AbstractMcpToolHandler<ElementTy
89
90
  const { nodeTypes, edgeTypes } = this.getElementTypes(resolved);
90
91
  return this.success(this.summarizeElementTypes(resolved, nodeTypes, edgeTypes), {
91
92
  diagramType: resolved,
92
- nodeTypes,
93
- edgeTypes
93
+ // `ElementTypeEntry` stays closed so a mistyped field on an adopter provider is still a
94
+ // compile error; the widening to the `loose()` schema shape happens here.
95
+ nodeTypes: nodeTypes as ElementTypesOutput['nodeTypes'],
96
+ edgeTypes: edgeTypes as ElementTypesOutput['edgeTypes']
94
97
  });
95
98
  }
96
99
 
@@ -27,10 +27,11 @@ export type GetSelectionInput = z.infer<typeof GetSelectionInputSchema>;
27
27
  export const GetSelectionOutputSchema = z.object({
28
28
  selectedElementIds: z.array(z.string()).describe('Aliased ids of the elements currently selected on the client.')
29
29
  });
30
+ export type GetSelectionOutput = z.infer<typeof GetSelectionOutputSchema>;
30
31
 
31
32
  /** Round-trips a sprotty {@link GetSelectionAction} via `ActionDispatcher.requestUntil` and awaits the matching `SelectionResult`. */
32
33
  @injectable()
33
- export class GetSelectionMcpToolHandler extends AbstractMcpDiagramToolHandler<GetSelectionInput> {
34
+ export class GetSelectionMcpToolHandler extends AbstractMcpDiagramToolHandler<GetSelectionInput, GetSelectionOutput> {
34
35
  /** Timeout (in ms) for awaiting the selection response from the client. Override via subclass + rebind. */
35
36
  protected readonly timeoutMs: number = 5000;
36
37
 
@@ -28,10 +28,11 @@ export const LayoutOutputSchema = z.object({
28
28
  applied: z.boolean().describe('Always true on success — surfaced for parity with other operations.'),
29
29
  dispatchedCommands
30
30
  });
31
+ export type LayoutOutput = z.infer<typeof LayoutOutputSchema>;
31
32
 
32
33
  /** Not registered by default: requires an adopter-supplied `LayoutEngine` to bind, which only some GLSP servers ship. */
33
34
  @injectable()
34
- export class LayoutMcpToolHandler extends OperationMcpDiagramToolHandler<LayoutInput> {
35
+ export class LayoutMcpToolHandler extends OperationMcpDiagramToolHandler<LayoutInput, LayoutOutput> {
35
36
  static readonly NAME = 'layout';
36
37
  readonly name = LayoutMcpToolHandler.NAME;
37
38
  override readonly title = 'Auto-Layout Diagram';
@@ -47,7 +48,12 @@ export class LayoutMcpToolHandler extends OperationMcpDiagramToolHandler<LayoutI
47
48
 
48
49
  @inject(LayoutEngine) @optional() protected layoutEngine?: LayoutEngine;
49
50
 
50
- /** Skip-bind when no `LayoutEngine` is bound every dispatch would otherwise no-op. */
51
+ /** Keep the tool out of the MCP catalog for diagram types that bind no `LayoutEngine`. */
52
+ override isSupportedByDiagramType(): boolean {
53
+ return this.layoutEngine !== undefined;
54
+ }
55
+
56
+ /** Skip session registration when no `LayoutEngine` is bound — every dispatch would otherwise no-op. */
51
57
  override canRegister(): boolean {
52
58
  return this.layoutEngine !== undefined;
53
59
  }
@@ -55,9 +55,10 @@ export const ModifyEdgesOutputSchema = z.object({
55
55
  dispatchedCommands,
56
56
  errors: z.array(z.string()).describe('Per-input failure messages; absent or empty when every input succeeded.')
57
57
  });
58
+ export type ModifyEdgesOutput = z.infer<typeof ModifyEdgesOutputSchema>;
58
59
 
59
60
  @injectable()
60
- export class ModifyEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<ModifyEdgesInput> {
61
+ export class ModifyEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<ModifyEdgesInput, ModifyEdgesOutput> {
61
62
  static readonly NAME = 'modify-edges';
62
63
  readonly name = ModifyEdgesMcpToolHandler.NAME;
63
64
  override readonly title = 'Modify Diagram Edges';
@@ -88,7 +89,6 @@ export class ModifyEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<Mo
88
89
  // instead of rejecting the whole call and losing the other outcomes.
89
90
  const dispatched: Array<{ promise: Promise<void>; realId: string; inputId: string }> = [];
90
91
  const errors: string[] = [];
91
- const modifiedRealIds = new Set<string>();
92
92
  elements.forEach(([change]) => {
93
93
  const { routingPoints } = change;
94
94
  const realId = this.aliasService.lookup(change.elementId);
@@ -114,7 +114,6 @@ export class ModifyEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<Mo
114
114
 
115
115
  const operation = ReconnectEdgeOperation.create({ edgeElementId: realId, sourceElementId, targetElementId });
116
116
  dispatched.push({ promise: this.actionDispatcher.dispatch(operation), realId, inputId: change.elementId });
117
- modifiedRealIds.add(realId);
118
117
  // Routing-point changes are skipped during a reconnect — the edge's path is recomputed from scratch.
119
118
  return;
120
119
  }
@@ -122,18 +121,25 @@ export class ModifyEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<Mo
122
121
  if (routingPoints) {
123
122
  const operation = ChangeRoutingPointsOperation.create([{ elementId: realId, newRoutingPoints: routingPoints }]);
124
123
  dispatched.push({ promise: this.actionDispatcher.dispatch(operation), realId, inputId: change.elementId });
125
- modifiedRealIds.add(realId);
124
+ return;
126
125
  }
126
+
127
+ errors.push(
128
+ `No change requested for edge '${change.elementId}' — provide \`sourceElementId\` + \`targetElementId\` ` +
129
+ 'to reconnect, or `routingPoints` to reroute.'
130
+ );
127
131
  });
128
132
 
133
+ // Two entries may target the same edge; one failing must not drop the other's success.
129
134
  const results = await Promise.allSettled(dispatched.map(entry => entry.promise));
135
+ const modifiedRealIds = new Set<string>();
130
136
  results.forEach((result, i) => {
137
+ const { realId, inputId } = dispatched[i];
131
138
  if (result.status === 'rejected') {
132
- const { realId, inputId } = dispatched[i];
133
139
  const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
134
140
  errors.push(`Failed to modify edge '${inputId}': ${reason}`);
135
- // The dispatch failed, so this id was *not* modified — drop it from the success list.
136
- modifiedRealIds.delete(realId);
141
+ } else {
142
+ modifiedRealIds.add(realId);
137
143
  }
138
144
  });
139
145
 
@@ -141,7 +147,7 @@ export class ModifyEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<Mo
141
147
  .map(realId => this.describeElement(realId))
142
148
  .filter((entry): entry is NonNullable<typeof entry> => entry !== undefined);
143
149
  return this.success(
144
- `Successfully modified ${edges.length - errors.length} edge(s) (in ${dispatched.length} commands)${formatNoticeList('errors', errors)}`,
150
+ `Successfully modified ${modifiedEdges.length} edge(s) (in ${dispatched.length} commands)${formatNoticeList('errors', errors)}`,
145
151
  { modifiedEdges, dispatchedCommands: dispatched.length, errors }
146
152
  );
147
153
  }