@eclipse-glsp/server-mcp 2.8.0-next.0 → 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,235 +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 { Logger, NullLogger } from '@eclipse-glsp/server';
|
|
18
|
-
import { ServerNotification } from '@modelcontextprotocol/sdk/types.js';
|
|
19
|
-
import { expect } from 'chai';
|
|
20
|
-
import { Container, ContainerModule } from 'inversify';
|
|
21
|
-
import { DefaultMcpLogLevelRegistry, McpLogLevelRegistry } from './mcp-log-level-registry';
|
|
22
|
-
import { McpLogger } from './mcp-logger';
|
|
23
|
-
import { NodeMcpRequestContext } from '../../node/server/node-mcp-request-context';
|
|
24
|
-
import { McpRequestContext, McpRequestExtra } from './mcp-request-context';
|
|
25
|
-
|
|
26
|
-
interface RecordedLog {
|
|
27
|
-
level: string;
|
|
28
|
-
message: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
class RecordingLogger extends NullLogger {
|
|
32
|
-
readonly entries: RecordedLog[] = [];
|
|
33
|
-
override info(message: string): void {
|
|
34
|
-
this.entries.push({ level: 'info', message });
|
|
35
|
-
}
|
|
36
|
-
override warn(message: string): void {
|
|
37
|
-
this.entries.push({ level: 'warn', message });
|
|
38
|
-
}
|
|
39
|
-
override error(message: string): void {
|
|
40
|
-
this.entries.push({ level: 'error', message });
|
|
41
|
-
}
|
|
42
|
-
override debug(message: string): void {
|
|
43
|
-
this.entries.push({ level: 'debug', message });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function buildLogger(): {
|
|
48
|
-
logger: McpLogger;
|
|
49
|
-
glspLogger: RecordingLogger;
|
|
50
|
-
levelRegistry: DefaultMcpLogLevelRegistry;
|
|
51
|
-
requestContext: McpRequestContext;
|
|
52
|
-
} {
|
|
53
|
-
const container = new Container();
|
|
54
|
-
const glspLogger = new RecordingLogger();
|
|
55
|
-
const levelRegistry = new DefaultMcpLogLevelRegistry();
|
|
56
|
-
const requestContext = new NodeMcpRequestContext();
|
|
57
|
-
container.load(
|
|
58
|
-
new ContainerModule(bind => {
|
|
59
|
-
bind(Logger).toConstantValue(glspLogger);
|
|
60
|
-
bind(McpLogLevelRegistry).toConstantValue(levelRegistry);
|
|
61
|
-
bind(McpRequestContext).toConstantValue(requestContext);
|
|
62
|
-
bind(McpLogger).toSelf().inSingletonScope();
|
|
63
|
-
})
|
|
64
|
-
);
|
|
65
|
-
return { logger: container.get(McpLogger), glspLogger, levelRegistry, requestContext };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Build a stub `RequestHandlerExtra` with a recording `sendNotification`. Only `sendNotification`
|
|
70
|
-
* is exercised by `McpLogger`; the remaining fields are never read here, so we cast rather
|
|
71
|
-
* than fabricate the full SDK shape.
|
|
72
|
-
*/
|
|
73
|
-
function buildExtra(sessionId?: string): { extra: McpRequestExtra; sent: ServerNotification[] } {
|
|
74
|
-
const sent: ServerNotification[] = [];
|
|
75
|
-
const extra = {
|
|
76
|
-
sessionId,
|
|
77
|
-
sendNotification: async (n: ServerNotification) => {
|
|
78
|
-
sent.push(n);
|
|
79
|
-
}
|
|
80
|
-
} as unknown as McpRequestExtra;
|
|
81
|
-
return { extra, sent };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
describe('McpLogger', () => {
|
|
85
|
-
describe('outside a request context (no MCP client to deliver to)', () => {
|
|
86
|
-
it('routes info/warn/error/debug to the GLSP Logger only', () => {
|
|
87
|
-
const { logger, glspLogger } = buildLogger();
|
|
88
|
-
|
|
89
|
-
logger.info('hello');
|
|
90
|
-
logger.warn('careful');
|
|
91
|
-
logger.error('boom');
|
|
92
|
-
logger.debug('trace');
|
|
93
|
-
|
|
94
|
-
expect(glspLogger.entries).to.deep.equal([
|
|
95
|
-
{ level: 'info', message: 'hello' },
|
|
96
|
-
{ level: 'warn', message: 'careful' },
|
|
97
|
-
{ level: 'error', message: 'boom' },
|
|
98
|
-
{ level: 'debug', message: 'trace' }
|
|
99
|
-
]);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('inside an requestContext.run frame', () => {
|
|
104
|
-
it('emits notifications/message to the bound MCP client AND the GLSP Logger', async () => {
|
|
105
|
-
const { logger, glspLogger, requestContext } = buildLogger();
|
|
106
|
-
const { extra, sent } = buildExtra();
|
|
107
|
-
|
|
108
|
-
await requestContext.run(extra, async () => {
|
|
109
|
-
logger.info('one');
|
|
110
|
-
logger.warn('two');
|
|
111
|
-
logger.error('three');
|
|
112
|
-
logger.debug('four');
|
|
113
|
-
// Allow the fire-and-forget `.catch` chain in McpLogger.notify to settle.
|
|
114
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
expect(glspLogger.entries.map(e => e.message)).to.deep.equal(['one', 'two', 'three', 'four']);
|
|
118
|
-
expect(
|
|
119
|
-
sent.map(n => ({
|
|
120
|
-
method: n.method,
|
|
121
|
-
level: (n.params as { level: string }).level,
|
|
122
|
-
data: (n.params as { data: string }).data
|
|
123
|
-
}))
|
|
124
|
-
).to.deep.equal([
|
|
125
|
-
{ method: 'notifications/message', level: 'info', data: 'one' },
|
|
126
|
-
{ method: 'notifications/message', level: 'warning', data: 'two' }, // GLSP warn → MCP warning
|
|
127
|
-
{ method: 'notifications/message', level: 'error', data: 'three' },
|
|
128
|
-
{ method: 'notifications/message', level: 'debug', data: 'four' }
|
|
129
|
-
]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('swallows transport failures so a broken MCP send never breaks the producing tool', async () => {
|
|
133
|
-
const { logger, glspLogger, requestContext } = buildLogger();
|
|
134
|
-
const failingExtra = {
|
|
135
|
-
sendNotification: async () => {
|
|
136
|
-
throw new Error('transport closed');
|
|
137
|
-
}
|
|
138
|
-
} as unknown as McpRequestExtra;
|
|
139
|
-
|
|
140
|
-
await requestContext.run(failingExtra, async () => {
|
|
141
|
-
expect(() => logger.error('still works')).to.not.throw();
|
|
142
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Server-side log still fired.
|
|
146
|
-
expect(glspLogger.entries).to.deep.equal([{ level: 'error', message: 'still works' }]);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('concurrent request contexts', () => {
|
|
151
|
-
it('keeps each request frame isolated via AsyncLocalStorage', async () => {
|
|
152
|
-
const { logger, requestContext } = buildLogger();
|
|
153
|
-
const { extra: extraA, sent: sentA } = buildExtra();
|
|
154
|
-
const { extra: extraB, sent: sentB } = buildExtra();
|
|
155
|
-
|
|
156
|
-
await Promise.all([
|
|
157
|
-
requestContext.run(extraA, async () => {
|
|
158
|
-
logger.info('A');
|
|
159
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
160
|
-
logger.info('A-after-yield');
|
|
161
|
-
}),
|
|
162
|
-
requestContext.run(extraB, async () => {
|
|
163
|
-
logger.info('B');
|
|
164
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
165
|
-
logger.info('B-after-yield');
|
|
166
|
-
})
|
|
167
|
-
]);
|
|
168
|
-
|
|
169
|
-
expect(sentA.map(n => (n.params as { data: string }).data)).to.deep.equal(['A', 'A-after-yield']);
|
|
170
|
-
expect(sentB.map(n => (n.params as { data: string }).data)).to.deep.equal(['B', 'B-after-yield']);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('logging/setLevel threshold gate (G4)', () => {
|
|
175
|
-
it('drops messages below the per-session threshold; the GLSP-side log still fires', async () => {
|
|
176
|
-
const { logger, glspLogger, levelRegistry, requestContext } = buildLogger();
|
|
177
|
-
const { extra, sent } = buildExtra('session-X');
|
|
178
|
-
// Client opted down to "warning" — info and debug must be dropped on the MCP side.
|
|
179
|
-
levelRegistry.setLevel('session-X', 'warning');
|
|
180
|
-
|
|
181
|
-
await requestContext.run(extra, async () => {
|
|
182
|
-
logger.info('chatty');
|
|
183
|
-
logger.debug('verbose');
|
|
184
|
-
logger.warn('important');
|
|
185
|
-
logger.error('critical');
|
|
186
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// GLSP-side log path is independent of the MCP threshold — keeps adopter logs intact.
|
|
190
|
-
expect(glspLogger.entries.map(entry => entry.message)).to.deep.equal(['chatty', 'verbose', 'important', 'critical']);
|
|
191
|
-
// MCP side: only warn + error survive.
|
|
192
|
-
expect(sent.map(n => (n.params as { level: string }).level)).to.deep.equal(['warning', 'error']);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('isolates thresholds across sessions (different setLevel per session id)', async () => {
|
|
196
|
-
const { logger, levelRegistry, requestContext } = buildLogger();
|
|
197
|
-
const { extra: extraA, sent: sentA } = buildExtra('session-A');
|
|
198
|
-
const { extra: extraB, sent: sentB } = buildExtra('session-B');
|
|
199
|
-
levelRegistry.setLevel('session-A', 'error');
|
|
200
|
-
levelRegistry.setLevel('session-B', 'debug');
|
|
201
|
-
|
|
202
|
-
await Promise.all([
|
|
203
|
-
requestContext.run(extraA, async () => {
|
|
204
|
-
logger.info('A-info');
|
|
205
|
-
logger.error('A-error');
|
|
206
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
207
|
-
}),
|
|
208
|
-
requestContext.run(extraB, async () => {
|
|
209
|
-
logger.info('B-info');
|
|
210
|
-
logger.error('B-error');
|
|
211
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
212
|
-
})
|
|
213
|
-
]);
|
|
214
|
-
|
|
215
|
-
expect(sentA.map(n => (n.params as { data: string }).data)).to.deep.equal(['A-error']);
|
|
216
|
-
expect(sentB.map(n => (n.params as { data: string }).data).sort()).to.deep.equal(['B-error', 'B-info']);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('default threshold (no setLevel sent) lets every level through (preserves prior behavior)', async () => {
|
|
220
|
-
const { logger, requestContext } = buildLogger();
|
|
221
|
-
const { extra, sent } = buildExtra('session-default');
|
|
222
|
-
// No setLevel call → default threshold = 'debug' → everything emitted.
|
|
223
|
-
|
|
224
|
-
await requestContext.run(extra, async () => {
|
|
225
|
-
logger.debug('d');
|
|
226
|
-
logger.info('i');
|
|
227
|
-
logger.warn('w');
|
|
228
|
-
logger.error('e');
|
|
229
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(sent.map(n => (n.params as { level: string }).level)).to.deep.equal(['debug', 'info', 'warning', 'error']);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
});
|
|
@@ -1,103 +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 { ServerNotification } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
-
import { expect } from 'chai';
|
|
19
|
-
import { McpProgressReporter } from './mcp-progress-reporter';
|
|
20
|
-
import { Container } from 'inversify';
|
|
21
|
-
import { NodeMcpRequestContext } from '../../node/server/node-mcp-request-context';
|
|
22
|
-
import { McpRequestContext, McpRequestExtra } from './mcp-request-context';
|
|
23
|
-
|
|
24
|
-
function buildExtra(progressToken?: string | number): { extra: McpRequestExtra; sent: ServerNotification[] } {
|
|
25
|
-
const sent: ServerNotification[] = [];
|
|
26
|
-
const extra = {
|
|
27
|
-
sendNotification: async (n: ServerNotification) => {
|
|
28
|
-
sent.push(n);
|
|
29
|
-
},
|
|
30
|
-
_meta: progressToken === undefined ? undefined : { progressToken }
|
|
31
|
-
} as unknown as McpRequestExtra;
|
|
32
|
-
return { extra, sent };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function buildReporter(): { reporter: McpProgressReporter; requestContext: McpRequestContext } {
|
|
36
|
-
const container = new Container();
|
|
37
|
-
const requestContext = new NodeMcpRequestContext();
|
|
38
|
-
container.bind(McpRequestContext).toConstantValue(requestContext);
|
|
39
|
-
container.bind(McpProgressReporter).toSelf().inSingletonScope();
|
|
40
|
-
return { reporter: container.get(McpProgressReporter), requestContext };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe('McpProgressReporter', () => {
|
|
44
|
-
it('no-ops inside a request that has no progressToken (client did not opt in)', async () => {
|
|
45
|
-
const { reporter, requestContext } = buildReporter();
|
|
46
|
-
const { extra, sent } = buildExtra(undefined);
|
|
47
|
-
|
|
48
|
-
await requestContext.run(extra, async () => {
|
|
49
|
-
await reporter.emit({ progress: 0, message: 'starting' });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
expect(sent).to.have.lengthOf(0);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('emits notifications/progress when the request carries a progressToken', async () => {
|
|
56
|
-
const { reporter, requestContext } = buildReporter();
|
|
57
|
-
const { extra, sent } = buildExtra('tok-42');
|
|
58
|
-
|
|
59
|
-
await requestContext.run(extra, async () => {
|
|
60
|
-
await reporter.emit({ progress: 0, message: 'starting' });
|
|
61
|
-
await reporter.emit({ progress: 1, total: 3, message: 'step 1/3' });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
expect(sent).to.have.lengthOf(2);
|
|
65
|
-
expect(sent[0]).to.deep.equal({
|
|
66
|
-
method: 'notifications/progress',
|
|
67
|
-
params: { progressToken: 'tok-42', progress: 0, message: 'starting' }
|
|
68
|
-
});
|
|
69
|
-
expect(sent[1]).to.deep.equal({
|
|
70
|
-
method: 'notifications/progress',
|
|
71
|
-
params: { progressToken: 'tok-42', progress: 1, total: 3, message: 'step 1/3' }
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('accepts numeric progress tokens', async () => {
|
|
76
|
-
const { reporter, requestContext } = buildReporter();
|
|
77
|
-
const { extra, sent } = buildExtra(7);
|
|
78
|
-
|
|
79
|
-
await requestContext.run(extra, async () => {
|
|
80
|
-
await reporter.emit({ progress: 0 });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
expect(sent).to.have.lengthOf(1);
|
|
84
|
-
expect(sent[0].params).to.deep.equal({ progressToken: 7, progress: 0 });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('swallows transport failures so a broken send never breaks the producing tool', async () => {
|
|
88
|
-
const { reporter, requestContext } = buildReporter();
|
|
89
|
-
const failingExtra = {
|
|
90
|
-
sendNotification: async () => {
|
|
91
|
-
throw new Error('transport closed');
|
|
92
|
-
},
|
|
93
|
-
_meta: { progressToken: 'tok-1' }
|
|
94
|
-
} as unknown as McpRequestExtra;
|
|
95
|
-
|
|
96
|
-
await requestContext.run(failingExtra, async () => {
|
|
97
|
-
// Must complete without throwing. If we re-threw, every PNG export would hard-fail
|
|
98
|
-
// when the client's SSE stream blipped — the opposite of progress reporting being
|
|
99
|
-
// a UX nicety.
|
|
100
|
-
await reporter.emit({ progress: 0, message: 'starting' });
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
});
|
|
@@ -1,182 +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 { expect } from 'chai';
|
|
18
|
-
import * as z from 'zod/v4';
|
|
19
|
-
import { McpToolResult } from './mcp-handler-shared';
|
|
20
|
-
import { CreateEdgesMcpToolHandler, CreateEdgesOutputSchema } from '../tools/handlers/create-edges-mcp-tool-handler';
|
|
21
|
-
import { CreateNodesMcpToolHandler, CreateNodesOutputSchema } from '../tools/handlers/create-nodes-mcp-tool-handler';
|
|
22
|
-
import { DeleteElementsMcpToolHandler, DeleteElementsOutputSchema } from '../tools/handlers/delete-elements-mcp-tool-handler';
|
|
23
|
-
import { GetSelectionMcpToolHandler, GetSelectionOutputSchema } from '../tools/handlers/get-selection-mcp-tool-handler';
|
|
24
|
-
import { LayoutMcpToolHandler, LayoutOutputSchema } from '../tools/handlers/layout-mcp-tool-handler';
|
|
25
|
-
import { ModifyEdgesMcpToolHandler, ModifyEdgesOutputSchema } from '../tools/handlers/modify-edges-mcp-tool-handler';
|
|
26
|
-
import { ModifyNodesMcpToolHandler, ModifyNodesOutputSchema } from '../tools/handlers/modify-nodes-mcp-tool-handler';
|
|
27
|
-
import { QueryElementsMcpToolHandler, QueryElementsOutputSchema } from '../tools/handlers/query-elements-mcp-tool-handler';
|
|
28
|
-
import { RedoMcpToolHandler, RedoOutputSchema } from '../tools/handlers/redo-mcp-tool-handler';
|
|
29
|
-
import { SaveModelMcpToolHandler, SaveModelOutputSchema } from '../tools/handlers/save-model-mcp-tool-handler';
|
|
30
|
-
import { SetViewMcpToolHandler, SetViewOutputSchema } from '../tools/handlers/set-view-mcp-tool-handler';
|
|
31
|
-
import { UndoMcpToolHandler, UndoOutputSchema } from '../tools/handlers/undo-mcp-tool-handler';
|
|
32
|
-
import { ValidateDiagramMcpToolHandler, ValidateDiagramOutputSchema } from '../tools/handlers/validate-diagram-mcp-tool-handler';
|
|
33
|
-
import { AbstractMcpToolHandler } from './mcp-tool-handler';
|
|
34
|
-
|
|
35
|
-
// ─── Framework: BaseMcpToolHandler.success(...) and toRegistrationConfig() ────────────────────
|
|
36
|
-
|
|
37
|
-
class WithoutOutputSchemaHandler extends AbstractMcpToolHandler {
|
|
38
|
-
readonly name = 'no-schema';
|
|
39
|
-
readonly description = 'no-schema';
|
|
40
|
-
readonly inputSchema = z.object({});
|
|
41
|
-
|
|
42
|
-
protected createResult(): McpToolResult {
|
|
43
|
-
return this.success('plain');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Test exposure of the protected helper. `BaseMcpToolHandler` doesn't read `logger` here. */
|
|
47
|
-
public testSuccess(message: string, structured?: Record<string, unknown>): McpToolResult {
|
|
48
|
-
return this.success(message, structured);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
class WithOutputSchemaHandler extends AbstractMcpToolHandler {
|
|
53
|
-
readonly name = 'with-schema';
|
|
54
|
-
readonly description = 'with-schema';
|
|
55
|
-
readonly inputSchema = z.object({});
|
|
56
|
-
override readonly outputSchema = z.object({ ok: z.boolean() });
|
|
57
|
-
|
|
58
|
-
protected createResult(): McpToolResult {
|
|
59
|
-
return this.success('with', { ok: true });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe('Dual-emit framework', () => {
|
|
64
|
-
it('success(message) without structured payload omits structuredContent', () => {
|
|
65
|
-
const result = new WithoutOutputSchemaHandler().testSuccess('hi');
|
|
66
|
-
expect(result.content).to.deep.equal([{ type: 'text', text: 'hi' }]);
|
|
67
|
-
expect(result.structuredContent).to.equal(undefined);
|
|
68
|
-
expect(result.isError).to.equal(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('success(message, structured) emits structuredContent', () => {
|
|
72
|
-
const result = new WithoutOutputSchemaHandler().testSuccess('hi', { count: 3, ids: ['a'] });
|
|
73
|
-
expect(result.content).to.deep.equal([{ type: 'text', text: 'hi' }]);
|
|
74
|
-
expect(result.structuredContent).to.deep.equal({ count: 3, ids: ['a'] });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('toRegistrationConfig() omits outputSchema when not declared', () => {
|
|
78
|
-
const config = new WithoutOutputSchemaHandler().toRegistrationConfig();
|
|
79
|
-
expect(config.outputSchema).to.equal(undefined);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('toRegistrationConfig() forwards outputSchema when declared', () => {
|
|
83
|
-
const config = new WithOutputSchemaHandler().toRegistrationConfig();
|
|
84
|
-
expect(config.outputSchema).to.not.equal(undefined);
|
|
85
|
-
// The SDK accepts a full `ZodObject` (or its raw shape); we pass the wrapped object so
|
|
86
|
-
// strict-mode rejection of unknown keys can extend uniformly to outputs.
|
|
87
|
-
expect(Object.keys(config.outputSchema!.shape)).to.deep.equal(['ok']);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// ─── Per-handler schema matrix ────────────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Each entry binds a handler constructor to its declared `OutputSchema` + a representative
|
|
95
|
-
* structured payload. The expectations:
|
|
96
|
-
* - Constructor's `outputSchema` field matches the exported schema (same reference).
|
|
97
|
-
* - The schema accepts the representative payload (zod parse passes).
|
|
98
|
-
*/
|
|
99
|
-
const matrix: Array<{
|
|
100
|
-
name: string;
|
|
101
|
-
Constructor: new () => { outputSchema?: unknown };
|
|
102
|
-
schema: z.ZodObject;
|
|
103
|
-
sample: Record<string, unknown>;
|
|
104
|
-
}> = [
|
|
105
|
-
{
|
|
106
|
-
name: 'create-nodes',
|
|
107
|
-
Constructor: CreateNodesMcpToolHandler,
|
|
108
|
-
schema: CreateNodesOutputSchema,
|
|
109
|
-
sample: { createdNodes: [{ id: 'n1', elementTypeId: 'node:foo' }], dispatchedCommands: 1, errors: [], warnings: [] }
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
name: 'create-edges',
|
|
113
|
-
Constructor: CreateEdgesMcpToolHandler,
|
|
114
|
-
schema: CreateEdgesOutputSchema,
|
|
115
|
-
sample: { createdEdges: [{ id: 'e1', elementTypeId: 'edge' }], dispatchedCommands: 1, errors: [] }
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: 'modify-nodes',
|
|
119
|
-
Constructor: ModifyNodesMcpToolHandler,
|
|
120
|
-
schema: ModifyNodesOutputSchema,
|
|
121
|
-
sample: { modifiedNodes: [{ id: 'n1', elementTypeId: 'node:foo' }], dispatchedCommands: 1, warnings: [] }
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
name: 'modify-edges',
|
|
125
|
-
Constructor: ModifyEdgesMcpToolHandler,
|
|
126
|
-
schema: ModifyEdgesOutputSchema,
|
|
127
|
-
sample: { modifiedEdges: [{ id: 'e1', elementTypeId: 'edge' }], dispatchedCommands: 1, errors: [] }
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
name: 'delete-elements',
|
|
131
|
-
Constructor: DeleteElementsMcpToolHandler,
|
|
132
|
-
schema: DeleteElementsOutputSchema,
|
|
133
|
-
sample: { deletedElements: [{ id: 'n1', elementTypeId: 'node:foo' }], deletedCount: 3, dispatchedCommands: 1 }
|
|
134
|
-
},
|
|
135
|
-
{ name: 'undo', Constructor: UndoMcpToolHandler, schema: UndoOutputSchema, sample: { commandsUndone: 2 } },
|
|
136
|
-
{ name: 'redo', Constructor: RedoMcpToolHandler, schema: RedoOutputSchema, sample: { commandsRedone: 2 } },
|
|
137
|
-
{ name: 'layout', Constructor: LayoutMcpToolHandler, schema: LayoutOutputSchema, sample: { applied: true, dispatchedCommands: 1 } },
|
|
138
|
-
{
|
|
139
|
-
name: 'validate-diagram',
|
|
140
|
-
Constructor: ValidateDiagramMcpToolHandler,
|
|
141
|
-
schema: ValidateDiagramOutputSchema,
|
|
142
|
-
sample: { markers: [{ kind: 'error', label: 'bad', description: 'bad bad', elementId: 'n1' }] }
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
name: 'get-selection',
|
|
146
|
-
Constructor: GetSelectionMcpToolHandler,
|
|
147
|
-
schema: GetSelectionOutputSchema,
|
|
148
|
-
sample: { selectedElementIds: ['n1', 'n2'] }
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
name: 'query-elements',
|
|
152
|
-
Constructor: QueryElementsMcpToolHandler,
|
|
153
|
-
schema: QueryElementsOutputSchema,
|
|
154
|
-
sample: { mode: 'inspect', elements: [{ id: 'n1', type: 'node:foo', position: { x: 0, y: 0 } }] }
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
name: 'save-model',
|
|
158
|
-
Constructor: SaveModelMcpToolHandler,
|
|
159
|
-
schema: SaveModelOutputSchema,
|
|
160
|
-
sample: { saved: true, fileUri: 'file:///a' }
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
name: 'set-view',
|
|
164
|
-
Constructor: SetViewMcpToolHandler,
|
|
165
|
-
schema: SetViewOutputSchema,
|
|
166
|
-
sample: { action: 'fit-to-screen', targetIds: ['n1'] }
|
|
167
|
-
}
|
|
168
|
-
];
|
|
169
|
-
|
|
170
|
-
describe('Tool output schemas · per-handler matrix', () => {
|
|
171
|
-
matrix.forEach(({ name, Constructor, schema, sample }) => {
|
|
172
|
-
describe(name, () => {
|
|
173
|
-
it('declares outputSchema referencing the exported schema constant', () => {
|
|
174
|
-
expect(new Constructor().outputSchema).to.equal(schema);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('schema accepts a representative structured payload', () => {
|
|
178
|
-
expect(() => schema.parse(sample)).to.not.throw();
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
});
|
|
@@ -1,101 +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, GModelElement, Logger, ModelState, NullLogger } from '@eclipse-glsp/server';
|
|
18
|
-
import { expect } from 'chai';
|
|
19
|
-
import { Container, ContainerModule } from 'inversify';
|
|
20
|
-
import { McpToolResult } from '../../server/mcp-handler-shared';
|
|
21
|
-
import { McpIdAliasService } from '../../server/mcp-id-alias-service';
|
|
22
|
-
import { DefaultMcpLabelProvider, McpLabelProvider } from '../../server/mcp-label-provider';
|
|
23
|
-
import { CountElementsMcpToolHandler } from './count-elements-mcp-tool-handler';
|
|
24
|
-
|
|
25
|
-
function makeElement(id: string, type: string): GModelElement {
|
|
26
|
-
return { id, type, children: [] } as unknown as GModelElement;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makeModelState(elements: GModelElement[]): ModelState {
|
|
30
|
-
const byId = new Map(elements.map(el => [el.id, el]));
|
|
31
|
-
return {
|
|
32
|
-
index: {
|
|
33
|
-
allIds: () => [...byId.keys()],
|
|
34
|
-
get: (id: string) => byId.get(id),
|
|
35
|
-
find: (id: string) => byId.get(id)
|
|
36
|
-
}
|
|
37
|
-
} as unknown as ModelState;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildHandler(elements: GModelElement[]): CountElementsMcpToolHandler {
|
|
41
|
-
const container = new Container();
|
|
42
|
-
container.load(
|
|
43
|
-
new ContainerModule(bind => {
|
|
44
|
-
bind(Logger).toConstantValue(new NullLogger());
|
|
45
|
-
bind(ClientId).toConstantValue('test-session');
|
|
46
|
-
bind(ModelState).toConstantValue(makeModelState(elements));
|
|
47
|
-
bind(McpIdAliasService).toConstantValue({
|
|
48
|
-
lookup: (id: string) => id,
|
|
49
|
-
alias: (id: string) => id
|
|
50
|
-
} as McpIdAliasService);
|
|
51
|
-
bind(McpLabelProvider).to(DefaultMcpLabelProvider);
|
|
52
|
-
bind(CountElementsMcpToolHandler).toSelf();
|
|
53
|
-
})
|
|
54
|
-
);
|
|
55
|
-
return container.get(CountElementsMcpToolHandler);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function callCreateResult(handler: CountElementsMcpToolHandler): Promise<McpToolResult> {
|
|
59
|
-
return (handler as unknown as { createResult: (params: { sessionId: string }) => Promise<McpToolResult> }).createResult({
|
|
60
|
-
sessionId: 'test-session'
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe('CountElementsMcpToolHandler', () => {
|
|
65
|
-
it('aggregates element counts grouped by `type`', async () => {
|
|
66
|
-
const handler = buildHandler([
|
|
67
|
-
makeElement('n1', 'task:manual'),
|
|
68
|
-
makeElement('n2', 'task:manual'),
|
|
69
|
-
makeElement('n3', 'task:automated'),
|
|
70
|
-
makeElement('e1', 'edge')
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
const result = await callCreateResult(handler);
|
|
74
|
-
expect(result.structuredContent).to.deep.equal({
|
|
75
|
-
total: 4,
|
|
76
|
-
countsByType: { 'task:manual': 2, 'task:automated': 1, edge: 1 }
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('orders rendered rows by count desc, then type alpha asc on ties', async () => {
|
|
81
|
-
const handler = buildHandler([
|
|
82
|
-
makeElement('e1', 'edge'),
|
|
83
|
-
makeElement('e2', 'edge'),
|
|
84
|
-
makeElement('n1', 'task:automated'),
|
|
85
|
-
makeElement('n2', 'task:manual'),
|
|
86
|
-
makeElement('n3', 'task:manual')
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
const result = await callCreateResult(handler);
|
|
90
|
-
const text = (result.content[0] as { text: string }).text;
|
|
91
|
-
// Counts: edge=2, task:manual=2, task:automated=1.
|
|
92
|
-
// Expected order: edge before task:manual (alpha tiebreak on count=2), then task:automated.
|
|
93
|
-
const expectedOrder = ['edge', 'task:manual', 'task:automated'];
|
|
94
|
-
const indices = expectedOrder.map(type => text.indexOf(`- ${type}: `));
|
|
95
|
-
expect(
|
|
96
|
-
indices.every(i => i >= 0),
|
|
97
|
-
'expected all type rows to be rendered'
|
|
98
|
-
).to.equal(true);
|
|
99
|
-
expect(indices).to.deep.equal([...indices].sort((a, b) => a - b));
|
|
100
|
-
});
|
|
101
|
-
});
|