@eclipse-glsp/server-mcp 2.7.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +642 -0
- package/README.md +57 -0
- package/lib/index.d.ts +66 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +82 -0
- package/lib/index.js.map +1 -0
- package/lib/modules/mcp-diagram-module.d.ts +123 -0
- package/lib/modules/mcp-diagram-module.d.ts.map +1 -0
- package/lib/modules/mcp-diagram-module.js +202 -0
- package/lib/modules/mcp-diagram-module.js.map +1 -0
- package/lib/modules/mcp-server-module.d.ts +143 -0
- package/lib/modules/mcp-server-module.d.ts.map +1 -0
- package/lib/modules/mcp-server-module.js +251 -0
- package/lib/modules/mcp-server-module.js.map +1 -0
- package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.d.ts +44 -0
- package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.d.ts.map +1 -0
- package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.js +102 -0
- package/lib/prompts/handlers/describe-diagram-mcp-prompt-handler.js.map +1 -0
- package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.d.ts +44 -0
- package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.d.ts.map +1 -0
- package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.js +99 -0
- package/lib/prompts/handlers/suggest-improvements-mcp-prompt-handler.js.map +1 -0
- package/lib/resources/handlers/diagram-png-mcp-resource-handler.d.ts +84 -0
- package/lib/resources/handlers/diagram-png-mcp-resource-handler.d.ts.map +1 -0
- package/lib/resources/handlers/diagram-png-mcp-resource-handler.js +177 -0
- package/lib/resources/handlers/diagram-png-mcp-resource-handler.js.map +1 -0
- package/lib/resources/handlers/diagram-svg-mcp-resource-handler.d.ts +55 -0
- package/lib/resources/handlers/diagram-svg-mcp-resource-handler.d.ts.map +1 -0
- package/lib/resources/handlers/diagram-svg-mcp-resource-handler.js +99 -0
- package/lib/resources/handlers/diagram-svg-mcp-resource-handler.js.map +1 -0
- package/lib/resources/services/element-types-provider.d.ts +65 -0
- package/lib/resources/services/element-types-provider.d.ts.map +1 -0
- package/lib/resources/services/element-types-provider.js +81 -0
- package/lib/resources/services/element-types-provider.js.map +1 -0
- package/lib/resources/services/mcp-model-serializer.d.ts +78 -0
- package/lib/resources/services/mcp-model-serializer.d.ts.map +1 -0
- package/lib/resources/services/mcp-model-serializer.js +188 -0
- package/lib/resources/services/mcp-model-serializer.js.map +1 -0
- package/lib/server/glsp-mcp-server.d.ts +82 -0
- package/lib/server/glsp-mcp-server.d.ts.map +1 -0
- package/lib/server/glsp-mcp-server.js +140 -0
- package/lib/server/glsp-mcp-server.js.map +1 -0
- package/lib/server/lru-event-store.d.ts +53 -0
- package/lib/server/lru-event-store.d.ts.map +1 -0
- package/lib/server/lru-event-store.js +100 -0
- package/lib/server/lru-event-store.js.map +1 -0
- package/lib/server/mcp-diagram-handler-dispatcher.d.ts +144 -0
- package/lib/server/mcp-diagram-handler-dispatcher.d.ts.map +1 -0
- package/lib/server/mcp-diagram-handler-dispatcher.js +382 -0
- package/lib/server/mcp-diagram-handler-dispatcher.js.map +1 -0
- package/lib/server/mcp-diagram-prompt-handler-registry.d.ts +33 -0
- package/lib/server/mcp-diagram-prompt-handler-registry.d.ts.map +1 -0
- package/lib/server/mcp-diagram-prompt-handler-registry.js +76 -0
- package/lib/server/mcp-diagram-prompt-handler-registry.js.map +1 -0
- package/lib/server/mcp-diagram-resource-handler-registry.d.ts +35 -0
- package/lib/server/mcp-diagram-resource-handler-registry.d.ts.map +1 -0
- package/lib/server/mcp-diagram-resource-handler-registry.js +94 -0
- package/lib/server/mcp-diagram-resource-handler-registry.js.map +1 -0
- package/lib/server/mcp-diagram-tool-handler-registry.d.ts +57 -0
- package/lib/server/mcp-diagram-tool-handler-registry.d.ts.map +1 -0
- package/lib/server/mcp-diagram-tool-handler-registry.js +111 -0
- package/lib/server/mcp-diagram-tool-handler-registry.js.map +1 -0
- package/lib/server/mcp-handler-shared.d.ts +142 -0
- package/lib/server/mcp-handler-shared.d.ts.map +1 -0
- package/lib/server/mcp-handler-shared.js +199 -0
- package/lib/server/mcp-handler-shared.js.map +1 -0
- package/lib/server/mcp-http-transport.d.ts +93 -0
- package/lib/server/mcp-http-transport.d.ts.map +1 -0
- package/lib/server/mcp-http-transport.js +350 -0
- package/lib/server/mcp-http-transport.js.map +1 -0
- package/lib/server/mcp-id-alias-service.d.ts +70 -0
- package/lib/server/mcp-id-alias-service.d.ts.map +1 -0
- package/lib/server/mcp-id-alias-service.js +85 -0
- package/lib/server/mcp-id-alias-service.js.map +1 -0
- package/lib/server/mcp-input-schemas.d.ts +73 -0
- package/lib/server/mcp-input-schemas.d.ts.map +1 -0
- package/lib/server/mcp-input-schemas.js +67 -0
- package/lib/server/mcp-input-schemas.js.map +1 -0
- package/lib/server/mcp-label-provider.d.ts +45 -0
- package/lib/server/mcp-label-provider.d.ts.map +1 -0
- package/lib/server/mcp-label-provider.js +42 -0
- package/lib/server/mcp-label-provider.js.map +1 -0
- package/lib/server/mcp-log-level-registry.d.ts +54 -0
- package/lib/server/mcp-log-level-registry.d.ts.map +1 -0
- package/lib/server/mcp-log-level-registry.js +80 -0
- package/lib/server/mcp-log-level-registry.js.map +1 -0
- package/lib/server/mcp-logger.d.ts +59 -0
- package/lib/server/mcp-logger.d.ts.map +1 -0
- package/lib/server/mcp-logger.js +104 -0
- package/lib/server/mcp-logger.js.map +1 -0
- package/lib/server/mcp-mime-types.d.ts +28 -0
- package/lib/server/mcp-mime-types.d.ts.map +1 -0
- package/lib/server/mcp-mime-types.js +18 -0
- package/lib/server/mcp-mime-types.js.map +1 -0
- package/lib/server/mcp-options.d.ts +39 -0
- package/lib/server/mcp-options.d.ts.map +1 -0
- package/lib/server/mcp-options.js +53 -0
- package/lib/server/mcp-options.js.map +1 -0
- package/lib/server/mcp-progress-reporter.d.ts +48 -0
- package/lib/server/mcp-progress-reporter.d.ts.map +1 -0
- package/lib/server/mcp-progress-reporter.js +66 -0
- package/lib/server/mcp-progress-reporter.js.map +1 -0
- package/lib/server/mcp-prompt-handler.d.ts +120 -0
- package/lib/server/mcp-prompt-handler.d.ts.map +1 -0
- package/lib/server/mcp-prompt-handler.js +131 -0
- package/lib/server/mcp-prompt-handler.js.map +1 -0
- package/lib/server/mcp-request-context.d.ts +37 -0
- package/lib/server/mcp-request-context.d.ts.map +1 -0
- package/lib/server/mcp-request-context.js +37 -0
- package/lib/server/mcp-request-context.js.map +1 -0
- package/lib/server/mcp-resource-handler.d.ts +212 -0
- package/lib/server/mcp-resource-handler.d.ts.map +1 -0
- package/lib/server/mcp-resource-handler.js +298 -0
- package/lib/server/mcp-resource-handler.js.map +1 -0
- package/lib/server/mcp-server-launcher.d.ts +143 -0
- package/lib/server/mcp-server-launcher.d.ts.map +1 -0
- package/lib/server/mcp-server-launcher.js +355 -0
- package/lib/server/mcp-server-launcher.js.map +1 -0
- package/lib/server/mcp-session.d.ts +44 -0
- package/lib/server/mcp-session.d.ts.map +1 -0
- package/lib/server/mcp-session.js +18 -0
- package/lib/server/mcp-session.js.map +1 -0
- package/lib/server/mcp-tool-handler.d.ts +259 -0
- package/lib/server/mcp-tool-handler.d.ts.map +1 -0
- package/lib/server/mcp-tool-handler.js +355 -0
- package/lib/server/mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/count-elements-mcp-tool-handler.d.ts +47 -0
- package/lib/tools/handlers/count-elements-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/count-elements-mcp-tool-handler.js +77 -0
- package/lib/tools/handlers/count-elements-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/create-edges-mcp-tool-handler.d.ts +113 -0
- package/lib/tools/handlers/create-edges-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/create-edges-mcp-tool-handler.js +191 -0
- package/lib/tools/handlers/create-edges-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/create-nodes-mcp-tool-handler.d.ts +82 -0
- package/lib/tools/handlers/create-nodes-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/create-nodes-mcp-tool-handler.js +124 -0
- package/lib/tools/handlers/create-nodes-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/delete-elements-mcp-tool-handler.d.ts +53 -0
- package/lib/tools/handlers/delete-elements-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/delete-elements-mcp-tool-handler.js +74 -0
- package/lib/tools/handlers/delete-elements-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/diagram-model-mcp-tool-handler.d.ts +60 -0
- package/lib/tools/handlers/diagram-model-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/diagram-model-mcp-tool-handler.js +79 -0
- package/lib/tools/handlers/diagram-model-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/element-types-mcp-tool-handler.d.ts +98 -0
- package/lib/tools/handlers/element-types-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/element-types-mcp-tool-handler.js +156 -0
- package/lib/tools/handlers/element-types-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/get-selection-mcp-tool-handler.d.ts +44 -0
- package/lib/tools/handlers/get-selection-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/get-selection-mcp-tool-handler.js +70 -0
- package/lib/tools/handlers/get-selection-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/layout-mcp-tool-handler.d.ts +44 -0
- package/lib/tools/handlers/layout-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/layout-mcp-tool-handler.js +72 -0
- package/lib/tools/handlers/layout-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/modify-edges-mcp-tool-handler.d.ts +79 -0
- package/lib/tools/handlers/modify-edges-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/modify-edges-mcp-tool-handler.js +138 -0
- package/lib/tools/handlers/modify-edges-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/modify-nodes-mcp-tool-handler.d.ts +93 -0
- package/lib/tools/handlers/modify-nodes-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/modify-nodes-mcp-tool-handler.js +127 -0
- package/lib/tools/handlers/modify-nodes-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/query-elements-mcp-tool-handler.d.ts +103 -0
- package/lib/tools/handlers/query-elements-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/query-elements-mcp-tool-handler.js +160 -0
- package/lib/tools/handlers/query-elements-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/redo-mcp-tool-handler.d.ts +46 -0
- package/lib/tools/handlers/redo-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/redo-mcp-tool-handler.js +75 -0
- package/lib/tools/handlers/redo-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/save-model-mcp-tool-handler.d.ts +57 -0
- package/lib/tools/handlers/save-model-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/save-model-mcp-tool-handler.js +93 -0
- package/lib/tools/handlers/save-model-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/session-info-mcp-tool-handler.d.ts +66 -0
- package/lib/tools/handlers/session-info-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/session-info-mcp-tool-handler.js +109 -0
- package/lib/tools/handlers/session-info-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/set-selection-mcp-tool-handler.d.ts +61 -0
- package/lib/tools/handlers/set-selection-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/set-selection-mcp-tool-handler.js +104 -0
- package/lib/tools/handlers/set-selection-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/set-view-mcp-tool-handler.d.ts +111 -0
- package/lib/tools/handlers/set-view-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/set-view-mcp-tool-handler.js +144 -0
- package/lib/tools/handlers/set-view-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/undo-mcp-tool-handler.d.ts +46 -0
- package/lib/tools/handlers/undo-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/undo-mcp-tool-handler.js +76 -0
- package/lib/tools/handlers/undo-mcp-tool-handler.js.map +1 -0
- package/lib/tools/handlers/validate-diagram-mcp-tool-handler.d.ts +67 -0
- package/lib/tools/handlers/validate-diagram-mcp-tool-handler.d.ts.map +1 -0
- package/lib/tools/handlers/validate-diagram-mcp-tool-handler.js +0 -0
- package/lib/tools/handlers/validate-diagram-mcp-tool-handler.js.map +1 -0
- package/lib/util/markdown-util.d.ts +20 -0
- package/lib/util/markdown-util.d.ts.map +1 -0
- package/lib/util/markdown-util.js +45 -0
- package/lib/util/markdown-util.js.map +1 -0
- package/lib/util/mcp-util.d.ts +22 -0
- package/lib/util/mcp-util.d.ts.map +1 -0
- package/lib/util/mcp-util.js +29 -0
- package/lib/util/mcp-util.js.map +1 -0
- package/package.json +63 -0
- package/src/index.ts +65 -0
- package/src/modules/mcp-diagram-module.ts +247 -0
- package/src/modules/mcp-server-module.ts +289 -0
- package/src/prompts/handlers/describe-diagram-mcp-prompt-handler.ts +88 -0
- package/src/prompts/handlers/suggest-improvements-mcp-prompt-handler.ts +85 -0
- package/src/resources/handlers/diagram-png-mcp-resource-handler.ts +177 -0
- package/src/resources/handlers/diagram-svg-mcp-resource-handler.ts +85 -0
- package/src/resources/services/element-types-provider.ts +105 -0
- package/src/resources/services/mcp-model-serializer.ts +211 -0
- package/src/server/glsp-mcp-server.spec.ts +73 -0
- package/src/server/glsp-mcp-server.ts +196 -0
- package/src/server/lru-event-store.spec.ts +121 -0
- package/src/server/lru-event-store.ts +112 -0
- package/src/server/mcp-diagram-handler-dispatcher.spec.ts +231 -0
- package/src/server/mcp-diagram-handler-dispatcher.ts +459 -0
- package/src/server/mcp-diagram-prompt-handler-registry.ts +59 -0
- package/src/server/mcp-diagram-resource-handler-registry.ts +73 -0
- package/src/server/mcp-diagram-tool-handler-registry.ts +97 -0
- package/src/server/mcp-handler-shared.spec.ts +53 -0
- package/src/server/mcp-handler-shared.ts +247 -0
- package/src/server/mcp-http-transport-e2e.spec.ts +151 -0
- package/src/server/mcp-http-transport.spec.ts +385 -0
- package/src/server/mcp-http-transport.ts +368 -0
- package/src/server/mcp-id-alias-service.spec.ts +106 -0
- package/src/server/mcp-id-alias-service.ts +104 -0
- package/src/server/mcp-input-schemas.ts +82 -0
- package/src/server/mcp-label-provider.ts +52 -0
- package/src/server/mcp-log-level-registry.spec.ts +75 -0
- package/src/server/mcp-log-level-registry.ts +90 -0
- package/src/server/mcp-logger.spec.ts +227 -0
- package/src/server/mcp-logger.ts +91 -0
- package/src/server/mcp-mime-types.ts +31 -0
- package/src/server/mcp-options.ts +43 -0
- package/src/server/mcp-progress-reporter.spec.ts +93 -0
- package/src/server/mcp-progress-reporter.ts +67 -0
- package/src/server/mcp-prompt-handler.ts +157 -0
- package/src/server/mcp-request-context.ts +39 -0
- package/src/server/mcp-resource-handler.ts +389 -0
- package/src/server/mcp-server-launcher.spec.ts +173 -0
- package/src/server/mcp-server-launcher.ts +369 -0
- package/src/server/mcp-session.ts +45 -0
- package/src/server/mcp-tool-handler.spec.ts +182 -0
- package/src/server/mcp-tool-handler.ts +431 -0
- package/src/server/raw-http.spec.ts +59 -0
- package/src/tools/handlers/count-elements-mcp-tool-handler.spec.ts +101 -0
- package/src/tools/handlers/count-elements-mcp-tool-handler.ts +68 -0
- package/src/tools/handlers/create-edges-mcp-tool-handler.spec.ts +198 -0
- package/src/tools/handlers/create-edges-mcp-tool-handler.ts +200 -0
- package/src/tools/handlers/create-nodes-mcp-tool-handler.spec.ts +199 -0
- package/src/tools/handlers/create-nodes-mcp-tool-handler.ts +126 -0
- package/src/tools/handlers/delete-elements-mcp-tool-handler.ts +69 -0
- package/src/tools/handlers/diagram-model-mcp-tool-handler.ts +68 -0
- package/src/tools/handlers/element-types-mcp-tool-handler.ts +152 -0
- package/src/tools/handlers/get-selection-mcp-tool-handler.ts +56 -0
- package/src/tools/handlers/layout-mcp-tool-handler.ts +58 -0
- package/src/tools/handlers/modify-edges-mcp-tool-handler.ts +142 -0
- package/src/tools/handlers/modify-nodes-mcp-tool-handler.ts +134 -0
- package/src/tools/handlers/query-elements-mcp-tool-handler.spec.ts +212 -0
- package/src/tools/handlers/query-elements-mcp-tool-handler.ts +157 -0
- package/src/tools/handlers/redo-mcp-tool-handler.ts +64 -0
- package/src/tools/handlers/save-model-mcp-tool-handler.ts +74 -0
- package/src/tools/handlers/session-info-mcp-tool-handler.spec.ts +152 -0
- package/src/tools/handlers/session-info-mcp-tool-handler.ts +98 -0
- package/src/tools/handlers/set-selection-mcp-tool-handler.spec.ts +120 -0
- package/src/tools/handlers/set-selection-mcp-tool-handler.ts +92 -0
- package/src/tools/handlers/set-view-mcp-tool-handler.ts +156 -0
- package/src/tools/handlers/undo-mcp-tool-handler.ts +63 -0
- package/src/tools/handlers/validate-diagram-mcp-tool-handler.ts +0 -0
- package/src/tools/tool-annotations.spec.ts +141 -0
- package/src/util/markdown-util.ts +44 -0
- package/src/util/mcp-util.ts +25 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
*
|
|
8
|
+
* This Source Code may also be made available under the following Secondary
|
|
9
|
+
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
* with the GNU Classpath Exception which is available at
|
|
12
|
+
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
*
|
|
14
|
+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
+
********************************************************************************/
|
|
16
|
+
|
|
17
|
+
import { ChangeRoutingPointsOperation, GEdge, ReconnectEdgeOperation } from '@eclipse-glsp/server';
|
|
18
|
+
import { injectable } from 'inversify';
|
|
19
|
+
import * as z from 'zod/v4';
|
|
20
|
+
import { McpToolError, McpToolResult } from '../../server/mcp-handler-shared';
|
|
21
|
+
import { ElementIdentitySchema, McpDiagramScopedInputSchema, elementId as elementIdSchema, position } from '../../server/mcp-input-schemas';
|
|
22
|
+
import { OperationMcpDiagramToolHandler } from '../../server/mcp-tool-handler';
|
|
23
|
+
import { formatNoticeList } from '../../util/mcp-util';
|
|
24
|
+
|
|
25
|
+
/** Single edge-modification entry. Strict so an LLM-typoed field surfaces as a validation error instead of being silently dropped. */
|
|
26
|
+
export const ModifyEdgeSpecSchema = z.strictObject({
|
|
27
|
+
elementId: elementIdSchema,
|
|
28
|
+
sourceElementId: z.string().optional().describe('ID of the source element (must exist in the diagram)'),
|
|
29
|
+
targetElementId: z.string().optional().describe('ID of the target element (must exist in the diagram)'),
|
|
30
|
+
routingPoints: z
|
|
31
|
+
.array(position)
|
|
32
|
+
.optional()
|
|
33
|
+
.describe(
|
|
34
|
+
'Optional array of routing point coordinates that allow for a complex edge path. ' +
|
|
35
|
+
'Using an empty array removes all routing points.'
|
|
36
|
+
)
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const ModifyEdgesInputSchema = McpDiagramScopedInputSchema.extend({
|
|
40
|
+
edges: z
|
|
41
|
+
.array(ModifyEdgeSpecSchema)
|
|
42
|
+
.min(1)
|
|
43
|
+
.describe('Array of edge changes — each entry needs `elementId` plus the fields to update. Must include at least one change.')
|
|
44
|
+
});
|
|
45
|
+
export type ModifyEdgesInput = z.infer<typeof ModifyEdgesInputSchema>;
|
|
46
|
+
|
|
47
|
+
export const ModifyEdgesOutputSchema = z.object({
|
|
48
|
+
modifiedEdges: z.array(ElementIdentitySchema).describe('Identity of each edge whose change request was dispatched.'),
|
|
49
|
+
dispatchedCommands: z.number().int().describe('Number of underlying GLSP operations dispatched.'),
|
|
50
|
+
errors: z.array(z.string()).describe('Per-input failure messages; absent or empty when every input succeeded.')
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
@injectable()
|
|
54
|
+
export class ModifyEdgesMcpToolHandler extends OperationMcpDiagramToolHandler<ModifyEdgesInput> {
|
|
55
|
+
static readonly NAME = 'modify-edges';
|
|
56
|
+
readonly name = ModifyEdgesMcpToolHandler.NAME;
|
|
57
|
+
override readonly title = 'Modify Diagram Edges';
|
|
58
|
+
readonly description =
|
|
59
|
+
'Modify one or more existing edges by reconnecting their source/target endpoints or rewriting their routing points. ' +
|
|
60
|
+
'Reconnection (provide both `sourceElementId` and `targetElementId`) and routing-point edits are mutually exclusive ' +
|
|
61
|
+
'per change entry — a reconnect recomputes the path from scratch and ignores `routingPoints`. ' +
|
|
62
|
+
'Pass an empty `routingPoints` array to remove all routing points (snap to a straight line). ' +
|
|
63
|
+
'This operation modifies the diagram state and requires user approval. ' +
|
|
64
|
+
'For nodes (position/size/text), use `modify-nodes` instead.';
|
|
65
|
+
readonly inputSchema = ModifyEdgesInputSchema;
|
|
66
|
+
override readonly outputSchema = ModifyEdgesOutputSchema;
|
|
67
|
+
|
|
68
|
+
protected async createResult({ edges }: ModifyEdgesInput): Promise<McpToolResult> {
|
|
69
|
+
const elements = this.lookupElements(edges, change => change.elementId);
|
|
70
|
+
|
|
71
|
+
// Type-validate so non-edge ids surface a clear error instead of "model element not found"
|
|
72
|
+
// from the dispatched operation handler. Aliases are sequential across all element kinds,
|
|
73
|
+
// so an LLM passing an arbitrary id may hit a node here.
|
|
74
|
+
const wrongType = elements
|
|
75
|
+
.filter(([, element]) => !(element instanceof GEdge))
|
|
76
|
+
.map(([change, element]) => `'${change.elementId}' (type '${element.type}')`);
|
|
77
|
+
if (wrongType.length) {
|
|
78
|
+
throw new McpToolError(`modify-edges accepts edges only — got: ${wrongType.join(', ')}. Use modify-nodes for nodes.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Dispatch in parallel via `allSettled` so one failed edge surfaces in `errors`
|
|
82
|
+
// instead of rejecting the whole call and losing the other outcomes.
|
|
83
|
+
const dispatched: Array<{ promise: Promise<void>; realId: string; inputId: string }> = [];
|
|
84
|
+
const errors: string[] = [];
|
|
85
|
+
const modifiedRealIds = new Set<string>();
|
|
86
|
+
elements.forEach(([change]) => {
|
|
87
|
+
const { routingPoints } = change;
|
|
88
|
+
const realId = this.aliasService.lookup(change.elementId);
|
|
89
|
+
const sourceElementId = change.sourceElementId ? this.aliasService.lookup(change.sourceElementId) : undefined;
|
|
90
|
+
const targetElementId = change.targetElementId ? this.aliasService.lookup(change.targetElementId) : undefined;
|
|
91
|
+
|
|
92
|
+
if ((sourceElementId && !targetElementId) || (!sourceElementId && targetElementId)) {
|
|
93
|
+
errors.push(`Both source and target ID are required for input: ${JSON.stringify(change)}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (sourceElementId && targetElementId) {
|
|
98
|
+
const source = this.modelState.index.find(sourceElementId);
|
|
99
|
+
if (!source) {
|
|
100
|
+
errors.push(`Source element not found: ${sourceElementId}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const target = this.modelState.index.find(targetElementId);
|
|
104
|
+
if (!target) {
|
|
105
|
+
errors.push(`Target element not found: ${targetElementId}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const operation = ReconnectEdgeOperation.create({ edgeElementId: realId, sourceElementId, targetElementId });
|
|
110
|
+
dispatched.push({ promise: this.actionDispatcher.dispatch(operation), realId, inputId: change.elementId });
|
|
111
|
+
modifiedRealIds.add(realId);
|
|
112
|
+
// Routing-point changes are skipped during a reconnect — the edge's path is recomputed from scratch.
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (routingPoints) {
|
|
117
|
+
const operation = ChangeRoutingPointsOperation.create([{ elementId: realId, newRoutingPoints: routingPoints }]);
|
|
118
|
+
dispatched.push({ promise: this.actionDispatcher.dispatch(operation), realId, inputId: change.elementId });
|
|
119
|
+
modifiedRealIds.add(realId);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const results = await Promise.allSettled(dispatched.map(entry => entry.promise));
|
|
124
|
+
results.forEach((result, i) => {
|
|
125
|
+
if (result.status === 'rejected') {
|
|
126
|
+
const { realId, inputId } = dispatched[i];
|
|
127
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
128
|
+
errors.push(`Failed to modify edge '${inputId}': ${reason}`);
|
|
129
|
+
// The dispatch failed, so this id was *not* modified — drop it from the success list.
|
|
130
|
+
modifiedRealIds.delete(realId);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const modifiedEdges = [...modifiedRealIds]
|
|
135
|
+
.map(realId => this.describeElement(realId))
|
|
136
|
+
.filter((entry): entry is NonNullable<typeof entry> => entry !== undefined);
|
|
137
|
+
return this.success(
|
|
138
|
+
`Successfully modified ${edges.length - errors.length} edge(s) (in ${dispatched.length} commands)${formatNoticeList('errors', errors)}`,
|
|
139
|
+
{ modifiedEdges, dispatchedCommands: dispatched.length, errors }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
*
|
|
8
|
+
* This Source Code may also be made available under the following Secondary
|
|
9
|
+
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
* with the GNU Classpath Exception which is available at
|
|
12
|
+
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
*
|
|
14
|
+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
+
********************************************************************************/
|
|
16
|
+
|
|
17
|
+
import { ApplyLabelEditOperation, ChangeBoundsOperation, GEdge, GShapeElement } from '@eclipse-glsp/server';
|
|
18
|
+
import { injectable } from 'inversify';
|
|
19
|
+
import * as z from 'zod/v4';
|
|
20
|
+
import { McpToolError, McpToolResult } from '../../server/mcp-handler-shared';
|
|
21
|
+
import { ElementIdentitySchema, McpDiagramScopedInputSchema, elementId, position } from '../../server/mcp-input-schemas';
|
|
22
|
+
import { OperationMcpDiagramToolHandler } from '../../server/mcp-tool-handler';
|
|
23
|
+
import { formatNoticeList } from '../../util/mcp-util';
|
|
24
|
+
|
|
25
|
+
/** Strict — any unknown field on the size object surfaces as a validation error. */
|
|
26
|
+
export const NodeSizeSchema = z.strictObject({
|
|
27
|
+
width: z.number().positive().describe('Width of the element in diagram space (must be > 0).'),
|
|
28
|
+
height: z.number().positive().describe('Height of the element in diagram space (must be > 0).')
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Single node-modification entry. Strict so an LLM-typoed field surfaces as a validation error instead of being silently dropped. */
|
|
32
|
+
export const ModifyNodeSpecSchema = z.strictObject({
|
|
33
|
+
elementId,
|
|
34
|
+
position: position.optional().describe('Position where the node should be moved to (absolute diagram coordinates)'),
|
|
35
|
+
size: NodeSizeSchema.optional().describe('New size of the node'),
|
|
36
|
+
text: z.string().optional().describe("Label text to use instead (given that the element's type allows for labels).")
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const ModifyNodesInputSchema = McpDiagramScopedInputSchema.extend({
|
|
40
|
+
nodes: z
|
|
41
|
+
.array(ModifyNodeSpecSchema)
|
|
42
|
+
.min(1)
|
|
43
|
+
.describe('Array of node changes — each entry needs `elementId` plus the fields to update. Must include at least one change.')
|
|
44
|
+
});
|
|
45
|
+
export type ModifyNodesInput = z.infer<typeof ModifyNodesInputSchema>;
|
|
46
|
+
|
|
47
|
+
export const ModifyNodesOutputSchema = z.object({
|
|
48
|
+
modifiedNodes: z
|
|
49
|
+
.array(ElementIdentitySchema)
|
|
50
|
+
.describe('Identity of each node whose change request was dispatched (post-modification labels).'),
|
|
51
|
+
dispatchedCommands: z.number().int().describe('Number of underlying GLSP operations dispatched (a single change may yield several).'),
|
|
52
|
+
warnings: z
|
|
53
|
+
.array(z.string())
|
|
54
|
+
.describe(
|
|
55
|
+
'Soft notices for inputs whose change applied with caveats (e.g. `text` supplied for a node whose type has no editable label).'
|
|
56
|
+
)
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
@injectable()
|
|
60
|
+
export class ModifyNodesMcpToolHandler extends OperationMcpDiagramToolHandler<ModifyNodesInput> {
|
|
61
|
+
static readonly NAME = 'modify-nodes';
|
|
62
|
+
readonly name = ModifyNodesMcpToolHandler.NAME;
|
|
63
|
+
override readonly title = 'Modify Diagram Nodes';
|
|
64
|
+
readonly description =
|
|
65
|
+
'Modify one or more existing nodes by changing their position, size, and/or label text. ' +
|
|
66
|
+
'When modifying position or size, absolutely consider the visual alignment with other nodes — ' +
|
|
67
|
+
'use `query-elements` (inspect mode) first to understand the layout. ' +
|
|
68
|
+
'Each change entry can include any combination of `position`, `size`, and `text`; omitted fields keep their current value. ' +
|
|
69
|
+
'This operation modifies the diagram state and requires user approval. ' +
|
|
70
|
+
'For edges (reconnect / routing-points), use `modify-edges` instead.';
|
|
71
|
+
readonly inputSchema = ModifyNodesInputSchema;
|
|
72
|
+
override readonly outputSchema = ModifyNodesOutputSchema;
|
|
73
|
+
|
|
74
|
+
protected async createResult({ nodes }: ModifyNodesInput): Promise<McpToolResult> {
|
|
75
|
+
const elements = this.lookupElements(nodes, change => change.elementId);
|
|
76
|
+
|
|
77
|
+
// Reject edge ids — they have no `position`/`size` semantics and would fail downstream
|
|
78
|
+
// with a misleading "model element not found" error from the operation handler. Aliases
|
|
79
|
+
// are sequential across all element kinds, so an LLM passing an arbitrary id may hit an edge.
|
|
80
|
+
const wrongType = elements.filter(([, element]) => element instanceof GEdge).map(([change]) => `'${change.elementId}'`);
|
|
81
|
+
if (wrongType.length) {
|
|
82
|
+
throw new McpToolError(`modify-nodes does not accept edges — got: ${wrongType.join(', ')}. Use modify-edges for edges.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Reject any other non-shape kinds (labels, compartments, ports, custom kinds) — they
|
|
86
|
+
// reach the index too and would silently produce no-op or undefined-bounds operations.
|
|
87
|
+
const nonShape = elements.filter(([, element]) => !(element instanceof GShapeElement)).map(([change]) => `'${change.elementId}'`);
|
|
88
|
+
if (nonShape.length) {
|
|
89
|
+
throw new McpToolError(
|
|
90
|
+
`modify-nodes only accepts shape elements — got: ${nonShape.join(', ')}. ` +
|
|
91
|
+
'Use `query-elements` (inspect mode) to find shape ids, or pick the parent shape.'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Modifications are independent of each other — dispatch in parallel.
|
|
96
|
+
const promises: Promise<void>[] = [];
|
|
97
|
+
const warnings: string[] = [];
|
|
98
|
+
elements.forEach(([change, element]) => {
|
|
99
|
+
// Guaranteed non-null and shape-typed by the missing-elements / nonShape checks above.
|
|
100
|
+
const resolved = element as GShapeElement;
|
|
101
|
+
const { size, position, text } = change;
|
|
102
|
+
const realId = this.aliasService.lookup(change.elementId);
|
|
103
|
+
|
|
104
|
+
if (size || position) {
|
|
105
|
+
const newSize = size ?? resolved.size;
|
|
106
|
+
const newPosition = position ?? resolved.position;
|
|
107
|
+
|
|
108
|
+
const operation = ChangeBoundsOperation.create([{ elementId: realId, newSize, newPosition }]);
|
|
109
|
+
promises.push(this.actionDispatcher.dispatch(operation));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (text) {
|
|
113
|
+
const labelId = this.labelProvider.getLabel(resolved)?.id;
|
|
114
|
+
if (labelId) {
|
|
115
|
+
promises.push(this.actionDispatcher.dispatch(ApplyLabelEditOperation.create({ labelId, text })));
|
|
116
|
+
} else {
|
|
117
|
+
warnings.push(
|
|
118
|
+
`Ignored \`text\` for '${change.elementId}' (type '${resolved.type}') — this element has no editable label.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await Promise.all(promises);
|
|
125
|
+
|
|
126
|
+
const modifiedNodes = nodes
|
|
127
|
+
.map(change => this.describeElement(change.elementId))
|
|
128
|
+
.filter((entry): entry is NonNullable<typeof entry> => entry !== undefined);
|
|
129
|
+
return this.success(
|
|
130
|
+
`Successfully modified ${nodes.length} node(s) (in ${promises.length} commands)` + formatNoticeList('warnings', warnings),
|
|
131
|
+
{ modifiedNodes, dispatchedCommands: promises.length, warnings }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
*
|
|
8
|
+
* This Source Code may also be made available under the following Secondary
|
|
9
|
+
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
* with the GNU Classpath Exception which is available at
|
|
12
|
+
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
*
|
|
14
|
+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
+
********************************************************************************/
|
|
16
|
+
|
|
17
|
+
import { ClientId, GLabel, GModelElement, Logger, ModelState, NullLogger } from '@eclipse-glsp/server';
|
|
18
|
+
import { expect } from 'chai';
|
|
19
|
+
import { Container, ContainerModule } from 'inversify';
|
|
20
|
+
import { McpModelSerializer } from '../../resources/services/mcp-model-serializer';
|
|
21
|
+
import { McpElementsNotFoundError, McpToolResult } from '../../server/mcp-handler-shared';
|
|
22
|
+
import { McpIdAliasService } from '../../server/mcp-id-alias-service';
|
|
23
|
+
import { DefaultMcpLabelProvider, McpLabelProvider } from '../../server/mcp-label-provider';
|
|
24
|
+
import { QueryElementsInput, QueryElementsMcpToolHandler } from './query-elements-mcp-tool-handler';
|
|
25
|
+
|
|
26
|
+
function makeLabel(text: string): GLabel {
|
|
27
|
+
// Set the prototype so `child instanceof GLabel` checks in the handler return true.
|
|
28
|
+
return Object.assign(Object.create(GLabel.prototype), { id: 'label', type: 'label', text, children: [] }) as GLabel;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeElement(id: string, type: string, labelText?: string): GModelElement {
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
type,
|
|
35
|
+
children: labelText !== undefined ? [makeLabel(labelText)] : []
|
|
36
|
+
} as unknown as GModelElement;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeModelState(elements: GModelElement[]): ModelState {
|
|
40
|
+
const byId = new Map(elements.map(el => [el.id, el]));
|
|
41
|
+
return {
|
|
42
|
+
index: {
|
|
43
|
+
allIds: () => [...byId.keys()],
|
|
44
|
+
get: (id: string) => byId.get(id),
|
|
45
|
+
find: (id: string) => byId.get(id)
|
|
46
|
+
}
|
|
47
|
+
} as unknown as ModelState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CapturingSerializer extends McpModelSerializer {
|
|
51
|
+
/** Element arrays the handler passed to {@link serializeStructuredArray}, in call order. */
|
|
52
|
+
capturedArrays: GModelElement[][];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeCapturingSerializer(): CapturingSerializer {
|
|
56
|
+
const capturedArrays: GModelElement[][] = [];
|
|
57
|
+
return {
|
|
58
|
+
capturedArrays,
|
|
59
|
+
serialize: () => 'serialized',
|
|
60
|
+
serializeStructured: () => ({}),
|
|
61
|
+
serializeArray: elements => elements.map(el => `- ${el.id} (${el.type})`).join('\n'),
|
|
62
|
+
serializeStructuredArray: elements => {
|
|
63
|
+
capturedArrays.push(elements);
|
|
64
|
+
return { elements: elements.map(el => ({ id: el.id, type: el.type })) };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildHandler(elements: GModelElement[], serializer: McpModelSerializer = makeCapturingSerializer()): QueryElementsMcpToolHandler {
|
|
70
|
+
const container = new Container();
|
|
71
|
+
container.load(
|
|
72
|
+
new ContainerModule(bind => {
|
|
73
|
+
bind(Logger).toConstantValue(new NullLogger());
|
|
74
|
+
bind(ClientId).toConstantValue('test-session');
|
|
75
|
+
bind(ModelState).toConstantValue(makeModelState(elements));
|
|
76
|
+
bind(McpIdAliasService).toConstantValue({
|
|
77
|
+
lookup: (id: string) => id,
|
|
78
|
+
alias: (id: string) => id
|
|
79
|
+
} as McpIdAliasService);
|
|
80
|
+
bind(McpModelSerializer).toConstantValue(serializer);
|
|
81
|
+
bind(McpLabelProvider).to(DefaultMcpLabelProvider);
|
|
82
|
+
bind(QueryElementsMcpToolHandler).toSelf();
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
return container.get(QueryElementsMcpToolHandler);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ListStructured {
|
|
89
|
+
mode: 'list';
|
|
90
|
+
matches: { id: string; type: string; label?: string }[];
|
|
91
|
+
truncated: boolean;
|
|
92
|
+
}
|
|
93
|
+
interface InspectStructured {
|
|
94
|
+
mode: 'inspect';
|
|
95
|
+
elements: { id: string; type: string }[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function callCreateResult(handler: QueryElementsMcpToolHandler, params: QueryElementsInput): Promise<McpToolResult> {
|
|
99
|
+
return (handler as unknown as { createResult: (p: QueryElementsInput) => Promise<McpToolResult> }).createResult(params);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('QueryElementsMcpToolHandler', () => {
|
|
103
|
+
describe('list mode (no `elementIds`)', () => {
|
|
104
|
+
it('filters by `types` (only matching types are returned)', async () => {
|
|
105
|
+
const handler = buildHandler([
|
|
106
|
+
makeElement('n1', 'task:manual', 'Build'),
|
|
107
|
+
makeElement('n2', 'task:automated', 'Deploy'),
|
|
108
|
+
makeElement('e1', 'edge')
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const result = await callCreateResult(handler, { sessionId: 's', types: ['task:manual'] });
|
|
112
|
+
const structured = result.structuredContent as unknown as ListStructured;
|
|
113
|
+
expect(structured.mode).to.equal('list');
|
|
114
|
+
expect(structured.matches).to.have.lengthOf(1);
|
|
115
|
+
expect(structured.matches[0].id).to.equal('n1');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('matches `labelMatch` case-insensitively against direct GLabel children', async () => {
|
|
119
|
+
const handler = buildHandler([
|
|
120
|
+
makeElement('n1', 'task', 'Build artifact'),
|
|
121
|
+
makeElement('n2', 'task', 'Deploy artifact'),
|
|
122
|
+
makeElement('n3', 'task', 'Validate spec')
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const result = await callCreateResult(handler, { sessionId: 's', labelMatch: 'ARTIFACT' });
|
|
126
|
+
const ids = (result.structuredContent as unknown as ListStructured).matches.map(m => m.id);
|
|
127
|
+
expect(ids).to.have.members(['n1', 'n2']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('combines `types` and `labelMatch` as AND', async () => {
|
|
131
|
+
const handler = buildHandler([
|
|
132
|
+
makeElement('n1', 'task:manual', 'Build artifact'),
|
|
133
|
+
makeElement('n2', 'task:automated', 'Build artifact')
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
const result = await callCreateResult(handler, {
|
|
137
|
+
sessionId: 's',
|
|
138
|
+
types: ['task:manual'],
|
|
139
|
+
labelMatch: 'build'
|
|
140
|
+
});
|
|
141
|
+
const matches = (result.structuredContent as unknown as ListStructured).matches;
|
|
142
|
+
expect(matches).to.have.lengthOf(1);
|
|
143
|
+
expect(matches[0].id).to.equal('n1');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('caps results at `limit` and reports `truncated: true`', async () => {
|
|
147
|
+
const elements = Array.from({ length: 10 }, (_, i) => makeElement(`n${i}`, 'task'));
|
|
148
|
+
const handler = buildHandler(elements);
|
|
149
|
+
|
|
150
|
+
const result = await callCreateResult(handler, { sessionId: 's', limit: 3 });
|
|
151
|
+
const structured = result.structuredContent as unknown as ListStructured;
|
|
152
|
+
expect(structured.matches).to.have.lengthOf(3);
|
|
153
|
+
expect(structured.truncated).to.equal(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('surfaces a no-match message and empty matches when nothing matches', async () => {
|
|
157
|
+
const handler = buildHandler([makeElement('n1', 'task', 'foo')]);
|
|
158
|
+
|
|
159
|
+
const result = await callCreateResult(handler, { sessionId: 's', types: ['nonexistent'] });
|
|
160
|
+
const text = (result.content[0] as { text: string }).text;
|
|
161
|
+
expect(text).to.include('No elements matched');
|
|
162
|
+
expect((result.structuredContent as unknown as ListStructured).matches).to.deep.equal([]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('inspect mode (with `elementIds`)', () => {
|
|
167
|
+
it('resolves the requested ids and forwards the matching elements to the serializer', async () => {
|
|
168
|
+
const elements = [makeElement('n1', 'task:manual', 'Build'), makeElement('n2', 'task:automated', 'Deploy')];
|
|
169
|
+
const serializer = makeCapturingSerializer();
|
|
170
|
+
const handler = buildHandler(elements, serializer);
|
|
171
|
+
|
|
172
|
+
const result = await callCreateResult(handler, { sessionId: 's', elementIds: ['n1', 'n2'] });
|
|
173
|
+
|
|
174
|
+
// The handler delegated rendering to the injected serializer with the exact
|
|
175
|
+
// GModelElement instances looked up from the model index — not just the ids. Inspect
|
|
176
|
+
// mode also probes per-element to detect container expansion before rendering, so the
|
|
177
|
+
// final array call is what produces the structuredContent — assert on that one.
|
|
178
|
+
expect(serializer.capturedArrays.length).to.be.greaterThan(0);
|
|
179
|
+
const finalCall = serializer.capturedArrays[serializer.capturedArrays.length - 1];
|
|
180
|
+
expect(finalCall).to.deep.equal(elements);
|
|
181
|
+
|
|
182
|
+
const structured = result.structuredContent as unknown as InspectStructured;
|
|
183
|
+
expect(structured.mode).to.equal('inspect');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('throws McpElementsNotFoundError when an id is missing from the model', async () => {
|
|
187
|
+
const handler = buildHandler([makeElement('n1', 'task')]);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await callCreateResult(handler, { sessionId: 's', elementIds: ['n1', 'unknown'] });
|
|
191
|
+
expect.fail('expected McpElementsNotFoundError');
|
|
192
|
+
} catch (err) {
|
|
193
|
+
expect(err).to.be.instanceOf(McpElementsNotFoundError);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('ignores `types` / `labelMatch` / `limit` when `elementIds` is set', async () => {
|
|
198
|
+
const handler = buildHandler([makeElement('n1', 'task:manual'), makeElement('n2', 'task:automated')]);
|
|
199
|
+
|
|
200
|
+
const result = await callCreateResult(handler, {
|
|
201
|
+
sessionId: 's',
|
|
202
|
+
elementIds: ['n1'],
|
|
203
|
+
types: ['task:automated'], // would exclude n1 in list mode
|
|
204
|
+
labelMatch: 'nope',
|
|
205
|
+
limit: 0 // intentionally would fail validation in list mode
|
|
206
|
+
});
|
|
207
|
+
const structured = result.structuredContent as unknown as InspectStructured;
|
|
208
|
+
expect(structured.mode).to.equal('inspect');
|
|
209
|
+
expect(structured.elements.map(e => e.id)).to.deep.equal(['n1']);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
*
|
|
8
|
+
* This Source Code may also be made available under the following Secondary
|
|
9
|
+
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
* with the GNU Classpath Exception which is available at
|
|
12
|
+
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
*
|
|
14
|
+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
+
********************************************************************************/
|
|
16
|
+
|
|
17
|
+
import { GModelElement } from '@eclipse-glsp/server';
|
|
18
|
+
import { inject, injectable } from 'inversify';
|
|
19
|
+
import * as z from 'zod/v4';
|
|
20
|
+
import { McpModelSerializer } from '../../resources/services/mcp-model-serializer';
|
|
21
|
+
import { McpElementsNotFoundError, McpToolResult } from '../../server/mcp-handler-shared';
|
|
22
|
+
import { McpDiagramScopedInputSchema, elementIds as elementIdsSchema } from '../../server/mcp-input-schemas';
|
|
23
|
+
import { AbstractMcpDiagramToolHandler } from '../../server/mcp-tool-handler';
|
|
24
|
+
|
|
25
|
+
export const QueryElementsInputSchema = McpDiagramScopedInputSchema.extend({
|
|
26
|
+
elementIds: elementIdsSchema
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Inspect-mode trigger. When set, returns rich per-element data and ignores the filter fields. Throws on unknown id.'),
|
|
29
|
+
types: z.array(z.string()).optional().describe('List-mode type filter. Empty/omitted = any. Ignored when `elementIds` is set.'),
|
|
30
|
+
labelMatch: z.string().optional().describe('List-mode case-insensitive substring on direct-child `GLabel.text`.'),
|
|
31
|
+
limit: z.number().int().min(1).max(1000).optional().describe('List-mode cap, 1–1000. Defaults to 100.')
|
|
32
|
+
});
|
|
33
|
+
export type QueryElementsInput = z.infer<typeof QueryElementsInputSchema>;
|
|
34
|
+
|
|
35
|
+
export const QueryElementMatchSchema = z.object({
|
|
36
|
+
id: z.string(),
|
|
37
|
+
type: z.string(),
|
|
38
|
+
label: z.string().optional()
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** `.loose()` lets adopter element types attach extra fields (position, size, …) without widening the schema each time. */
|
|
42
|
+
export const DiagramElementSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
type: z.string(),
|
|
46
|
+
parentId: z.string().optional()
|
|
47
|
+
})
|
|
48
|
+
.loose();
|
|
49
|
+
|
|
50
|
+
export const QueryElementsOutputSchema = z.object({
|
|
51
|
+
mode: z.enum(['list', 'inspect']).describe('Echoes which mode the call ran in.'),
|
|
52
|
+
matches: z.array(QueryElementMatchSchema).optional().describe('Present in `list` mode: slim id/type/label entries.'),
|
|
53
|
+
elements: z.array(DiagramElementSchema).optional().describe('Present in `inspect` mode: rich per-element detail.'),
|
|
54
|
+
truncated: z.boolean().optional().describe('Present in `list` mode: true when more elements matched than `limit`.'),
|
|
55
|
+
expandedFromContainers: z
|
|
56
|
+
.array(z.string())
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(
|
|
59
|
+
'Present in `inspect` mode when one or more requested ids referred to containers — lists those container ids. ' +
|
|
60
|
+
'The `elements` array then includes the container plus its descendants.'
|
|
61
|
+
)
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Two-mode element query — list/filter or inspect-by-id, discriminated by `elementIds` presence. */
|
|
65
|
+
@injectable()
|
|
66
|
+
export class QueryElementsMcpToolHandler extends AbstractMcpDiagramToolHandler<QueryElementsInput> {
|
|
67
|
+
static readonly NAME = 'query-elements';
|
|
68
|
+
readonly name = QueryElementsMcpToolHandler.NAME;
|
|
69
|
+
override readonly title = 'Query Diagram Elements';
|
|
70
|
+
readonly description =
|
|
71
|
+
'Find or inspect elements in the session diagram. Pass `elementIds` to inspect specific ' +
|
|
72
|
+
'elements in detail (rich per-element data). Pass `types` and/or `labelMatch` to search by ' +
|
|
73
|
+
'filter (slim id/type/label summaries with truncation). Useful as a precursor to the ' +
|
|
74
|
+
'create/modify/delete tools, and a cheaper alternative to `diagram-model` on large diagrams.';
|
|
75
|
+
readonly inputSchema = QueryElementsInputSchema;
|
|
76
|
+
override readonly outputSchema = QueryElementsOutputSchema;
|
|
77
|
+
|
|
78
|
+
@inject(McpModelSerializer) protected serializer: McpModelSerializer;
|
|
79
|
+
|
|
80
|
+
/** List-mode result cap when the call doesn't override `limit`. Override via subclass. */
|
|
81
|
+
protected readonly defaultLimit: number = 100;
|
|
82
|
+
|
|
83
|
+
protected async createResult(params: QueryElementsInput): Promise<McpToolResult> {
|
|
84
|
+
return params.elementIds && params.elementIds.length > 0 ? this.inspect(params.elementIds) : this.list(params);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected inspect(inputIds: string[]): McpToolResult {
|
|
88
|
+
const { realIds, missingIds } = this.resolveIds(inputIds);
|
|
89
|
+
if (missingIds.length > 0) {
|
|
90
|
+
throw new McpElementsNotFoundError(missingIds);
|
|
91
|
+
}
|
|
92
|
+
const elements: GModelElement[] = realIds.map(id => this.modelState.index.get(id)!);
|
|
93
|
+
// Per-input serialize lets us identify which inputs caused expansion: the serializer
|
|
94
|
+
// walks descendants for containers, so a 1-input call may produce N output entries.
|
|
95
|
+
// The structural-children check the previous version used overstated this — a leaf node
|
|
96
|
+
// with a label child counts as having `children.length > 0` but doesn't produce extra
|
|
97
|
+
// entries because the serializer's adjuster drops labels.
|
|
98
|
+
const expandedFromContainers = elements
|
|
99
|
+
.filter(element => {
|
|
100
|
+
// `serializeStructuredArray` returns an open `Record<string, unknown>` per the
|
|
101
|
+
// `McpStructuredContent` contract; narrow before reading `.elements.length`.
|
|
102
|
+
const entries = this.serializer.serializeStructuredArray([element]).elements;
|
|
103
|
+
return Array.isArray(entries) && entries.length > 1;
|
|
104
|
+
})
|
|
105
|
+
.map(element => this.aliasService.alias(element.id));
|
|
106
|
+
// Serializer's `serializeStructuredArray` already returns `{ elements: [...] }`; spread to keep one source of truth.
|
|
107
|
+
return this.success(this.summarizeInspect(elements, expandedFromContainers), {
|
|
108
|
+
mode: 'inspect',
|
|
109
|
+
...this.serializer.serializeStructuredArray(elements),
|
|
110
|
+
...(expandedFromContainers.length > 0 ? { expandedFromContainers } : {})
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Builds the LLM-facing summary for inspect mode. Override to customize per-adopter wording. */
|
|
115
|
+
protected summarizeInspect(elements: GModelElement[], expandedFromContainers: string[]): string {
|
|
116
|
+
const lines = elements.map(element => `- ${this.aliasService.alias(element.id)} (${element.type})`);
|
|
117
|
+
const expansionNote =
|
|
118
|
+
expandedFromContainers.length > 0 ? `\nContainer(s) ${expandedFromContainers.join(', ')} expanded to include descendants.` : '';
|
|
119
|
+
return `Inspected ${elements.length} element(s); full data in structuredContent.\n${lines.join('\n')}${expansionNote}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected list({ types, labelMatch, limit }: QueryElementsInput): McpToolResult {
|
|
123
|
+
const cap = limit ?? this.defaultLimit;
|
|
124
|
+
const typeFilter = types && types.length > 0 ? new Set(types) : undefined;
|
|
125
|
+
const needle = labelMatch?.toLowerCase();
|
|
126
|
+
|
|
127
|
+
const matches: { id: string; type: string; label?: string }[] = [];
|
|
128
|
+
let truncated = false;
|
|
129
|
+
for (const id of this.modelState.index.allIds()) {
|
|
130
|
+
const element = this.modelState.index.get(id);
|
|
131
|
+
if (!element) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (typeFilter && !typeFilter.has(element.type)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const label = this.labelProvider.getLabel(element)?.text;
|
|
138
|
+
if (needle !== undefined && !label?.toLowerCase().includes(needle)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (matches.length >= cap) {
|
|
142
|
+
truncated = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
matches.push({ id: this.aliasService.alias(element.id), type: element.type, ...(label !== undefined ? { label } : {}) });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const summary = matches.length === 0 ? 'No elements matched the query.' : this.renderMarkdown(matches, truncated);
|
|
149
|
+
return this.success(summary, { mode: 'list', matches, truncated });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
protected renderMarkdown(matches: { id: string; type: string; label?: string }[], truncated: boolean): string {
|
|
153
|
+
const rows = matches.map(match => `- ${match.id} (${match.type})${match.label ? ` — "${match.label}"` : ''}`).join('\n');
|
|
154
|
+
const tail = truncated ? '\n\n_(truncated — increase `limit` or refine filters to see more)_' : '';
|
|
155
|
+
return `Matched ${matches.length} element${matches.length === 1 ? '' : 's'}:\n${rows}${tail}`;
|
|
156
|
+
}
|
|
157
|
+
}
|