@eclipse-glsp/server-mcp 2.8.0-next.5 → 2.8.0-next.7
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/package.json +8 -8
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eclipse-glsp/server-mcp",
|
|
3
|
-
"version": "2.8.0-next.
|
|
3
|
+
"version": "2.8.0-next.7",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for the GLSP TypeScript server — runs on Node, browser, and Fetch-API runtimes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eclipse",
|
|
@@ -42,13 +42,16 @@
|
|
|
42
42
|
"browser.js",
|
|
43
43
|
"browser.d.ts",
|
|
44
44
|
"common.js",
|
|
45
|
-
"common.d.ts"
|
|
45
|
+
"common.d.ts",
|
|
46
|
+
"!**/*.spec.ts",
|
|
47
|
+
"!**/*.spec.tsx",
|
|
48
|
+
"!**/test/**"
|
|
46
49
|
],
|
|
47
50
|
"dependencies": {
|
|
48
51
|
"@hono/node-server": "^1.19.9",
|
|
49
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
50
53
|
"zod": "^4.0.0",
|
|
51
|
-
"@eclipse-glsp/server": "2.8.0-next.
|
|
54
|
+
"@eclipse-glsp/server": "2.8.0-next.7"
|
|
52
55
|
},
|
|
53
56
|
"peerDependencies": {
|
|
54
57
|
"inversify": "^6.1.3",
|
|
@@ -59,11 +62,8 @@
|
|
|
59
62
|
},
|
|
60
63
|
"scripts": {
|
|
61
64
|
"build": "tsc -b",
|
|
62
|
-
"clean": "rimraf lib *.tsbuildinfo coverage
|
|
65
|
+
"clean": "rimraf lib *.tsbuildinfo coverage",
|
|
63
66
|
"generate:index": "glsp generateIndex src/browser src/common src/node -f -s",
|
|
64
|
-
"lint": "eslint --ext .ts,.tsx ./src"
|
|
65
|
-
"test": "mocha --config ../../.mocharc \"./src/**/*.spec.?(ts|tsx)\"",
|
|
66
|
-
"test:ci": "pnpm test --reporter mocha-ctrf-json-reporter",
|
|
67
|
-
"test:coverage": "nyc pnpm test"
|
|
67
|
+
"lint": "eslint --ext .ts,.tsx ./src"
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -1,458 +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
|
-
ClientSessionManager,
|
|
19
|
-
InitializeParameters,
|
|
20
|
-
InitializeResult,
|
|
21
|
-
Logger,
|
|
22
|
-
NullLogger,
|
|
23
|
-
McpInitializeResult
|
|
24
|
-
} from '@eclipse-glsp/server';
|
|
25
|
-
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
26
|
-
import { expect } from 'chai';
|
|
27
|
-
import { Container, ContainerModule } from 'inversify';
|
|
28
|
-
import * as z from 'zod/v4';
|
|
29
|
-
import { AbstractMcpServerLauncher, FullMcpServerConfiguration, TransportEndpoint } from './abstract-mcp-server-launcher';
|
|
30
|
-
import { DefaultGLSPMcpServer, GLSPMcpServerFactory } from './glsp-mcp-server';
|
|
31
|
-
import { McpDiagramHandlerDispatcher } from './mcp-diagram-handler-dispatcher';
|
|
32
|
-
import { DefaultMcpLogLevelRegistry, McpLogLevelRegistry } from './mcp-log-level-registry';
|
|
33
|
-
import { McpServerDefaults, McpServerOptions } from './mcp-options';
|
|
34
|
-
|
|
35
|
-
class StubDispatcher implements McpDiagramHandlerDispatcher {
|
|
36
|
-
harvest(): void {
|
|
37
|
-
/* no-op */
|
|
38
|
-
}
|
|
39
|
-
reset(): void {
|
|
40
|
-
/* no-op */
|
|
41
|
-
}
|
|
42
|
-
hasDiagramTools(): boolean {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
hasDiagramResources(): boolean {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
hasDiagramPrompts(): boolean {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
registerAll(): void {
|
|
52
|
-
/* no-op */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
class StubClientSessionManager {
|
|
57
|
-
addListener(): boolean {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
removeListener(): boolean {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
getSessions(): unknown[] {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
getSession(): unknown {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
getSessionsByType(): unknown[] {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
disposeClientSession(): void {
|
|
73
|
-
/* no-op */
|
|
74
|
-
}
|
|
75
|
-
addClientSession(): boolean {
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface RegisteredAuthCapture {
|
|
81
|
-
authInfo?: AuthInfo;
|
|
82
|
-
invoked: boolean;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Test subclass: `bindTransport` is a no-op so the test drives `handleRequest` directly against
|
|
87
|
-
* synthetic `Request` objects, asserting the routed `Response`.
|
|
88
|
-
*/
|
|
89
|
-
class TestNodeMcpServerLauncher extends AbstractMcpServerLauncher {
|
|
90
|
-
readonly authCapture: RegisteredAuthCapture = { invoked: false };
|
|
91
|
-
|
|
92
|
-
protected async bindTransport(_config: FullMcpServerConfiguration): Promise<TransportEndpoint> {
|
|
93
|
-
return {};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Register an `echo` tool on each new GLSP MCP server so we can drive a tool/list + tools/call
|
|
98
|
-
* round-trip and capture the `extra.authInfo` argument forwarded by the SDK.
|
|
99
|
-
*/
|
|
100
|
-
protected override createGlspMcpServer(config: FullMcpServerConfiguration): DefaultGLSPMcpServer {
|
|
101
|
-
const server = super.createGlspMcpServer(config) as DefaultGLSPMcpServer;
|
|
102
|
-
const capture = this.authCapture;
|
|
103
|
-
server.registerTool(
|
|
104
|
-
'echo',
|
|
105
|
-
{ description: 'Echoes back the supplied message.', inputSchema: { message: z.string() } },
|
|
106
|
-
async ({ message }, extra) => {
|
|
107
|
-
capture.invoked = true;
|
|
108
|
-
capture.authInfo = extra?.authInfo;
|
|
109
|
-
return { content: [{ type: 'text', text: message }] };
|
|
110
|
-
}
|
|
111
|
-
);
|
|
112
|
-
return server;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function buildLauncher(): TestNodeMcpServerLauncher {
|
|
117
|
-
const container = new Container();
|
|
118
|
-
container.load(
|
|
119
|
-
new ContainerModule(bind => {
|
|
120
|
-
bind(Logger).toConstantValue(new NullLogger());
|
|
121
|
-
bind(McpServerOptions).toSelf().inSingletonScope();
|
|
122
|
-
bind(McpServerDefaults).toConstantValue({});
|
|
123
|
-
bind(McpDiagramHandlerDispatcher).toConstantValue(new StubDispatcher());
|
|
124
|
-
bind(McpLogLevelRegistry).to(DefaultMcpLogLevelRegistry).inSingletonScope();
|
|
125
|
-
bind(ClientSessionManager).toConstantValue(new StubClientSessionManager());
|
|
126
|
-
const factory: GLSPMcpServerFactory = (mcpServer, options) => new DefaultGLSPMcpServer(mcpServer, options, new NullLogger());
|
|
127
|
-
bind(GLSPMcpServerFactory).toConstantValue(factory);
|
|
128
|
-
bind(TestNodeMcpServerLauncher).toSelf().inSingletonScope();
|
|
129
|
-
})
|
|
130
|
-
);
|
|
131
|
-
return container.get(TestNodeMcpServerLauncher);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const INIT_BODY = {
|
|
135
|
-
jsonrpc: '2.0',
|
|
136
|
-
id: 1,
|
|
137
|
-
method: 'initialize',
|
|
138
|
-
params: {
|
|
139
|
-
protocolVersion: '2025-06-18',
|
|
140
|
-
capabilities: {},
|
|
141
|
-
clientInfo: { name: 'spec-client', version: '0.0.1' }
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
async function initLauncher(
|
|
146
|
-
launcher: TestNodeMcpServerLauncher,
|
|
147
|
-
overrides: Partial<{ route: string; port: number; host: string; name: string; options: Record<string, unknown> }> = {}
|
|
148
|
-
): Promise<InitializeResult> {
|
|
149
|
-
const params: InitializeParameters = {
|
|
150
|
-
applicationId: 'spec-app',
|
|
151
|
-
clientSessionId: 'spec-session',
|
|
152
|
-
protocolVersion: '1.0.0',
|
|
153
|
-
args: {},
|
|
154
|
-
mcpServer: {
|
|
155
|
-
port: overrides.port ?? 0,
|
|
156
|
-
host: overrides.host ?? '127.0.0.1',
|
|
157
|
-
route: overrides.route ?? '/mcp',
|
|
158
|
-
name: overrides.name ?? 'spec',
|
|
159
|
-
options: overrides.options ?? {}
|
|
160
|
-
}
|
|
161
|
-
} as unknown as InitializeParameters;
|
|
162
|
-
const baseResult: InitializeResult = { protocolVersion: '1.0.0', serverActions: {} } as unknown as InitializeResult;
|
|
163
|
-
return launcher.initializeServer({} as never, params, baseResult);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Read an SSE stream into a list of `data:` payloads. The Web-standard transport streams JSON-RPC
|
|
168
|
-
* responses over `text/event-stream`; we only need to find the first complete event for assertions.
|
|
169
|
-
*/
|
|
170
|
-
async function readSseFirstData(response: Response, timeoutMs = 2000): Promise<unknown> {
|
|
171
|
-
const reader = response.body?.getReader();
|
|
172
|
-
if (!reader) {
|
|
173
|
-
throw new Error('Response has no body');
|
|
174
|
-
}
|
|
175
|
-
const decoder = new TextDecoder();
|
|
176
|
-
let buffer = '';
|
|
177
|
-
const deadline = Date.now() + timeoutMs;
|
|
178
|
-
while (Date.now() < deadline) {
|
|
179
|
-
const { value, done } = await reader.read();
|
|
180
|
-
if (done) break;
|
|
181
|
-
buffer += decoder.decode(value, { stream: true });
|
|
182
|
-
for (const block of buffer.split('\n\n')) {
|
|
183
|
-
const dataLine = block.split('\n').find(line => line.startsWith('data: '));
|
|
184
|
-
if (dataLine) {
|
|
185
|
-
reader.cancel().catch(() => undefined);
|
|
186
|
-
return JSON.parse(dataLine.slice('data: '.length));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
reader.cancel().catch(() => undefined);
|
|
191
|
-
throw new Error('Timed out waiting for SSE data event');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function postInit(): Request {
|
|
195
|
-
return new Request('http://test/mcp', {
|
|
196
|
-
method: 'POST',
|
|
197
|
-
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
|
|
198
|
-
body: JSON.stringify(INIT_BODY)
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function postJson(sessionId: string | undefined, body: unknown): Request {
|
|
203
|
-
const headers: Record<string, string> = {
|
|
204
|
-
'content-type': 'application/json',
|
|
205
|
-
accept: 'application/json, text/event-stream'
|
|
206
|
-
};
|
|
207
|
-
if (sessionId !== undefined) headers['mcp-session-id'] = sessionId;
|
|
208
|
-
return new Request('http://test/mcp', { method: 'POST', headers, body: JSON.stringify(body) });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
describe('AbstractMcpServerLauncher.handleRequest', () => {
|
|
212
|
-
let launcher: TestNodeMcpServerLauncher;
|
|
213
|
-
|
|
214
|
-
beforeEach(async () => {
|
|
215
|
-
launcher = buildLauncher();
|
|
216
|
-
await initLauncher(launcher);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
afterEach(() => {
|
|
220
|
-
launcher.dispose();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('POST initialize creates a session and returns 200 with mcp-session-id header', async () => {
|
|
224
|
-
const response = await launcher.handleRequest(postInit());
|
|
225
|
-
expect(response.status).to.equal(200);
|
|
226
|
-
expect(response.headers.get('mcp-session-id'), 'must echo a session id after initialize').to.be.a('string');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('POST to a known session dispatches to its transport (tools/call round-trip)', async () => {
|
|
230
|
-
const initResponse = await launcher.handleRequest(postInit());
|
|
231
|
-
const sessionId = initResponse.headers.get('mcp-session-id')!;
|
|
232
|
-
// Read+discard the init SSE response so the transport is ready for the next POST.
|
|
233
|
-
await readSseFirstData(initResponse).catch(() => undefined);
|
|
234
|
-
|
|
235
|
-
// Per MCP spec, send `notifications/initialized` after the initialize handshake.
|
|
236
|
-
await launcher.handleRequest(postJson(sessionId, { jsonrpc: '2.0', method: 'notifications/initialized', params: {} }));
|
|
237
|
-
|
|
238
|
-
const callResponse = await launcher.handleRequest(
|
|
239
|
-
postJson(sessionId, {
|
|
240
|
-
jsonrpc: '2.0',
|
|
241
|
-
id: 2,
|
|
242
|
-
method: 'tools/call',
|
|
243
|
-
params: { name: 'echo', arguments: { message: 'hello' } }
|
|
244
|
-
})
|
|
245
|
-
);
|
|
246
|
-
expect(callResponse.status).to.equal(200);
|
|
247
|
-
const payload = (await readSseFirstData(callResponse)) as { result?: { content?: Array<{ text?: string }> } };
|
|
248
|
-
expect(payload.result?.content?.[0]?.text).to.equal('hello');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it('POST without session id, non-initialize body → 400 with JSON-RPC error envelope', async () => {
|
|
252
|
-
const response = await launcher.handleRequest(postJson(undefined, { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }));
|
|
253
|
-
expect(response.status).to.equal(400);
|
|
254
|
-
const payload = (await response.json()) as { jsonrpc: string; error: { code: number; message: string }; id: unknown };
|
|
255
|
-
expect(payload.jsonrpc).to.equal('2.0');
|
|
256
|
-
// eslint-disable-next-line no-null/no-null
|
|
257
|
-
expect(payload.id).to.equal(null);
|
|
258
|
-
expect(payload.error.code).to.equal(-32000);
|
|
259
|
-
expect(payload.error.message).to.match(/No valid session ID/);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('POST with unknown session id → 404 with JSON-RPC error envelope', async () => {
|
|
263
|
-
const response = await launcher.handleRequest(
|
|
264
|
-
postJson('not-a-real-session', { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} })
|
|
265
|
-
);
|
|
266
|
-
expect(response.status).to.equal(404);
|
|
267
|
-
const payload = (await response.json()) as { error: { code: number } };
|
|
268
|
-
expect(payload.error.code).to.equal(-32001);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('GET with accept: text/event-stream on a known session → 200, text/event-stream', async () => {
|
|
272
|
-
const initResponse = await launcher.handleRequest(postInit());
|
|
273
|
-
const sessionId = initResponse.headers.get('mcp-session-id')!;
|
|
274
|
-
await readSseFirstData(initResponse).catch(() => undefined);
|
|
275
|
-
|
|
276
|
-
const getResponse = await launcher.handleRequest(
|
|
277
|
-
new Request('http://test/mcp', {
|
|
278
|
-
method: 'GET',
|
|
279
|
-
headers: { accept: 'text/event-stream', 'mcp-session-id': sessionId }
|
|
280
|
-
})
|
|
281
|
-
);
|
|
282
|
-
expect(getResponse.status).to.equal(200);
|
|
283
|
-
expect(getResponse.headers.get('content-type')).to.match(/text\/event-stream/);
|
|
284
|
-
getResponse.body?.cancel().catch(() => undefined);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('GET on unknown session → 404', async () => {
|
|
288
|
-
const response = await launcher.handleRequest(
|
|
289
|
-
new Request('http://test/mcp', {
|
|
290
|
-
method: 'GET',
|
|
291
|
-
headers: { accept: 'text/event-stream', 'mcp-session-id': 'nope' }
|
|
292
|
-
})
|
|
293
|
-
);
|
|
294
|
-
expect(response.status).to.equal(404);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it('DELETE on known session fires onSessionClosed; subsequent POSTs return 404', async () => {
|
|
298
|
-
const closedIds: string[] = [];
|
|
299
|
-
launcher.onSessionClosed(sessionId => closedIds.push(sessionId));
|
|
300
|
-
|
|
301
|
-
const initResponse = await launcher.handleRequest(postInit());
|
|
302
|
-
const sessionId = initResponse.headers.get('mcp-session-id')!;
|
|
303
|
-
await readSseFirstData(initResponse).catch(() => undefined);
|
|
304
|
-
|
|
305
|
-
const deleteResponse = await launcher.handleRequest(
|
|
306
|
-
new Request('http://test/mcp', { method: 'DELETE', headers: { 'mcp-session-id': sessionId } })
|
|
307
|
-
);
|
|
308
|
-
expect(deleteResponse.status).to.be.lessThan(300);
|
|
309
|
-
expect(closedIds).to.include(sessionId);
|
|
310
|
-
|
|
311
|
-
const followUp = await launcher.handleRequest(postJson(sessionId, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }));
|
|
312
|
-
expect(followUp.status).to.equal(404);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('Non-init POST with unsupported MCP-Protocol-Version → 400 with JSON-RPC error envelope', async () => {
|
|
316
|
-
const response = await launcher.handleRequest(
|
|
317
|
-
new Request('http://test/mcp', {
|
|
318
|
-
method: 'POST',
|
|
319
|
-
headers: {
|
|
320
|
-
'content-type': 'application/json',
|
|
321
|
-
'mcp-session-id': 'anything',
|
|
322
|
-
'mcp-protocol-version': '1999-01-01'
|
|
323
|
-
},
|
|
324
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} })
|
|
325
|
-
})
|
|
326
|
-
);
|
|
327
|
-
expect(response.status).to.equal(400);
|
|
328
|
-
const payload = (await response.json()) as { error: { code: number; message: string } };
|
|
329
|
-
expect(payload.error.code).to.equal(-32000);
|
|
330
|
-
expect(payload.error.message).to.match(/Unsupported MCP-Protocol-Version/);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('POST with no MCP-Protocol-Version header passes the version gate (falls through to session-id check)', async () => {
|
|
334
|
-
// Spec: server defaults to `2025-03-26` when the header is absent. We assert by
|
|
335
|
-
// sending no session id and observing the 400 "No valid session ID" — proving the
|
|
336
|
-
// version gate didn't short-circuit.
|
|
337
|
-
const response = await launcher.handleRequest(
|
|
338
|
-
new Request('http://test/mcp', {
|
|
339
|
-
method: 'POST',
|
|
340
|
-
headers: { 'content-type': 'application/json' },
|
|
341
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} })
|
|
342
|
-
})
|
|
343
|
-
);
|
|
344
|
-
expect(response.status).to.equal(400);
|
|
345
|
-
const payload = (await response.json()) as { error: { message: string } };
|
|
346
|
-
expect(payload.error.message).to.match(/No valid session ID/);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('authInfo from handleRequest reaches the registered tool handler extra', async () => {
|
|
350
|
-
const initResponse = await launcher.handleRequest(postInit());
|
|
351
|
-
const sessionId = initResponse.headers.get('mcp-session-id')!;
|
|
352
|
-
await readSseFirstData(initResponse).catch(() => undefined);
|
|
353
|
-
|
|
354
|
-
await launcher.handleRequest(postJson(sessionId, { jsonrpc: '2.0', method: 'notifications/initialized', params: {} }));
|
|
355
|
-
|
|
356
|
-
const authInfo: AuthInfo = { token: 'bearer-xyz', clientId: 'spec-client', scopes: ['mcp:tools'] };
|
|
357
|
-
const callResponse = await launcher.handleRequest(
|
|
358
|
-
postJson(sessionId, {
|
|
359
|
-
jsonrpc: '2.0',
|
|
360
|
-
id: 3,
|
|
361
|
-
method: 'tools/call',
|
|
362
|
-
params: { name: 'echo', arguments: { message: 'auth-check' } }
|
|
363
|
-
}),
|
|
364
|
-
{ authInfo }
|
|
365
|
-
);
|
|
366
|
-
await readSseFirstData(callResponse).catch(() => undefined);
|
|
367
|
-
|
|
368
|
-
expect(launcher.authCapture.invoked).to.equal(true);
|
|
369
|
-
expect(launcher.authCapture.authInfo?.token).to.equal('bearer-xyz');
|
|
370
|
-
expect(launcher.authCapture.authInfo?.clientId).to.equal('spec-client');
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it('onSessionInitialized fires on initialize', async () => {
|
|
374
|
-
const sessionIds: string[] = [];
|
|
375
|
-
launcher.onSessionInitialized(session => sessionIds.push(session.sessionId));
|
|
376
|
-
|
|
377
|
-
const response = await launcher.handleRequest(postInit());
|
|
378
|
-
const sessionId = response.headers.get('mcp-session-id')!;
|
|
379
|
-
expect(sessionIds).to.include(sessionId);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('Route mismatch (POST /other when launcher route is /mcp) → 404', async () => {
|
|
383
|
-
const response = await launcher.handleRequest(
|
|
384
|
-
new Request('http://test/other', { method: 'POST', body: '{}', headers: { 'content-type': 'application/json' } })
|
|
385
|
-
);
|
|
386
|
-
expect(response.status).to.equal(404);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('Unsupported method (PUT /mcp) → 405 with Allow header', async () => {
|
|
390
|
-
const response = await launcher.handleRequest(new Request('http://test/mcp', { method: 'PUT' }));
|
|
391
|
-
expect(response.status).to.equal(405);
|
|
392
|
-
expect(response.headers.get('Allow')).to.equal('POST, GET, DELETE');
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
describe('AbstractMcpServerLauncher · initializeServer lifecycle', () => {
|
|
397
|
-
it('Idempotent initializeServer: second call reuses existing config without re-binding', async () => {
|
|
398
|
-
const launcher = buildLauncher();
|
|
399
|
-
let bindCount = 0;
|
|
400
|
-
const original = (launcher as unknown as { bindTransport: () => Promise<TransportEndpoint> }).bindTransport.bind(launcher);
|
|
401
|
-
(launcher as unknown as { bindTransport: () => Promise<TransportEndpoint> }).bindTransport = async () => {
|
|
402
|
-
bindCount += 1;
|
|
403
|
-
return original();
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
await initLauncher(launcher);
|
|
407
|
-
await initLauncher(launcher);
|
|
408
|
-
expect(bindCount).to.equal(1);
|
|
409
|
-
launcher.dispose();
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it('dispose() closes sessions and resets state; a subsequent initializeServer boots cleanly', async () => {
|
|
413
|
-
const launcher = buildLauncher();
|
|
414
|
-
await initLauncher(launcher);
|
|
415
|
-
const firstResponse = await launcher.handleRequest(postInit());
|
|
416
|
-
expect(firstResponse.headers.get('mcp-session-id')).to.be.a('string');
|
|
417
|
-
await readSseFirstData(firstResponse).catch(() => undefined);
|
|
418
|
-
|
|
419
|
-
launcher.dispose();
|
|
420
|
-
|
|
421
|
-
// Re-init from the same instance — proves dispose() cleared the singleton state.
|
|
422
|
-
await initLauncher(launcher, { route: '/mcp', name: 'spec-2' });
|
|
423
|
-
const secondResponse = await launcher.handleRequest(postInit());
|
|
424
|
-
expect(secondResponse.status).to.equal(200);
|
|
425
|
-
expect(secondResponse.headers.get('mcp-session-id')).to.be.a('string');
|
|
426
|
-
launcher.dispose();
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
it('honors the custom `route` from initialize params', async () => {
|
|
430
|
-
const launcher = buildLauncher();
|
|
431
|
-
await initLauncher(launcher, { route: '/custom' });
|
|
432
|
-
|
|
433
|
-
const wrongRoute = await launcher.handleRequest(postInit());
|
|
434
|
-
expect(wrongRoute.status).to.equal(404);
|
|
435
|
-
|
|
436
|
-
const correctRoute = await launcher.handleRequest(
|
|
437
|
-
new Request('http://test/custom', {
|
|
438
|
-
method: 'POST',
|
|
439
|
-
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
|
|
440
|
-
body: JSON.stringify(INIT_BODY)
|
|
441
|
-
})
|
|
442
|
-
);
|
|
443
|
-
expect(correctRoute.status).to.equal(200);
|
|
444
|
-
launcher.dispose();
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it('attaches mcpServer.url to the returned InitializeResult when bindTransport reports a URL', async () => {
|
|
448
|
-
const launcher = buildLauncher();
|
|
449
|
-
// Override the test subclass's `bindTransport` to report a synthetic URL.
|
|
450
|
-
(launcher as unknown as { bindTransport: () => Promise<TransportEndpoint> }).bindTransport = async () => ({
|
|
451
|
-
url: 'http://announced.example/mcp'
|
|
452
|
-
});
|
|
453
|
-
const result = await initLauncher(launcher);
|
|
454
|
-
expect(McpInitializeResult.is(result)).to.equal(true);
|
|
455
|
-
expect(McpInitializeResult.getServer(result)?.url).to.equal('http://announced.example/mcp');
|
|
456
|
-
launcher.dispose();
|
|
457
|
-
});
|
|
458
|
-
});
|
|
@@ -1,73 +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 { NullLogger } from '@eclipse-glsp/server';
|
|
18
|
-
import { McpServer, RegisteredResource } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
19
|
-
import { expect } from 'chai';
|
|
20
|
-
import * as sinon from 'sinon';
|
|
21
|
-
import * as z from 'zod/v4';
|
|
22
|
-
import { DefaultGLSPMcpServer } from './glsp-mcp-server';
|
|
23
|
-
|
|
24
|
-
describe('DefaultGLSPMcpServer', () => {
|
|
25
|
-
function makeServer(): { wrapper: DefaultGLSPMcpServer; sdk: McpServer } {
|
|
26
|
-
const sdk = new McpServer({ name: 'test', version: '1.0.0' }, { capabilities: {} });
|
|
27
|
-
const wrapper = new DefaultGLSPMcpServer(sdk, { dataMode: 'tools' }, new NullLogger());
|
|
28
|
-
return { wrapper, sdk };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
it('tracks every registerTool call so listTools returns the registered set', () => {
|
|
32
|
-
const { wrapper } = makeServer();
|
|
33
|
-
wrapper.registerTool('alpha', { description: 'first', inputSchema: { x: z.string() } }, async () => ({ content: [] }));
|
|
34
|
-
wrapper.registerTool('beta', { description: 'second' }, async () => ({ content: [] }));
|
|
35
|
-
|
|
36
|
-
const names = wrapper.listTools().map(tool => tool.description);
|
|
37
|
-
expect(names).to.have.members(['first', 'second']);
|
|
38
|
-
expect(wrapper.listTools()).to.have.lengthOf(2);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('tracks registerResource (static URI) and listResources reflects it by name', () => {
|
|
42
|
-
const { wrapper } = makeServer();
|
|
43
|
-
wrapper.registerResource('my-resource', 'glsp://test', { title: 'res', mimeType: 'text/plain' }, async () => ({
|
|
44
|
-
contents: [{ uri: 'glsp://test', text: 'ok' }]
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
const resources = wrapper.listResources();
|
|
48
|
-
expect(resources).to.have.lengthOf(1);
|
|
49
|
-
expect((resources[0] as RegisteredResource).name).to.equal('my-resource');
|
|
50
|
-
expect((resources[0] as RegisteredResource).title).to.equal('res');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('tracks registerPrompt and listPrompts reflects it by description', () => {
|
|
54
|
-
const { wrapper } = makeServer();
|
|
55
|
-
wrapper.registerPrompt('describe', { description: 'desc' }, async () => ({ messages: [] }));
|
|
56
|
-
|
|
57
|
-
const prompts = wrapper.listPrompts();
|
|
58
|
-
expect(prompts).to.have.lengthOf(1);
|
|
59
|
-
expect(prompts[0].description).to.equal('desc');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('getRawServer() returns the exact SDK instance passed in (escape hatch identity)', () => {
|
|
63
|
-
const { wrapper, sdk } = makeServer();
|
|
64
|
-
expect(wrapper.getRawServer()).to.equal(sdk);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('dispose() invokes close() on the underlying SDK server', () => {
|
|
68
|
-
const { wrapper, sdk } = makeServer();
|
|
69
|
-
const closeSpy = sinon.spy(sdk, 'close');
|
|
70
|
-
wrapper.dispose();
|
|
71
|
-
expect(closeSpy.calledOnce).to.be.true;
|
|
72
|
-
});
|
|
73
|
-
});
|