@eclipse-glsp/server-mcp 2.8.0-next.9 → 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
@@ -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, ChangeBoundsOperation, GEdge, GShapeElement } from '@eclipse-glsp/server';
17
+ import { ApplyLabelEditOperation, ChangeBoundsOperation, GEdge, GModelRoot, GNode } from '@eclipse-glsp/server';
18
18
  import { injectable } from 'inversify';
19
19
  import * as z from 'zod/v4';
20
20
  import { McpToolError, McpToolResult } from '../../server/mcp-handler-shared';
@@ -37,7 +37,13 @@ export const NodeSizeSchema = z.strictObject({
37
37
  /** Single node-modification entry. Strict so an LLM-typoed field surfaces as a validation error instead of being silently dropped. */
38
38
  export const ModifyNodeSpecSchema = z.strictObject({
39
39
  elementId,
40
- position: position.optional().describe('Position where the node should be moved to (absolute diagram coordinates)'),
40
+ position: position
41
+ .optional()
42
+ .describe(
43
+ 'Position where the node should be moved to, relative to the parent element ' +
44
+ '(identical to absolute diagram coordinates for direct children of the root, which is the common case). ' +
45
+ 'Matches the `position` reported by `query-elements`. Note `create-nodes` takes absolute coordinates instead.'
46
+ ),
41
47
  size: NodeSizeSchema.optional().describe('New size of the node'),
42
48
  text: z.string().optional().describe("Label text to use instead (given that the element's type allows for labels).")
43
49
  });
@@ -55,15 +61,17 @@ export const ModifyNodesOutputSchema = z.object({
55
61
  .array(ElementIdentitySchema)
56
62
  .describe('Identity of each node whose change request was dispatched (post-modification labels).'),
57
63
  dispatchedCommands,
64
+ errors: z.array(z.string()).describe('Per-input failure messages; absent or empty when every input succeeded.'),
58
65
  warnings: z
59
66
  .array(z.string())
60
67
  .describe(
61
68
  'Soft notices for inputs whose change applied with caveats (e.g. `text` supplied for a node whose type has no editable label).'
62
69
  )
63
70
  });
71
+ export type ModifyNodesOutput = z.infer<typeof ModifyNodesOutputSchema>;
64
72
 
65
73
  @injectable()
66
- export class ModifyNodesMcpToolHandler extends OperationMcpDiagramToolHandler<ModifyNodesInput> {
74
+ export class ModifyNodesMcpToolHandler extends OperationMcpDiagramToolHandler<ModifyNodesInput, ModifyNodesOutput> {
67
75
  static readonly NAME = 'modify-nodes';
68
76
  readonly name = ModifyNodesMcpToolHandler.NAME;
69
77
  override readonly title = 'Modify Diagram Nodes';
@@ -88,53 +96,83 @@ export class ModifyNodesMcpToolHandler extends OperationMcpDiagramToolHandler<Mo
88
96
  throw new McpToolError(`modify-nodes does not accept edges — got: ${wrongType.join(', ')}. Use modify-edges for edges.`);
89
97
  }
90
98
 
91
- // Reject any other non-shape kinds (labels, compartments, ports, custom kinds) they
92
- // reach the index too and would silently produce no-op or undefined-bounds operations.
93
- const nonShape = elements.filter(([, element]) => !(element instanceof GShapeElement)).map(([change]) => `'${change.elementId}'`);
94
- if (nonShape.length) {
99
+ // The root has no bounds of its own, and a `text` edit against it would rename whatever
100
+ // top-level `GLabel` the label provider happens to find first.
101
+ const roots = elements.filter(([, element]) => element instanceof GModelRoot).map(([change]) => `'${change.elementId}'`);
102
+ if (roots.length) {
103
+ throw new McpToolError(
104
+ `modify-nodes does not accept the diagram root — got: ${roots.join(', ')}. Target a node inside the diagram instead.`
105
+ );
106
+ }
107
+
108
+ // Core's `GModelChangeBoundsOperationHandler` only applies bounds to a `GNode`
109
+ // (`findByClass`). Label-only edits stay open to every element kind.
110
+ const unmovable = elements
111
+ .filter(([change, element]) => (change.position || change.size) && !(element instanceof GNode))
112
+ .map(([change, element]) => `'${change.elementId}' (type '${element.type}')`);
113
+ if (unmovable.length) {
95
114
  throw new McpToolError(
96
- `modify-nodes only accepts shape elements — got: ${nonShape.join(', ')}. ` +
97
- 'Use `query-elements` (inspect mode) to find shape ids, or pick the parent shape.'
115
+ `modify-nodes can only change \`position\` / \`size\` of nodes — got: ${unmovable.join(', ')}. ` +
116
+ 'Use `query-elements` (inspect mode) to find node ids, or target the parent node.'
98
117
  );
99
118
  }
100
119
 
101
- // Modifications are independent of each other — dispatch in parallel.
102
- const promises: Promise<void>[] = [];
120
+ // Modifications are independent of each other — dispatch in parallel. `allSettled` so one
121
+ // failed dispatch surfaces in `errors` instead of rejecting the whole call and losing the
122
+ // other outcomes, which have already mutated the model.
123
+ const dispatched: Array<{ promise: Promise<void>; inputId: string }> = [];
124
+ const errors: string[] = [];
103
125
  const warnings: string[] = [];
104
126
  elements.forEach(([change, element]) => {
105
- // Guaranteed non-null and shape-typed by the missing-elements / nonShape checks above.
106
- const resolved = element as GShapeElement;
107
127
  const { size, position, text } = change;
108
128
  const realId = this.aliasService.lookup(change.elementId);
109
129
 
110
- if (size || position) {
111
- const newSize = size ?? resolved.size;
112
- const newPosition = position ?? resolved.position;
130
+ if (!size && !position && !text) {
131
+ errors.push(`No change requested for node '${change.elementId}' — provide \`position\`, \`size\` or a non-empty \`text\`.`);
132
+ return;
133
+ }
134
+
135
+ if ((size || position) && element instanceof GNode) {
136
+ const newSize = size ?? element.size;
137
+ const newPosition = position ?? element.position;
113
138
 
114
139
  const operation = ChangeBoundsOperation.create([{ elementId: realId, newSize, newPosition }]);
115
- promises.push(this.actionDispatcher.dispatch(operation));
140
+ dispatched.push({ promise: this.actionDispatcher.dispatch(operation), inputId: change.elementId });
116
141
  }
117
142
 
118
143
  if (text) {
119
- const labelId = this.labelProvider.getLabel(resolved)?.id;
144
+ const labelId = this.labelProvider.getLabel(element)?.id;
120
145
  if (labelId) {
121
- promises.push(this.actionDispatcher.dispatch(ApplyLabelEditOperation.create({ labelId, text })));
146
+ const operation = ApplyLabelEditOperation.create({ labelId, text });
147
+ dispatched.push({ promise: this.actionDispatcher.dispatch(operation), inputId: change.elementId });
122
148
  } else {
123
149
  warnings.push(
124
- `Ignored \`text\` for '${change.elementId}' (type '${resolved.type}') — this element has no editable label.`
150
+ `Ignored \`text\` for '${change.elementId}' (type '${element.type}') — this element has no editable label.`
125
151
  );
126
152
  }
127
153
  }
128
154
  });
129
155
 
130
- await Promise.all(promises);
156
+ const results = await Promise.allSettled(dispatched.map(entry => entry.promise));
157
+ const modifiedInputIds = new Set<string>();
158
+ results.forEach((result, index) => {
159
+ const { inputId } = dispatched[index];
160
+ if (result.status === 'rejected') {
161
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
162
+ errors.push(`Failed to modify node '${inputId}': ${reason}`);
163
+ } else {
164
+ modifiedInputIds.add(inputId);
165
+ }
166
+ });
131
167
 
132
- const modifiedNodes = nodes
133
- .map(change => this.describeElement(change.elementId))
168
+ const modifiedNodes = [...modifiedInputIds]
169
+ .map(inputId => this.describeElement(inputId))
134
170
  .filter((entry): entry is NonNullable<typeof entry> => entry !== undefined);
135
171
  return this.success(
136
- `Successfully modified ${nodes.length} node(s) (in ${promises.length} commands)` + formatNoticeList('warnings', warnings),
137
- { modifiedNodes, dispatchedCommands: promises.length, warnings }
172
+ `Successfully modified ${modifiedNodes.length} node(s) (in ${dispatched.length} commands)` +
173
+ formatNoticeList('errors', errors) +
174
+ formatNoticeList('warnings', warnings),
175
+ { modifiedNodes, dispatchedCommands: dispatched.length, errors, warnings }
138
176
  );
139
177
  }
140
178
  }
@@ -60,10 +60,11 @@ export const QueryElementsOutputSchema = z.object({
60
60
  'The `elements` array then includes the container plus its descendants.'
61
61
  )
62
62
  });
63
+ export type QueryElementsOutput = z.infer<typeof QueryElementsOutputSchema>;
63
64
 
64
65
  /** Two-mode element query — list/filter or inspect-by-id, discriminated by `elementIds` presence. */
65
66
  @injectable()
66
- export class QueryElementsMcpToolHandler extends AbstractMcpDiagramToolHandler<QueryElementsInput> {
67
+ export class QueryElementsMcpToolHandler extends AbstractMcpDiagramToolHandler<QueryElementsInput, QueryElementsOutput> {
67
68
  static readonly NAME = 'query-elements';
68
69
  readonly name = QueryElementsMcpToolHandler.NAME;
69
70
  override readonly title = 'Query Diagram Elements';
@@ -22,19 +22,20 @@ import { McpDiagramScopedInputSchema } from '../../server/mcp-input-schemas';
22
22
  import { OperationMcpDiagramToolHandler } from '../../server/mcp-tool-handler';
23
23
 
24
24
  export const RedoInputSchema = McpDiagramScopedInputSchema.extend({
25
- commandsToRedo: z.number().min(1).default(1).describe('Number of commands to redo. Defaults to 1 (most recent undone command).')
25
+ commandsToRedo: z.number().int().min(1).default(1).describe('Number of commands to redo. Defaults to 1 (most recent undone command).')
26
26
  });
27
27
  export type RedoInput = z.infer<typeof RedoInputSchema>;
28
28
 
29
29
  export const RedoOutputSchema = z.object({
30
30
  commandsRedone: z.number().int().describe('Number of previously-undone commands re-applied.')
31
31
  });
32
+ export type RedoOutput = z.infer<typeof RedoOutputSchema>;
32
33
 
33
34
  /**
34
35
  * Redo a given number of the most recent undone actions on the command stack.
35
36
  */
36
37
  @injectable()
37
- export class RedoMcpToolHandler extends OperationMcpDiagramToolHandler<RedoInput> {
38
+ export class RedoMcpToolHandler extends OperationMcpDiagramToolHandler<RedoInput, RedoOutput> {
38
39
  static readonly NAME = 'redo';
39
40
  readonly name = RedoMcpToolHandler.NAME;
40
41
  override readonly title = 'Redo Diagram Commands';
@@ -55,10 +56,14 @@ export class RedoMcpToolHandler extends OperationMcpDiagramToolHandler<RedoInput
55
56
  );
56
57
  }
57
58
 
58
- for (let i = 0; i < commandsToRedo; i++) {
59
+ // The stack may hold fewer commands than requested.
60
+ let commandsRedone = 0;
61
+ while (commandsRedone < commandsToRedo && this.commandStack.canRedo()) {
59
62
  await this.actionDispatcher.dispatch(RedoAction.create());
63
+ commandsRedone++;
60
64
  }
61
65
 
62
- return this.success('Redo successful', { commandsRedone: commandsToRedo });
66
+ const shortfall = commandsRedone < commandsToRedo ? ` (${commandsToRedo} requested; the redo stack held no more commands)` : '';
67
+ return this.success(`Redo successful: re-applied ${commandsRedone} command(s)${shortfall}`, { commandsRedone });
63
68
  }
64
69
  }
@@ -34,10 +34,11 @@ export const SaveModelOutputSchema = z.object({
34
34
  .optional()
35
35
  .describe('Destination file URI when explicitly provided; absent when saving to the original source location.')
36
36
  });
37
+ export type SaveModelOutput = z.infer<typeof SaveModelOutputSchema>;
37
38
 
38
39
  /** Doesn't extend `OperationMcpDiagramToolHandler` — saving a read-only model is a legitimate adopter scenario (e.g. "save as" to a writable location). */
39
40
  @injectable()
40
- export class SaveModelMcpToolHandler extends AbstractMcpDiagramToolHandler<SaveModelInput> {
41
+ export class SaveModelMcpToolHandler extends AbstractMcpDiagramToolHandler<SaveModelInput, SaveModelOutput> {
41
42
  static readonly NAME = 'save-model';
42
43
  readonly name = SaveModelMcpToolHandler.NAME;
43
44
  override readonly title = 'Save Diagram Model';
@@ -62,7 +63,9 @@ export class SaveModelMcpToolHandler extends AbstractMcpDiagramToolHandler<SaveM
62
63
  @inject(McpLogger) protected mcpLogger: McpLogger;
63
64
 
64
65
  protected async createResult({ fileUri }: SaveModelInput): Promise<McpToolResult> {
65
- if (!this.commandStack.isDirty) {
66
+ // Only save-in-place can be short-circuited: a "save as" destination has no copy yet,
67
+ // dirty or not.
68
+ if (!fileUri && !this.commandStack.isDirty) {
66
69
  this.mcpLogger.info('save-model: nothing to save');
67
70
  return this.success('No changes to save', { saved: false, fileUri });
68
71
  }
@@ -40,9 +40,10 @@ export type SessionInfoRow = z.infer<typeof SessionInfoRowSchema>;
40
40
  export const SessionInfoOutputSchema = z.object({
41
41
  sessions: z.array(SessionInfoRowSchema)
42
42
  });
43
+ export type SessionInfoOutput = z.infer<typeof SessionInfoOutputSchema>;
43
44
 
44
45
  @injectable()
45
- export class SessionInfoMcpToolHandler extends AbstractMcpToolHandler<SessionInfoInput> {
46
+ export class SessionInfoMcpToolHandler extends AbstractMcpToolHandler<SessionInfoInput, SessionInfoOutput> {
46
47
  @inject(ClientSessionManager) protected clientSessionManager: ClientSessionManager;
47
48
 
48
49
  static readonly NAME = 'session-info';
@@ -18,14 +18,16 @@ import { ActionDispatcher, SelectAction, SelectAllAction } from '@eclipse-glsp/s
18
18
  import { inject, injectable } from 'inversify';
19
19
  import * as z from 'zod/v4';
20
20
  import { McpToolResult } from '../../server/mcp-handler-shared';
21
- import { McpDiagramScopedInputSchema, elementIds } from '../../server/mcp-input-schemas';
21
+ import { McpDiagramScopedInputSchema, elementIdsAllowingEmpty } from '../../server/mcp-input-schemas';
22
22
  import { AbstractMcpDiagramToolHandler } from '../../server/mcp-tool-handler';
23
23
 
24
24
  export const SetSelectionInputSchema = McpDiagramScopedInputSchema.extend({
25
- selectedElementIds: elementIds
25
+ selectedElementIds: elementIdsAllowingEmpty
26
26
  .optional()
27
- .describe('Element IDs to select. Pass an empty array (or omit + set `clear: true`) to clear the selection.'),
28
- deselectedElementIds: elementIds.optional().describe('Element IDs to remove from the selection. Used to subtract without replacing.'),
27
+ .describe('Element IDs to select. Pass an empty array (or omit the field) together with `clear: true` to clear the selection.'),
28
+ deselectedElementIds: elementIdsAllowingEmpty
29
+ .optional()
30
+ .describe('Element IDs to remove from the selection. Used to subtract without replacing.'),
29
31
  clear: z
30
32
  .boolean()
31
33
  .optional()
@@ -38,6 +40,7 @@ export const SetSelectionOutputSchema = z.object({
38
40
  deselectedElementIds: z.array(z.string()).describe('Aliased ids requested for deselection.'),
39
41
  cleared: z.boolean().describe('Whether the existing selection was cleared before applying.')
40
42
  });
43
+ export type SetSelectionOutput = z.infer<typeof SetSelectionOutputSchema>;
41
44
 
42
45
  /**
43
46
  * Pushes a selection change to the client. Counterpart to `get-selection` — useful for the
@@ -48,7 +51,7 @@ export const SetSelectionOutputSchema = z.object({
48
51
  * even though the underlying GModel is unaffected.
49
52
  */
50
53
  @injectable()
51
- export class SetSelectionMcpToolHandler extends AbstractMcpDiagramToolHandler<SetSelectionInput> {
54
+ export class SetSelectionMcpToolHandler extends AbstractMcpDiagramToolHandler<SetSelectionInput, SetSelectionOutput> {
52
55
  static readonly NAME = 'set-selection';
53
56
  readonly name = SetSelectionMcpToolHandler.NAME;
54
57
  override readonly title = 'Set Diagram Selection';
@@ -74,9 +74,10 @@ export const SetViewOutputSchema = z.object({
74
74
  .optional()
75
75
  .describe("Resolved final viewport when `action: 'set-viewport'` was applied (current values merged with the supplied overrides).")
76
76
  });
77
+ export type SetViewOutput = z.infer<typeof SetViewOutputSchema>;
77
78
 
78
79
  @injectable()
79
- export class SetViewMcpToolHandler extends AbstractMcpDiagramToolHandler<SetViewInput> {
80
+ export class SetViewMcpToolHandler extends AbstractMcpDiagramToolHandler<SetViewInput, SetViewOutput> {
80
81
  /** Timeout (in ms) for the `GetViewportAction` round-trip used by `set-viewport` partial updates. Override via subclass + rebind. */
81
82
  protected readonly viewportQueryTimeoutMs: number = 5000;
82
83
 
@@ -102,7 +103,7 @@ export class SetViewMcpToolHandler extends AbstractMcpDiagramToolHandler<SetView
102
103
  if (action === 'set-viewport') {
103
104
  return this.applyExplicitViewport(zoom, scroll);
104
105
  }
105
- const resolvedIds = elementIds ? elementIds.map(id => this.aliasService.lookup(id)) : this.modelState.index.allIds();
106
+ const resolvedIds = elementIds ? this.resolveExistingIds(elementIds) : this.modelState.index.allIds();
106
107
  const dispatchAction = this.buildIntentAction(action, resolvedIds);
107
108
  await this.actionDispatcher.dispatch(dispatchAction);
108
109
  return this.success('Viewport successfully changed', { action, targetIds: this.encodeIds(resolvedIds) });
@@ -22,19 +22,20 @@ import { McpDiagramScopedInputSchema } from '../../server/mcp-input-schemas';
22
22
  import { OperationMcpDiagramToolHandler } from '../../server/mcp-tool-handler';
23
23
 
24
24
  export const UndoInputSchema = McpDiagramScopedInputSchema.extend({
25
- commandsToUndo: z.number().min(1).default(1).describe('Number of commands to undo. Defaults to 1 (most recent command).')
25
+ commandsToUndo: z.number().int().min(1).default(1).describe('Number of commands to undo. Defaults to 1 (most recent command).')
26
26
  });
27
27
  export type UndoInput = z.infer<typeof UndoInputSchema>;
28
28
 
29
29
  export const UndoOutputSchema = z.object({
30
30
  commandsUndone: z.number().int().describe('Number of commands actually reverted.')
31
31
  });
32
+ export type UndoOutput = z.infer<typeof UndoOutputSchema>;
32
33
 
33
34
  /**
34
35
  * Undo a given number of the most recent actions on the command stack.
35
36
  */
36
37
  @injectable()
37
- export class UndoMcpToolHandler extends OperationMcpDiagramToolHandler<UndoInput> {
38
+ export class UndoMcpToolHandler extends OperationMcpDiagramToolHandler<UndoInput, UndoOutput> {
38
39
  static readonly NAME = 'undo';
39
40
  readonly name = UndoMcpToolHandler.NAME;
40
41
  override readonly title = 'Undo Diagram Commands';
@@ -54,10 +55,14 @@ export class UndoMcpToolHandler extends OperationMcpDiagramToolHandler<UndoInput
54
55
  throw new McpToolError('Nothing to undo (undo stack is empty; the model is at its initial state for this session).');
55
56
  }
56
57
 
57
- for (let i = 0; i < commandsToUndo; i++) {
58
+ // The stack may hold fewer commands than requested.
59
+ let commandsUndone = 0;
60
+ while (commandsUndone < commandsToUndo && this.commandStack.canUndo()) {
58
61
  await this.actionDispatcher.dispatch(UndoAction.create());
62
+ commandsUndone++;
59
63
  }
60
64
 
61
- return this.success('Undo successful', { commandsUndone: commandsToUndo });
65
+ const shortfall = commandsUndone < commandsToUndo ? ` (${commandsToUndo} requested; the undo stack held no more commands)` : '';
66
+ return this.success(`Undo successful: reverted ${commandsUndone} command(s)${shortfall}`, { commandsUndone });
62
67
  }
63
68
  }