@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.
- package/lib/common/modules/mcp-diagram-module.js +1 -1
- package/lib/common/modules/mcp-diagram-module.js.map +1 -1
- package/lib/common/server/mcp-diagram-handler-dispatcher.d.ts +20 -4
- package/lib/common/server/mcp-diagram-handler-dispatcher.d.ts.map +1 -1
- package/lib/common/server/mcp-diagram-handler-dispatcher.js +36 -5
- package/lib/common/server/mcp-diagram-handler-dispatcher.js.map +1 -1
- package/lib/common/server/mcp-input-schemas.d.ts +2 -0
- package/lib/common/server/mcp-input-schemas.d.ts.map +1 -1
- package/lib/common/server/mcp-input-schemas.js +3 -1
- package/lib/common/server/mcp-input-schemas.js.map +1 -1
- package/lib/common/server/mcp-resource-handler.d.ts +10 -1
- package/lib/common/server/mcp-resource-handler.d.ts.map +1 -1
- package/lib/common/server/mcp-resource-handler.js +12 -1
- package/lib/common/server/mcp-resource-handler.js.map +1 -1
- package/lib/common/server/mcp-tool-handler.d.ts +33 -16
- package/lib/common/server/mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/server/mcp-tool-handler.js +18 -1
- package/lib/common/server/mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/count-elements-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/count-elements-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/count-elements-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/create-edges-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/create-edges-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/create-edges-mcp-tool-handler.js +11 -6
- package/lib/common/tools/handlers/create-edges-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.js +14 -6
- package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.js +4 -2
- package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/element-types-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/element-types-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/element-types-mcp-tool-handler.js +4 -2
- package/lib/common/tools/handlers/element-types-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/get-selection-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/get-selection-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/get-selection-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/layout-mcp-tool-handler.d.ts +5 -2
- package/lib/common/tools/handlers/layout-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/layout-mcp-tool-handler.js +5 -1
- package/lib/common/tools/handlers/layout-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.js +10 -7
- package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.d.ts +4 -1
- package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.js +54 -22
- package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/query-elements-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/query-elements-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/query-elements-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/redo-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/redo-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/redo-mcp-tool-handler.js +7 -3
- package/lib/common/tools/handlers/redo-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/save-model-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/save-model-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/save-model-mcp-tool-handler.js +3 -1
- package/lib/common/tools/handlers/save-model-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/session-info-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/session-info-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/session-info-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/set-selection-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/set-selection-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/set-selection-mcp-tool-handler.js +5 -3
- package/lib/common/tools/handlers/set-selection-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/set-view-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/set-view-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/set-view-mcp-tool-handler.js +1 -1
- package/lib/common/tools/handlers/set-view-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/undo-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/undo-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/undo-mcp-tool-handler.js +7 -3
- package/lib/common/tools/handlers/undo-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.d.ts +2 -1
- package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.d.ts.map +1 -1
- package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.js +0 -0
- package/lib/common/tools/handlers/validate-diagram-mcp-tool-handler.js.map +1 -1
- package/package.json +2 -2
- package/src/common/modules/mcp-diagram-module.ts +1 -1
- package/src/common/server/mcp-diagram-handler-dispatcher.ts +56 -5
- package/src/common/server/mcp-input-schemas.ts +3 -0
- package/src/common/server/mcp-resource-handler.ts +13 -1
- package/src/common/server/mcp-tool-handler.ts +47 -19
- package/src/common/tools/handlers/count-elements-mcp-tool-handler.ts +2 -1
- package/src/common/tools/handlers/create-edges-mcp-tool-handler.ts +20 -8
- package/src/common/tools/handlers/create-nodes-mcp-tool-handler.ts +19 -8
- package/src/common/tools/handlers/delete-elements-mcp-tool-handler.ts +2 -1
- package/src/common/tools/handlers/diagram-model-mcp-tool-handler.ts +6 -3
- package/src/common/tools/handlers/element-types-mcp-tool-handler.ts +6 -3
- package/src/common/tools/handlers/get-selection-mcp-tool-handler.ts +2 -1
- package/src/common/tools/handlers/layout-mcp-tool-handler.ts +8 -2
- package/src/common/tools/handlers/modify-edges-mcp-tool-handler.ts +14 -8
- package/src/common/tools/handlers/modify-nodes-mcp-tool-handler.ts +63 -25
- package/src/common/tools/handlers/query-elements-mcp-tool-handler.ts +2 -1
- package/src/common/tools/handlers/redo-mcp-tool-handler.ts +9 -4
- package/src/common/tools/handlers/save-model-mcp-tool-handler.ts +5 -2
- package/src/common/tools/handlers/session-info-mcp-tool-handler.ts +2 -1
- package/src/common/tools/handlers/set-selection-mcp-tool-handler.ts +8 -5
- package/src/common/tools/handlers/set-view-mcp-tool-handler.ts +3 -2
- package/src/common/tools/handlers/undo-mcp-tool-handler.ts +9 -4
- 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
|
|
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.
|
|
104
|
-
*
|
|
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 =
|
|
127
|
-
|
|
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
|
-
/**
|
|
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?:
|
|
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<
|
|
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:
|
|
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
|
|
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<
|
|
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:
|
|
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:
|
|
273
|
+
abstract handle(params: I): Promise<McpToolResult>;
|
|
266
274
|
|
|
267
|
-
/**
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
392
|
-
|
|
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:
|
|
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 {
|
|
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
|
|
141
|
-
const newElements =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
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
|
|
95
|
-
const newElements =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
modifiedRealIds.
|
|
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 ${
|
|
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
|
}
|