@eclipse-glsp/server-mcp 2.8.0-next.1 → 2.8.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/README.md +7 -7
- package/lib/browser/modules/browser-mcp-server-module.js +1 -1
- package/lib/browser/modules/browser-mcp-server-module.js.map +1 -1
- package/lib/common/modules/abstract-mcp-server-module.d.ts +1 -2
- package/lib/common/modules/abstract-mcp-server-module.d.ts.map +1 -1
- package/lib/common/modules/abstract-mcp-server-module.js.map +1 -1
- package/lib/common/prompts/handlers/describe-diagram-mcp-prompt-handler.js +3 -2
- package/lib/common/prompts/handlers/describe-diagram-mcp-prompt-handler.js.map +1 -1
- package/lib/common/prompts/handlers/suggest-improvements-mcp-prompt-handler.js +3 -2
- package/lib/common/prompts/handlers/suggest-improvements-mcp-prompt-handler.js.map +1 -1
- package/lib/common/resources/handlers/diagram-png-mcp-resource-handler.js +5 -4
- package/lib/common/resources/handlers/diagram-png-mcp-resource-handler.js.map +1 -1
- package/lib/common/resources/handlers/diagram-svg-mcp-resource-handler.js +4 -3
- package/lib/common/resources/handlers/diagram-svg-mcp-resource-handler.js.map +1 -1
- package/lib/common/resources/services/element-types-provider.d.ts.map +1 -1
- package/lib/common/resources/services/element-types-provider.js +1 -2
- package/lib/common/resources/services/element-types-provider.js.map +1 -1
- package/lib/common/resources/services/mcp-model-serializer.d.ts +1 -2
- package/lib/common/resources/services/mcp-model-serializer.d.ts.map +1 -1
- package/lib/common/resources/services/mcp-model-serializer.js +3 -5
- package/lib/common/resources/services/mcp-model-serializer.js.map +1 -1
- package/lib/common/server/abstract-mcp-server-launcher.d.ts +3 -4
- package/lib/common/server/abstract-mcp-server-launcher.d.ts.map +1 -1
- package/lib/common/server/abstract-mcp-server-launcher.js +9 -14
- package/lib/common/server/abstract-mcp-server-launcher.js.map +1 -1
- package/lib/common/server/glsp-mcp-server.d.ts.map +1 -1
- package/lib/common/server/glsp-mcp-server.js +2 -2
- package/lib/common/server/glsp-mcp-server.js.map +1 -1
- package/lib/common/server/lru-event-store.js +2 -3
- package/lib/common/server/lru-event-store.js.map +1 -1
- package/lib/common/server/mcp-diagram-handler-dispatcher.js +4 -8
- package/lib/common/server/mcp-diagram-handler-dispatcher.js.map +1 -1
- package/lib/common/server/mcp-handler-shared.d.ts +1 -2
- package/lib/common/server/mcp-handler-shared.d.ts.map +1 -1
- package/lib/common/server/mcp-handler-shared.js +1 -1
- package/lib/common/server/mcp-handler-shared.js.map +1 -1
- package/lib/common/server/mcp-id-alias-service.js +1 -2
- package/lib/common/server/mcp-id-alias-service.js.map +1 -1
- package/lib/common/server/mcp-log-level-registry.js +10 -10
- package/lib/common/server/mcp-log-level-registry.js.map +1 -1
- package/lib/common/server/mcp-mime-types.d.ts +1 -1
- package/lib/common/server/mcp-mime-types.d.ts.map +1 -1
- package/lib/common/server/mcp-options.d.ts +1 -1
- package/lib/common/server/mcp-options.d.ts.map +1 -1
- package/lib/common/server/mcp-progress-reporter.js +1 -2
- package/lib/common/server/mcp-progress-reporter.js.map +1 -1
- package/lib/common/server/mcp-resource-handler.js +5 -9
- package/lib/common/server/mcp-resource-handler.js.map +1 -1
- package/lib/common/server/mcp-tool-handler.js +1 -2
- package/lib/common/server/mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/count-elements-mcp-tool-handler.js +4 -4
- package/lib/common/tools/handlers/count-elements-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/create-edges-mcp-tool-handler.js +4 -3
- package/lib/common/tools/handlers/create-edges-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.js +5 -5
- package/lib/common/tools/handlers/create-nodes-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.js +3 -2
- package/lib/common/tools/handlers/delete-elements-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/diagram-model-mcp-tool-handler.js +3 -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.js +4 -3
- package/lib/common/tools/handlers/element-types-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/get-selection-mcp-tool-handler.js +3 -2
- package/lib/common/tools/handlers/get-selection-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/layout-mcp-tool-handler.js +3 -2
- package/lib/common/tools/handlers/layout-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.js +3 -2
- package/lib/common/tools/handlers/modify-edges-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.js +6 -6
- package/lib/common/tools/handlers/modify-nodes-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/query-elements-mcp-tool-handler.js +7 -7
- package/lib/common/tools/handlers/query-elements-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/redo-mcp-tool-handler.js +3 -2
- package/lib/common/tools/handlers/redo-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/save-model-mcp-tool-handler.js +3 -2
- package/lib/common/tools/handlers/save-model-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/session-info-mcp-tool-handler.js +4 -4
- package/lib/common/tools/handlers/session-info-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/set-selection-mcp-tool-handler.js +5 -4
- package/lib/common/tools/handlers/set-selection-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/set-view-mcp-tool-handler.js +5 -4
- package/lib/common/tools/handlers/set-view-mcp-tool-handler.js.map +1 -1
- package/lib/common/tools/handlers/undo-mcp-tool-handler.js +3 -2
- package/lib/common/tools/handlers/undo-mcp-tool-handler.js.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/lib/common/util/markdown-util.js +1 -2
- package/lib/common/util/markdown-util.js.map +1 -1
- package/lib/node/modules/node-mcp-server-module.js +8 -8
- package/lib/node/modules/node-mcp-server-module.js.map +1 -1
- package/package.json +17 -17
- package/src/common/modules/abstract-mcp-server-module.ts +3 -3
- package/src/common/resources/services/element-types-provider.ts +2 -1
- package/src/common/resources/services/mcp-model-serializer.ts +1 -2
- package/src/common/server/abstract-mcp-server-launcher.ts +3 -3
- package/src/common/server/glsp-mcp-server.ts +2 -1
- package/src/common/server/mcp-handler-shared.ts +2 -4
- package/src/common/server/mcp-mime-types.ts +1 -1
- package/src/common/server/mcp-options.ts +1 -1
- package/src/common/server/mcp-resource-handler.ts +1 -1
- package/src/common/server/abstract-mcp-server-launcher.spec.ts +0 -458
- package/src/common/server/glsp-mcp-server.spec.ts +0 -73
- package/src/common/server/lru-event-store.spec.ts +0 -121
- package/src/common/server/mcp-diagram-handler-dispatcher.spec.ts +0 -231
- package/src/common/server/mcp-handler-shared.spec.ts +0 -53
- package/src/common/server/mcp-id-alias-service.spec.ts +0 -106
- package/src/common/server/mcp-log-level-registry.spec.ts +0 -75
- package/src/common/server/mcp-logger.spec.ts +0 -235
- package/src/common/server/mcp-progress-reporter.spec.ts +0 -103
- package/src/common/server/mcp-tool-handler.spec.ts +0 -182
- package/src/common/tools/handlers/count-elements-mcp-tool-handler.spec.ts +0 -101
- package/src/common/tools/handlers/create-edges-mcp-tool-handler.spec.ts +0 -198
- package/src/common/tools/handlers/create-nodes-mcp-tool-handler.spec.ts +0 -199
- package/src/common/tools/handlers/query-elements-mcp-tool-handler.spec.ts +0 -212
- package/src/common/tools/handlers/session-info-mcp-tool-handler.spec.ts +0 -152
- package/src/common/tools/handlers/set-selection-mcp-tool-handler.spec.ts +0 -120
- package/src/common/tools/tool-annotations.spec.ts +0 -141
- package/src/node/server/mcp-http-transport-e2e.spec.ts +0 -297
- package/src/node/server/node-mcp-server-launcher.spec.ts +0 -184
- package/src/node/server/raw-http.spec.ts +0 -59
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
-
*
|
|
4
|
-
* This program and the accompanying materials are made available under the
|
|
5
|
-
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
*
|
|
8
|
-
* This Source Code may also be made available under the following Secondary
|
|
9
|
-
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
* with the GNU Classpath Exception which is available at
|
|
12
|
-
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
*
|
|
14
|
-
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
-
********************************************************************************/
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
Action,
|
|
19
|
-
ActionDispatcher,
|
|
20
|
-
ClientId,
|
|
21
|
-
DiagramConfiguration,
|
|
22
|
-
EdgeCreationChecker,
|
|
23
|
-
EdgeTypeHint,
|
|
24
|
-
GModelElement,
|
|
25
|
-
Logger,
|
|
26
|
-
ModelState,
|
|
27
|
-
NullLogger
|
|
28
|
-
} from '@eclipse-glsp/server';
|
|
29
|
-
import { expect } from 'chai';
|
|
30
|
-
import { Container, ContainerModule } from 'inversify';
|
|
31
|
-
import { McpToolResult } from '../../server/mcp-handler-shared';
|
|
32
|
-
import { McpIdAliasService } from '../../server/mcp-id-alias-service';
|
|
33
|
-
import { DefaultMcpLabelProvider, McpLabelProvider } from '../../server/mcp-label-provider';
|
|
34
|
-
import { CreateEdgesInput, CreateEdgesMcpToolHandler } from './create-edges-mcp-tool-handler';
|
|
35
|
-
|
|
36
|
-
function makeElement(id: string, type: string): GModelElement {
|
|
37
|
-
return { id, type, children: [] } as unknown as GModelElement;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function makeModelState(elements: GModelElement[]): ModelState {
|
|
41
|
-
const byId = new Map(elements.map(el => [el.id, el]));
|
|
42
|
-
return {
|
|
43
|
-
index: {
|
|
44
|
-
allIds: () => [...byId.keys()],
|
|
45
|
-
get: (id: string) => byId.get(id),
|
|
46
|
-
find: (id: string) => byId.get(id)
|
|
47
|
-
},
|
|
48
|
-
isReadonly: false
|
|
49
|
-
} as unknown as ModelState;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
class StubEdgeCreationChecker implements EdgeCreationChecker {
|
|
53
|
-
public lastTargetCall?: { edgeType: string; source: GModelElement; target: GModelElement };
|
|
54
|
-
constructor(private readonly result: boolean) {}
|
|
55
|
-
isValidSource(): boolean {
|
|
56
|
-
return this.result;
|
|
57
|
-
}
|
|
58
|
-
isValidTarget(edgeType: string, source: GModelElement, target: GModelElement): boolean {
|
|
59
|
-
this.lastTargetCall = { edgeType, source, target };
|
|
60
|
-
return this.result;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
interface BuildArgs {
|
|
65
|
-
elements: GModelElement[];
|
|
66
|
-
edgeTypeHints: Pick<EdgeTypeHint, 'elementTypeId' | 'dynamic'>[];
|
|
67
|
-
checker?: EdgeCreationChecker;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Records dispatched actions so create-mode tests can assert no dispatch happened. */
|
|
71
|
-
class RecordingDispatcher {
|
|
72
|
-
public dispatched: Action[] = [];
|
|
73
|
-
async dispatch(action: Action): Promise<void> {
|
|
74
|
-
this.dispatched.push(action);
|
|
75
|
-
}
|
|
76
|
-
async dispatchAll(): Promise<void> {
|
|
77
|
-
// not used by create-edges
|
|
78
|
-
}
|
|
79
|
-
dispatchAfterNextUpdate(): void {
|
|
80
|
-
// not used
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function buildHandler({ elements, edgeTypeHints, checker }: BuildArgs): {
|
|
85
|
-
handler: CreateEdgesMcpToolHandler;
|
|
86
|
-
dispatcher: RecordingDispatcher;
|
|
87
|
-
} {
|
|
88
|
-
const dispatcher = new RecordingDispatcher();
|
|
89
|
-
const container = new Container();
|
|
90
|
-
container.load(
|
|
91
|
-
new ContainerModule(bind => {
|
|
92
|
-
bind(Logger).toConstantValue(new NullLogger());
|
|
93
|
-
bind(ClientId).toConstantValue('test-session');
|
|
94
|
-
bind(ModelState).toConstantValue(makeModelState(elements));
|
|
95
|
-
bind(McpIdAliasService).toConstantValue({
|
|
96
|
-
lookup: (id: string) => id,
|
|
97
|
-
alias: (id: string) => id
|
|
98
|
-
} as McpIdAliasService);
|
|
99
|
-
bind(DiagramConfiguration).toConstantValue({ edgeTypeHints } as unknown as DiagramConfiguration);
|
|
100
|
-
bind(ActionDispatcher).toConstantValue(dispatcher as unknown as ActionDispatcher);
|
|
101
|
-
if (checker) {
|
|
102
|
-
bind(EdgeCreationChecker).toConstantValue(checker);
|
|
103
|
-
}
|
|
104
|
-
bind(McpLabelProvider).to(DefaultMcpLabelProvider);
|
|
105
|
-
bind(CreateEdgesMcpToolHandler).toSelf();
|
|
106
|
-
})
|
|
107
|
-
);
|
|
108
|
-
return { handler: container.get(CreateEdgesMcpToolHandler), dispatcher };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function callCreateResult(handler: CreateEdgesMcpToolHandler, params: CreateEdgesInput): Promise<McpToolResult> {
|
|
112
|
-
return (handler as unknown as { createResult: (p: CreateEdgesInput) => Promise<McpToolResult> }).createResult(params);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface DryRunStructured {
|
|
116
|
-
createdEdges: { id: string; elementTypeId: string; label?: string }[];
|
|
117
|
-
errors: string[];
|
|
118
|
-
validationResults: { edgeType: string; sourceElementId: string; targetElementId: string; isValid: boolean; reason?: string }[];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
describe('CreateEdgesMcpToolHandler · dryRun', () => {
|
|
122
|
-
const baseInput = (overrides: Partial<CreateEdgesInput> = {}): CreateEdgesInput => ({
|
|
123
|
-
sessionId: 's',
|
|
124
|
-
dryRun: true,
|
|
125
|
-
edges: [{ elementTypeId: 'edge:dynamic', sourceElementId: 's', targetElementId: 't' }],
|
|
126
|
-
...overrides
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('returns isValid:true with a "no dynamic hint" reason when the edgeType has no dynamic hint', async () => {
|
|
130
|
-
const { handler, dispatcher } = buildHandler({
|
|
131
|
-
elements: [makeElement('s', 'task'), makeElement('t', 'task')],
|
|
132
|
-
edgeTypeHints: [{ elementTypeId: 'edge:static', dynamic: false }]
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
const result = await callCreateResult(
|
|
136
|
-
handler,
|
|
137
|
-
baseInput({ edges: [{ elementTypeId: 'edge:static', sourceElementId: 's', targetElementId: 't' }] })
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const structured = result.structuredContent as unknown as DryRunStructured;
|
|
141
|
-
expect(structured.validationResults).to.have.lengthOf(1);
|
|
142
|
-
expect(structured.validationResults[0].isValid).to.equal(true);
|
|
143
|
-
expect(structured.validationResults[0].reason).to.match(/no dynamic edge-type hint/i);
|
|
144
|
-
expect(structured.createdEdges).to.deep.equal([]);
|
|
145
|
-
expect(dispatcher.dispatched).to.have.lengthOf(0);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('flags isValid:false with a config-gap reason when edgeType declares a dynamic hint but no checker is bound', async () => {
|
|
149
|
-
const { handler, dispatcher } = buildHandler({
|
|
150
|
-
elements: [makeElement('s', 'task'), makeElement('t', 'task')],
|
|
151
|
-
edgeTypeHints: [{ elementTypeId: 'edge:dynamic', dynamic: true }]
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const result = await callCreateResult(handler, baseInput());
|
|
155
|
-
|
|
156
|
-
const structured = result.structuredContent as unknown as DryRunStructured;
|
|
157
|
-
expect(structured.validationResults[0].isValid).to.equal(false);
|
|
158
|
-
expect(structured.validationResults[0].reason).to.include('EdgeCreationChecker is not bound');
|
|
159
|
-
expect(dispatcher.dispatched).to.have.lengthOf(0);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('delegates to EdgeCreationChecker.isValidTarget when target + dynamic hint + checker are present', async () => {
|
|
163
|
-
const sourceElement = makeElement('s', 'task');
|
|
164
|
-
const targetElement = makeElement('t', 'task');
|
|
165
|
-
const checker = new StubEdgeCreationChecker(false);
|
|
166
|
-
const { handler, dispatcher } = buildHandler({
|
|
167
|
-
elements: [sourceElement, targetElement],
|
|
168
|
-
edgeTypeHints: [{ elementTypeId: 'edge:dynamic', dynamic: true }],
|
|
169
|
-
checker
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const result = await callCreateResult(handler, baseInput());
|
|
173
|
-
|
|
174
|
-
expect(checker.lastTargetCall).to.deep.equal({
|
|
175
|
-
edgeType: 'edge:dynamic',
|
|
176
|
-
source: sourceElement,
|
|
177
|
-
target: targetElement
|
|
178
|
-
});
|
|
179
|
-
const structured = result.structuredContent as unknown as DryRunStructured;
|
|
180
|
-
expect(structured.validationResults[0].isValid).to.equal(false);
|
|
181
|
-
expect(dispatcher.dispatched).to.have.lengthOf(0);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('reports source/target not-found inline as a per-edge validation failure (does not throw)', async () => {
|
|
185
|
-
const { handler } = buildHandler({
|
|
186
|
-
elements: [makeElement('s', 'task')],
|
|
187
|
-
edgeTypeHints: [{ elementTypeId: 'edge:dynamic', dynamic: true }]
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const result = await callCreateResult(
|
|
191
|
-
handler,
|
|
192
|
-
baseInput({ edges: [{ elementTypeId: 'edge:dynamic', sourceElementId: 's', targetElementId: 'missing' }] })
|
|
193
|
-
);
|
|
194
|
-
const structured = result.structuredContent as unknown as DryRunStructured;
|
|
195
|
-
expect(structured.validationResults[0].isValid).to.equal(false);
|
|
196
|
-
expect(structured.validationResults[0].reason).to.include('Target element not found');
|
|
197
|
-
});
|
|
198
|
-
});
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
-
*
|
|
4
|
-
* This program and the accompanying materials are made available under the
|
|
5
|
-
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
*
|
|
8
|
-
* This Source Code may also be made available under the following Secondary
|
|
9
|
-
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
* with the GNU Classpath Exception which is available at
|
|
12
|
-
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
*
|
|
14
|
-
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
-
********************************************************************************/
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
Action,
|
|
19
|
-
ActionDispatcher,
|
|
20
|
-
ApplyLabelEditOperation,
|
|
21
|
-
ClientId,
|
|
22
|
-
CreateNodeOperation,
|
|
23
|
-
GLabel,
|
|
24
|
-
GModelElement,
|
|
25
|
-
Logger,
|
|
26
|
-
ModelState,
|
|
27
|
-
NullLogger
|
|
28
|
-
} from '@eclipse-glsp/server';
|
|
29
|
-
import { expect } from 'chai';
|
|
30
|
-
import { Container, ContainerModule } from 'inversify';
|
|
31
|
-
import { McpToolResult } from '../../server/mcp-handler-shared';
|
|
32
|
-
import { McpIdAliasService } from '../../server/mcp-id-alias-service';
|
|
33
|
-
import { McpLabelProvider } from '../../server/mcp-label-provider';
|
|
34
|
-
import { CreateNodesInput, CreateNodesMcpToolHandler } from './create-nodes-mcp-tool-handler';
|
|
35
|
-
|
|
36
|
-
interface CreatedElementsStructured {
|
|
37
|
-
createdNodes: { id: string; elementTypeId: string; label?: string }[];
|
|
38
|
-
errors: string[];
|
|
39
|
-
warnings: string[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function makeElement(id: string, type: string, children: GModelElement[] = []): GModelElement {
|
|
43
|
-
return { id, type, children } as unknown as GModelElement;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Mutable model that drops in new elements as CreateNodeOperations are dispatched. */
|
|
47
|
-
class FakeModelState {
|
|
48
|
-
public elements = new Map<string, GModelElement>();
|
|
49
|
-
constructor(initial: GModelElement[]) {
|
|
50
|
-
initial.forEach(el => this.elements.set(el.id, el));
|
|
51
|
-
}
|
|
52
|
-
addElement(el: GModelElement): void {
|
|
53
|
-
this.elements.set(el.id, el);
|
|
54
|
-
}
|
|
55
|
-
get index(): ModelState['index'] {
|
|
56
|
-
return {
|
|
57
|
-
allIds: () => [...this.elements.keys()],
|
|
58
|
-
get: (id: string) => this.elements.get(id),
|
|
59
|
-
find: (id: string) => this.elements.get(id)
|
|
60
|
-
} as unknown as ModelState['index'];
|
|
61
|
-
}
|
|
62
|
-
get isReadonly(): boolean {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Dispatcher that materializes a new element per `CreateNodeOperation` and records the action. */
|
|
68
|
-
class CreatingDispatcher {
|
|
69
|
-
public dispatched: Action[] = [];
|
|
70
|
-
private idCounter = 0;
|
|
71
|
-
constructor(
|
|
72
|
-
private readonly model: FakeModelState,
|
|
73
|
-
private readonly labeled: Set<string>
|
|
74
|
-
) {}
|
|
75
|
-
|
|
76
|
-
async dispatch(action: Action): Promise<void> {
|
|
77
|
-
this.dispatched.push(action);
|
|
78
|
-
if (CreateNodeOperation.is(action)) {
|
|
79
|
-
const id = `${action.elementTypeId}#${++this.idCounter}`;
|
|
80
|
-
const children: GModelElement[] = this.labeled.has(action.elementTypeId)
|
|
81
|
-
? [Object.assign(new GLabel(), { id: `${id}_label`, type: 'label', children: [] })]
|
|
82
|
-
: [];
|
|
83
|
-
this.model.addElement(makeElement(id, action.elementTypeId, children));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface BuildArgs {
|
|
89
|
-
/** Element types whose newly created instance includes a child GLabel. */
|
|
90
|
-
labeled?: string[];
|
|
91
|
-
/** Element types for which CreateNodeOperation should produce no new element (silent failure simulation). */
|
|
92
|
-
failed?: string[];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function buildHandler({ labeled = [], failed = [] }: BuildArgs = {}): {
|
|
96
|
-
handler: CreateNodesMcpToolHandler;
|
|
97
|
-
dispatcher: CreatingDispatcher;
|
|
98
|
-
} {
|
|
99
|
-
const model = new FakeModelState([]);
|
|
100
|
-
const failedSet = new Set(failed);
|
|
101
|
-
const labeledSet = new Set(labeled);
|
|
102
|
-
const dispatcher = new (class extends CreatingDispatcher {
|
|
103
|
-
override async dispatch(action: Action): Promise<void> {
|
|
104
|
-
if (CreateNodeOperation.is(action) && failedSet.has(action.elementTypeId)) {
|
|
105
|
-
this.dispatched.push(action);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
return super.dispatch(action);
|
|
109
|
-
}
|
|
110
|
-
})(model, labeledSet);
|
|
111
|
-
|
|
112
|
-
const container = new Container();
|
|
113
|
-
container.load(
|
|
114
|
-
new ContainerModule(bind => {
|
|
115
|
-
bind(Logger).toConstantValue(new NullLogger());
|
|
116
|
-
bind(ClientId).toConstantValue('test-session');
|
|
117
|
-
bind(ModelState).toConstantValue(model as unknown as ModelState);
|
|
118
|
-
bind(McpIdAliasService).toConstantValue({
|
|
119
|
-
lookup: (id: string) => id,
|
|
120
|
-
alias: (id: string) => id
|
|
121
|
-
} as McpIdAliasService);
|
|
122
|
-
bind(ActionDispatcher).toConstantValue(dispatcher as unknown as ActionDispatcher);
|
|
123
|
-
bind(McpLabelProvider).toConstantValue({
|
|
124
|
-
getLabel: (element: GModelElement) => element.children.find(c => c instanceof GLabel) as GLabel | undefined
|
|
125
|
-
} as McpLabelProvider);
|
|
126
|
-
bind(CreateNodesMcpToolHandler).toSelf();
|
|
127
|
-
})
|
|
128
|
-
);
|
|
129
|
-
return { handler: container.get(CreateNodesMcpToolHandler), dispatcher };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function callCreateResult(handler: CreateNodesMcpToolHandler, params: CreateNodesInput): Promise<McpToolResult> {
|
|
133
|
-
return (handler as unknown as { createResult: (p: CreateNodesInput) => Promise<McpToolResult> }).createResult(params);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
describe('CreateNodesMcpToolHandler', () => {
|
|
137
|
-
it('dispatches one CreateNodeOperation per node and reports the new element ids', async () => {
|
|
138
|
-
const { handler, dispatcher } = buildHandler();
|
|
139
|
-
|
|
140
|
-
const result = await callCreateResult(handler, {
|
|
141
|
-
sessionId: 's',
|
|
142
|
-
nodes: [
|
|
143
|
-
{ elementTypeId: 'task:manual', position: { x: 0, y: 0 } },
|
|
144
|
-
{ elementTypeId: 'task:manual', position: { x: 100, y: 0 } }
|
|
145
|
-
]
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const structured = result.structuredContent as unknown as CreatedElementsStructured;
|
|
149
|
-
expect(structured.createdNodes).to.have.lengthOf(2);
|
|
150
|
-
expect(structured.createdNodes[0].id).to.equal('task:manual#1');
|
|
151
|
-
expect(structured.createdNodes[1].id).to.equal('task:manual#2');
|
|
152
|
-
expect(structured.errors).to.deep.equal([]);
|
|
153
|
-
expect(structured.warnings).to.deep.equal([]);
|
|
154
|
-
expect(dispatcher.dispatched.filter(CreateNodeOperation.is)).to.have.lengthOf(2);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('dispatches ApplyLabelEditOperation when `text` is supplied and the new element has a label', async () => {
|
|
158
|
-
const { handler, dispatcher } = buildHandler({ labeled: ['task:manual'] });
|
|
159
|
-
|
|
160
|
-
await callCreateResult(handler, {
|
|
161
|
-
sessionId: 's',
|
|
162
|
-
nodes: [{ elementTypeId: 'task:manual', position: { x: 0, y: 0 }, text: 'Hello' }]
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const edits = dispatcher.dispatched.filter(ApplyLabelEditOperation.is);
|
|
166
|
-
expect(edits).to.have.lengthOf(1);
|
|
167
|
-
expect(edits[0].text).to.equal('Hello');
|
|
168
|
-
expect(edits[0].labelId).to.equal('task:manual#1_label');
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('emits a warning (no ApplyLabelEditOperation) when `text` is supplied but the element type has no label', async () => {
|
|
172
|
-
const { handler, dispatcher } = buildHandler({ labeled: [] });
|
|
173
|
-
|
|
174
|
-
const result = await callCreateResult(handler, {
|
|
175
|
-
sessionId: 's',
|
|
176
|
-
nodes: [{ elementTypeId: 'decision', position: { x: 0, y: 0 }, text: 'Ignored' }]
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
const structured = result.structuredContent as unknown as CreatedElementsStructured;
|
|
180
|
-
expect(structured.warnings).to.have.lengthOf(1);
|
|
181
|
-
expect(structured.warnings[0]).to.match(/no editable label/i);
|
|
182
|
-
expect(dispatcher.dispatched.filter(ApplyLabelEditOperation.is)).to.have.lengthOf(0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('records an error and skips label-edit when the CreateNodeOperation produces no new element', async () => {
|
|
186
|
-
const { handler, dispatcher } = buildHandler({ failed: ['ghost'] });
|
|
187
|
-
|
|
188
|
-
const result = await callCreateResult(handler, {
|
|
189
|
-
sessionId: 's',
|
|
190
|
-
nodes: [{ elementTypeId: 'ghost', position: { x: 0, y: 0 }, text: 'Hello' }]
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const structured = result.structuredContent as unknown as CreatedElementsStructured;
|
|
194
|
-
expect(structured.createdNodes).to.deep.equal([]);
|
|
195
|
-
expect(structured.errors).to.have.lengthOf(1);
|
|
196
|
-
expect(structured.errors[0]).to.include('ghost');
|
|
197
|
-
expect(dispatcher.dispatched.filter(ApplyLabelEditOperation.is)).to.have.lengthOf(0);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
@@ -1,212 +0,0 @@
|
|
|
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
|
-
});
|