@modelcontextprotocol/server-everything 2026.1.14 → 2026.7.4
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 +35 -1
- package/dist/__tests__/prompts.test.js +131 -0
- package/dist/__tests__/registrations.test.js +121 -0
- package/dist/__tests__/resources.test.js +240 -0
- package/dist/__tests__/server.test.js +31 -0
- package/dist/__tests__/tools.test.js +904 -0
- package/dist/docs/features.md +55 -2
- package/dist/docs/structure.md +18 -6
- package/dist/resources/session.js +15 -1
- package/dist/server/index.js +21 -1
- package/dist/server/roots.js +1 -5
- package/dist/tools/echo.js +6 -0
- package/dist/tools/get-annotated-message.js +6 -0
- package/dist/tools/get-env.js +6 -0
- package/dist/tools/get-resource-links.js +6 -0
- package/dist/tools/get-resource-reference.js +6 -0
- package/dist/tools/get-roots-list.js +6 -0
- package/dist/tools/get-structured-content.js +6 -0
- package/dist/tools/get-sum.js +6 -0
- package/dist/tools/get-tiny-image.js +6 -0
- package/dist/tools/gzip-file-as-resource.js +6 -1
- package/dist/tools/index.js +10 -0
- package/dist/tools/simulate-research-query.js +248 -0
- package/dist/tools/toggle-simulated-logging.js +6 -0
- package/dist/tools/toggle-subscriber-updates.js +6 -0
- package/dist/tools/trigger-elicitation-request-async.js +206 -0
- package/dist/tools/trigger-elicitation-request.js +7 -1
- package/dist/tools/trigger-long-running-operation.js +6 -0
- package/dist/tools/trigger-sampling-request-async.js +172 -0
- package/dist/tools/trigger-sampling-request.js +6 -0
- package/dist/tools/trigger-url-elicitation.js +169 -0
- package/dist/transports/streamableHttp.js +23 -2
- package/dist/vitest.config.js +13 -0
- package/package.json +10 -8
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { registerEchoTool, EchoSchema } from '../tools/echo.js';
|
|
3
|
+
import { registerGetSumTool } from '../tools/get-sum.js';
|
|
4
|
+
import { registerGetEnvTool } from '../tools/get-env.js';
|
|
5
|
+
import { registerGetTinyImageTool, MCP_TINY_IMAGE } from '../tools/get-tiny-image.js';
|
|
6
|
+
import { registerGetStructuredContentTool } from '../tools/get-structured-content.js';
|
|
7
|
+
import { registerGetAnnotatedMessageTool } from '../tools/get-annotated-message.js';
|
|
8
|
+
import { registerTriggerLongRunningOperationTool } from '../tools/trigger-long-running-operation.js';
|
|
9
|
+
import { registerGetResourceLinksTool } from '../tools/get-resource-links.js';
|
|
10
|
+
import { registerGetResourceReferenceTool } from '../tools/get-resource-reference.js';
|
|
11
|
+
import { registerToggleSimulatedLoggingTool } from '../tools/toggle-simulated-logging.js';
|
|
12
|
+
import { registerToggleSubscriberUpdatesTool } from '../tools/toggle-subscriber-updates.js';
|
|
13
|
+
import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js';
|
|
14
|
+
import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js';
|
|
15
|
+
import { registerTriggerUrlElicitationTool, __resetIssuedErrorPathElicitations, } from '../tools/trigger-url-elicitation.js';
|
|
16
|
+
import { registerGetRootsListTool } from '../tools/get-roots-list.js';
|
|
17
|
+
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';
|
|
18
|
+
import { registerSimulateResearchQueryTool } from '../tools/simulate-research-query.js';
|
|
19
|
+
// Helper to capture registered tool handlers
|
|
20
|
+
function createMockServer() {
|
|
21
|
+
const handlers = new Map();
|
|
22
|
+
const configs = new Map();
|
|
23
|
+
const mockServer = {
|
|
24
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
25
|
+
handlers.set(name, handler);
|
|
26
|
+
configs.set(name, config);
|
|
27
|
+
}),
|
|
28
|
+
server: {
|
|
29
|
+
getClientCapabilities: vi.fn(() => ({})),
|
|
30
|
+
notification: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
sendLoggingMessage: vi.fn(),
|
|
33
|
+
sendResourceUpdated: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
return { mockServer, handlers, configs };
|
|
36
|
+
}
|
|
37
|
+
describe('Tools', () => {
|
|
38
|
+
describe('echo', () => {
|
|
39
|
+
it('should echo back the message', async () => {
|
|
40
|
+
const { mockServer, handlers } = createMockServer();
|
|
41
|
+
registerEchoTool(mockServer);
|
|
42
|
+
const handler = handlers.get('echo');
|
|
43
|
+
const result = await handler({ message: 'Hello, World!' });
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
content: [{ type: 'text', text: 'Echo: Hello, World!' }],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('should handle empty message', async () => {
|
|
49
|
+
const { mockServer, handlers } = createMockServer();
|
|
50
|
+
registerEchoTool(mockServer);
|
|
51
|
+
const handler = handlers.get('echo');
|
|
52
|
+
const result = await handler({ message: '' });
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
content: [{ type: 'text', text: 'Echo: ' }],
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
it('should reject invalid input', async () => {
|
|
58
|
+
const { mockServer, handlers } = createMockServer();
|
|
59
|
+
registerEchoTool(mockServer);
|
|
60
|
+
const handler = handlers.get('echo');
|
|
61
|
+
await expect(handler({})).rejects.toThrow();
|
|
62
|
+
await expect(handler({ message: 123 })).rejects.toThrow();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('EchoSchema', () => {
|
|
66
|
+
it('should validate correct input', () => {
|
|
67
|
+
const result = EchoSchema.parse({ message: 'test' });
|
|
68
|
+
expect(result).toEqual({ message: 'test' });
|
|
69
|
+
});
|
|
70
|
+
it('should reject missing message', () => {
|
|
71
|
+
expect(() => EchoSchema.parse({})).toThrow();
|
|
72
|
+
});
|
|
73
|
+
it('should reject non-string message', () => {
|
|
74
|
+
expect(() => EchoSchema.parse({ message: 123 })).toThrow();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('get-sum', () => {
|
|
78
|
+
it('should calculate sum of two positive numbers', async () => {
|
|
79
|
+
const { mockServer, handlers } = createMockServer();
|
|
80
|
+
registerGetSumTool(mockServer);
|
|
81
|
+
const handler = handlers.get('get-sum');
|
|
82
|
+
const result = await handler({ a: 5, b: 3 });
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
content: [{ type: 'text', text: 'The sum of 5 and 3 is 8.' }],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it('should calculate sum with negative numbers', async () => {
|
|
88
|
+
const { mockServer, handlers } = createMockServer();
|
|
89
|
+
registerGetSumTool(mockServer);
|
|
90
|
+
const handler = handlers.get('get-sum');
|
|
91
|
+
const result = await handler({ a: -5, b: 3 });
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
content: [{ type: 'text', text: 'The sum of -5 and 3 is -2.' }],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('should calculate sum with zero', async () => {
|
|
97
|
+
const { mockServer, handlers } = createMockServer();
|
|
98
|
+
registerGetSumTool(mockServer);
|
|
99
|
+
const handler = handlers.get('get-sum');
|
|
100
|
+
const result = await handler({ a: 0, b: 0 });
|
|
101
|
+
expect(result).toEqual({
|
|
102
|
+
content: [{ type: 'text', text: 'The sum of 0 and 0 is 0.' }],
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('should handle floating point numbers', async () => {
|
|
106
|
+
const { mockServer, handlers } = createMockServer();
|
|
107
|
+
registerGetSumTool(mockServer);
|
|
108
|
+
const handler = handlers.get('get-sum');
|
|
109
|
+
const result = await handler({ a: 1.5, b: 2.5 });
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
content: [{ type: 'text', text: 'The sum of 1.5 and 2.5 is 4.' }],
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it('should reject invalid input', async () => {
|
|
115
|
+
const { mockServer, handlers } = createMockServer();
|
|
116
|
+
registerGetSumTool(mockServer);
|
|
117
|
+
const handler = handlers.get('get-sum');
|
|
118
|
+
await expect(handler({})).rejects.toThrow();
|
|
119
|
+
await expect(handler({ a: 'not a number', b: 5 })).rejects.toThrow();
|
|
120
|
+
await expect(handler({ a: 5 })).rejects.toThrow();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('get-env', () => {
|
|
124
|
+
it('should return all environment variables as JSON', async () => {
|
|
125
|
+
const { mockServer, handlers } = createMockServer();
|
|
126
|
+
registerGetEnvTool(mockServer);
|
|
127
|
+
const handler = handlers.get('get-env');
|
|
128
|
+
process.env.TEST_VAR_EVERYTHING = 'test_value';
|
|
129
|
+
const result = await handler({});
|
|
130
|
+
expect(result.content).toHaveLength(1);
|
|
131
|
+
expect(result.content[0].type).toBe('text');
|
|
132
|
+
const envJson = JSON.parse(result.content[0].text);
|
|
133
|
+
expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value');
|
|
134
|
+
delete process.env.TEST_VAR_EVERYTHING;
|
|
135
|
+
});
|
|
136
|
+
it('should return valid JSON', async () => {
|
|
137
|
+
const { mockServer, handlers } = createMockServer();
|
|
138
|
+
registerGetEnvTool(mockServer);
|
|
139
|
+
const handler = handlers.get('get-env');
|
|
140
|
+
const result = await handler({});
|
|
141
|
+
expect(() => JSON.parse(result.content[0].text)).not.toThrow();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('get-tiny-image', () => {
|
|
145
|
+
it('should return image content with text descriptions', async () => {
|
|
146
|
+
const { mockServer, handlers } = createMockServer();
|
|
147
|
+
registerGetTinyImageTool(mockServer);
|
|
148
|
+
const handler = handlers.get('get-tiny-image');
|
|
149
|
+
const result = await handler({});
|
|
150
|
+
expect(result.content).toHaveLength(3);
|
|
151
|
+
expect(result.content[0]).toEqual({
|
|
152
|
+
type: 'text',
|
|
153
|
+
text: "Here's the image you requested:",
|
|
154
|
+
});
|
|
155
|
+
expect(result.content[1]).toEqual({
|
|
156
|
+
type: 'image',
|
|
157
|
+
data: MCP_TINY_IMAGE,
|
|
158
|
+
mimeType: 'image/png',
|
|
159
|
+
});
|
|
160
|
+
expect(result.content[2]).toEqual({
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: 'The image above is the MCP logo.',
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
it('should return valid base64 image data', async () => {
|
|
166
|
+
const { mockServer, handlers } = createMockServer();
|
|
167
|
+
registerGetTinyImageTool(mockServer);
|
|
168
|
+
const handler = handlers.get('get-tiny-image');
|
|
169
|
+
const result = await handler({});
|
|
170
|
+
const imageContent = result.content[1];
|
|
171
|
+
expect(imageContent.type).toBe('image');
|
|
172
|
+
expect(imageContent.mimeType).toBe('image/png');
|
|
173
|
+
// Verify it's valid base64
|
|
174
|
+
expect(() => Buffer.from(imageContent.data, 'base64')).not.toThrow();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('get-structured-content', () => {
|
|
178
|
+
it('should return weather for New York', async () => {
|
|
179
|
+
const { mockServer, handlers } = createMockServer();
|
|
180
|
+
registerGetStructuredContentTool(mockServer);
|
|
181
|
+
const handler = handlers.get('get-structured-content');
|
|
182
|
+
const result = await handler({ location: 'New York' });
|
|
183
|
+
expect(result.structuredContent).toEqual({
|
|
184
|
+
temperature: 33,
|
|
185
|
+
conditions: 'Cloudy',
|
|
186
|
+
humidity: 82,
|
|
187
|
+
});
|
|
188
|
+
expect(result.content[0].type).toBe('text');
|
|
189
|
+
expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent);
|
|
190
|
+
});
|
|
191
|
+
it('should return weather for Chicago', async () => {
|
|
192
|
+
const { mockServer, handlers } = createMockServer();
|
|
193
|
+
registerGetStructuredContentTool(mockServer);
|
|
194
|
+
const handler = handlers.get('get-structured-content');
|
|
195
|
+
const result = await handler({ location: 'Chicago' });
|
|
196
|
+
expect(result.structuredContent).toEqual({
|
|
197
|
+
temperature: 36,
|
|
198
|
+
conditions: 'Light rain / drizzle',
|
|
199
|
+
humidity: 82,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('should return weather for Los Angeles', async () => {
|
|
203
|
+
const { mockServer, handlers } = createMockServer();
|
|
204
|
+
registerGetStructuredContentTool(mockServer);
|
|
205
|
+
const handler = handlers.get('get-structured-content');
|
|
206
|
+
const result = await handler({ location: 'Los Angeles' });
|
|
207
|
+
expect(result.structuredContent).toEqual({
|
|
208
|
+
temperature: 73,
|
|
209
|
+
conditions: 'Sunny / Clear',
|
|
210
|
+
humidity: 48,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('get-annotated-message', () => {
|
|
215
|
+
it('should return error message with high priority', async () => {
|
|
216
|
+
const { mockServer, handlers } = createMockServer();
|
|
217
|
+
registerGetAnnotatedMessageTool(mockServer);
|
|
218
|
+
const handler = handlers.get('get-annotated-message');
|
|
219
|
+
const result = await handler({ messageType: 'error', includeImage: false });
|
|
220
|
+
expect(result.content).toHaveLength(1);
|
|
221
|
+
expect(result.content[0].text).toBe('Error: Operation failed');
|
|
222
|
+
expect(result.content[0].annotations).toEqual({
|
|
223
|
+
priority: 1.0,
|
|
224
|
+
audience: ['user', 'assistant'],
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
it('should return success message with medium priority', async () => {
|
|
228
|
+
const { mockServer, handlers } = createMockServer();
|
|
229
|
+
registerGetAnnotatedMessageTool(mockServer);
|
|
230
|
+
const handler = handlers.get('get-annotated-message');
|
|
231
|
+
const result = await handler({ messageType: 'success', includeImage: false });
|
|
232
|
+
expect(result.content[0].text).toBe('Operation completed successfully');
|
|
233
|
+
expect(result.content[0].annotations.priority).toBe(0.7);
|
|
234
|
+
expect(result.content[0].annotations.audience).toEqual(['user']);
|
|
235
|
+
});
|
|
236
|
+
it('should return debug message with low priority', async () => {
|
|
237
|
+
const { mockServer, handlers } = createMockServer();
|
|
238
|
+
registerGetAnnotatedMessageTool(mockServer);
|
|
239
|
+
const handler = handlers.get('get-annotated-message');
|
|
240
|
+
const result = await handler({ messageType: 'debug', includeImage: false });
|
|
241
|
+
expect(result.content[0].text).toContain('Debug:');
|
|
242
|
+
expect(result.content[0].annotations.priority).toBe(0.3);
|
|
243
|
+
expect(result.content[0].annotations.audience).toEqual(['assistant']);
|
|
244
|
+
});
|
|
245
|
+
it('should include annotated image when requested', async () => {
|
|
246
|
+
const { mockServer, handlers } = createMockServer();
|
|
247
|
+
registerGetAnnotatedMessageTool(mockServer);
|
|
248
|
+
const handler = handlers.get('get-annotated-message');
|
|
249
|
+
const result = await handler({ messageType: 'success', includeImage: true });
|
|
250
|
+
expect(result.content).toHaveLength(2);
|
|
251
|
+
expect(result.content[1].type).toBe('image');
|
|
252
|
+
expect(result.content[1].annotations).toEqual({
|
|
253
|
+
priority: 0.5,
|
|
254
|
+
audience: ['user'],
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
describe('trigger-long-running-operation', () => {
|
|
259
|
+
it('should complete operation and return result', async () => {
|
|
260
|
+
const { mockServer, handlers } = createMockServer();
|
|
261
|
+
registerTriggerLongRunningOperationTool(mockServer);
|
|
262
|
+
const handler = handlers.get('trigger-long-running-operation');
|
|
263
|
+
// Use very short duration for test
|
|
264
|
+
const result = await handler({ duration: 0.1, steps: 2 }, { _meta: {}, requestId: 'test-123' });
|
|
265
|
+
expect(result.content[0].text).toContain('Long running operation completed');
|
|
266
|
+
expect(result.content[0].text).toContain('Duration: 0.1 seconds');
|
|
267
|
+
expect(result.content[0].text).toContain('Steps: 2');
|
|
268
|
+
}, 10000);
|
|
269
|
+
it('should send progress notifications when progressToken provided', async () => {
|
|
270
|
+
const { mockServer, handlers } = createMockServer();
|
|
271
|
+
registerTriggerLongRunningOperationTool(mockServer);
|
|
272
|
+
const handler = handlers.get('trigger-long-running-operation');
|
|
273
|
+
await handler({ duration: 0.1, steps: 2 }, { _meta: { progressToken: 'token-123' }, requestId: 'test-456', sessionId: 'session-1' });
|
|
274
|
+
expect(mockServer.server.notification).toHaveBeenCalledTimes(2);
|
|
275
|
+
expect(mockServer.server.notification).toHaveBeenCalledWith(expect.objectContaining({
|
|
276
|
+
method: 'notifications/progress',
|
|
277
|
+
params: expect.objectContaining({
|
|
278
|
+
progressToken: 'token-123',
|
|
279
|
+
}),
|
|
280
|
+
}), expect.any(Object));
|
|
281
|
+
}, 10000);
|
|
282
|
+
});
|
|
283
|
+
describe('get-resource-links', () => {
|
|
284
|
+
it('should return specified number of resource links', async () => {
|
|
285
|
+
const { mockServer, handlers } = createMockServer();
|
|
286
|
+
registerGetResourceLinksTool(mockServer);
|
|
287
|
+
const handler = handlers.get('get-resource-links');
|
|
288
|
+
const result = await handler({ count: 3 });
|
|
289
|
+
// 1 intro text + 3 resource links
|
|
290
|
+
expect(result.content).toHaveLength(4);
|
|
291
|
+
expect(result.content[0].type).toBe('text');
|
|
292
|
+
expect(result.content[0].text).toContain('3 resource links');
|
|
293
|
+
// Check resource links
|
|
294
|
+
for (let i = 1; i < 4; i++) {
|
|
295
|
+
expect(result.content[i].type).toBe('resource_link');
|
|
296
|
+
expect(result.content[i].uri).toBeDefined();
|
|
297
|
+
expect(result.content[i].name).toBeDefined();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
it('should alternate between text and blob resources', async () => {
|
|
301
|
+
const { mockServer, handlers } = createMockServer();
|
|
302
|
+
registerGetResourceLinksTool(mockServer);
|
|
303
|
+
const handler = handlers.get('get-resource-links');
|
|
304
|
+
const result = await handler({ count: 4 });
|
|
305
|
+
// Odd IDs (1, 3) are blob, even IDs (2, 4) are text
|
|
306
|
+
expect(result.content[1].name).toContain('Blob');
|
|
307
|
+
expect(result.content[2].name).toContain('Text');
|
|
308
|
+
expect(result.content[3].name).toContain('Blob');
|
|
309
|
+
expect(result.content[4].name).toContain('Text');
|
|
310
|
+
});
|
|
311
|
+
it('should use default count of 3', async () => {
|
|
312
|
+
const { mockServer, handlers } = createMockServer();
|
|
313
|
+
registerGetResourceLinksTool(mockServer);
|
|
314
|
+
const handler = handlers.get('get-resource-links');
|
|
315
|
+
const result = await handler({});
|
|
316
|
+
// 1 intro text + 3 resource links (default)
|
|
317
|
+
expect(result.content).toHaveLength(4);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
describe('get-resource-reference', () => {
|
|
321
|
+
it('should return text resource reference', async () => {
|
|
322
|
+
const { mockServer, handlers } = createMockServer();
|
|
323
|
+
registerGetResourceReferenceTool(mockServer);
|
|
324
|
+
const handler = handlers.get('get-resource-reference');
|
|
325
|
+
const result = await handler({ resourceType: 'Text', resourceId: 1 });
|
|
326
|
+
expect(result.content).toHaveLength(3);
|
|
327
|
+
expect(result.content[0].text).toContain('Resource 1');
|
|
328
|
+
expect(result.content[1].type).toBe('resource');
|
|
329
|
+
expect(result.content[1].resource.uri).toContain('text/1');
|
|
330
|
+
expect(result.content[2].text).toContain('URI');
|
|
331
|
+
});
|
|
332
|
+
it('should return blob resource reference', async () => {
|
|
333
|
+
const { mockServer, handlers } = createMockServer();
|
|
334
|
+
registerGetResourceReferenceTool(mockServer);
|
|
335
|
+
const handler = handlers.get('get-resource-reference');
|
|
336
|
+
const result = await handler({ resourceType: 'Blob', resourceId: 5 });
|
|
337
|
+
expect(result.content[1].resource.uri).toContain('blob/5');
|
|
338
|
+
});
|
|
339
|
+
it('should reject invalid resource type', async () => {
|
|
340
|
+
const { mockServer, handlers } = createMockServer();
|
|
341
|
+
registerGetResourceReferenceTool(mockServer);
|
|
342
|
+
const handler = handlers.get('get-resource-reference');
|
|
343
|
+
await expect(handler({ resourceType: 'Invalid', resourceId: 1 })).rejects.toThrow('Invalid resourceType');
|
|
344
|
+
});
|
|
345
|
+
it('should reject invalid resource ID', async () => {
|
|
346
|
+
const { mockServer, handlers } = createMockServer();
|
|
347
|
+
registerGetResourceReferenceTool(mockServer);
|
|
348
|
+
const handler = handlers.get('get-resource-reference');
|
|
349
|
+
await expect(handler({ resourceType: 'Text', resourceId: -1 })).rejects.toThrow('Invalid resourceId');
|
|
350
|
+
await expect(handler({ resourceType: 'Text', resourceId: 0 })).rejects.toThrow('Invalid resourceId');
|
|
351
|
+
await expect(handler({ resourceType: 'Text', resourceId: 1.5 })).rejects.toThrow('Invalid resourceId');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
describe('toggle-simulated-logging', () => {
|
|
355
|
+
it('should start logging when not active', async () => {
|
|
356
|
+
const { mockServer, handlers } = createMockServer();
|
|
357
|
+
registerToggleSimulatedLoggingTool(mockServer);
|
|
358
|
+
const handler = handlers.get('toggle-simulated-logging');
|
|
359
|
+
const result = await handler({}, { sessionId: 'test-session-1' });
|
|
360
|
+
expect(result.content[0].text).toContain('Started');
|
|
361
|
+
expect(result.content[0].text).toContain('test-session-1');
|
|
362
|
+
});
|
|
363
|
+
it('should stop logging when already active', async () => {
|
|
364
|
+
const { mockServer, handlers } = createMockServer();
|
|
365
|
+
registerToggleSimulatedLoggingTool(mockServer);
|
|
366
|
+
const handler = handlers.get('toggle-simulated-logging');
|
|
367
|
+
// First call starts logging
|
|
368
|
+
await handler({}, { sessionId: 'test-session-2' });
|
|
369
|
+
// Second call stops logging
|
|
370
|
+
const result = await handler({}, { sessionId: 'test-session-2' });
|
|
371
|
+
expect(result.content[0].text).toContain('Stopped');
|
|
372
|
+
expect(result.content[0].text).toContain('test-session-2');
|
|
373
|
+
});
|
|
374
|
+
it('should handle undefined sessionId', async () => {
|
|
375
|
+
const { mockServer, handlers } = createMockServer();
|
|
376
|
+
registerToggleSimulatedLoggingTool(mockServer);
|
|
377
|
+
const handler = handlers.get('toggle-simulated-logging');
|
|
378
|
+
const result = await handler({}, {});
|
|
379
|
+
expect(result.content[0].text).toContain('Started');
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
describe('toggle-subscriber-updates', () => {
|
|
383
|
+
it('should start updates when not active', async () => {
|
|
384
|
+
const { mockServer, handlers } = createMockServer();
|
|
385
|
+
registerToggleSubscriberUpdatesTool(mockServer);
|
|
386
|
+
const handler = handlers.get('toggle-subscriber-updates');
|
|
387
|
+
const result = await handler({}, { sessionId: 'sub-session-1' });
|
|
388
|
+
expect(result.content[0].text).toContain('Started');
|
|
389
|
+
expect(result.content[0].text).toContain('sub-session-1');
|
|
390
|
+
});
|
|
391
|
+
it('should stop updates when already active', async () => {
|
|
392
|
+
const { mockServer, handlers } = createMockServer();
|
|
393
|
+
registerToggleSubscriberUpdatesTool(mockServer);
|
|
394
|
+
const handler = handlers.get('toggle-subscriber-updates');
|
|
395
|
+
// First call starts updates
|
|
396
|
+
await handler({}, { sessionId: 'sub-session-2' });
|
|
397
|
+
// Second call stops updates
|
|
398
|
+
const result = await handler({}, { sessionId: 'sub-session-2' });
|
|
399
|
+
expect(result.content[0].text).toContain('Stopped');
|
|
400
|
+
expect(result.content[0].text).toContain('sub-session-2');
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
describe('trigger-sampling-request', () => {
|
|
404
|
+
it('should not register when client does not support sampling', () => {
|
|
405
|
+
const { mockServer } = createMockServer();
|
|
406
|
+
registerTriggerSamplingRequestTool(mockServer);
|
|
407
|
+
// Tool should not be registered since mock server returns empty capabilities
|
|
408
|
+
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
|
409
|
+
});
|
|
410
|
+
it('should register when client supports sampling', () => {
|
|
411
|
+
const handlers = new Map();
|
|
412
|
+
const mockServer = {
|
|
413
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
414
|
+
handlers.set(name, handler);
|
|
415
|
+
}),
|
|
416
|
+
server: {
|
|
417
|
+
getClientCapabilities: vi.fn(() => ({ sampling: {} })),
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
registerTriggerSamplingRequestTool(mockServer);
|
|
421
|
+
expect(mockServer.registerTool).toHaveBeenCalledWith('trigger-sampling-request', expect.objectContaining({
|
|
422
|
+
title: 'Trigger Sampling Request Tool',
|
|
423
|
+
description: expect.stringContaining('Sampling'),
|
|
424
|
+
}), expect.any(Function));
|
|
425
|
+
});
|
|
426
|
+
it('should send sampling request and return result', async () => {
|
|
427
|
+
const handlers = new Map();
|
|
428
|
+
const mockSendRequest = vi.fn().mockResolvedValue({
|
|
429
|
+
model: 'test-model',
|
|
430
|
+
content: { type: 'text', text: 'LLM response' },
|
|
431
|
+
});
|
|
432
|
+
const mockServer = {
|
|
433
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
434
|
+
handlers.set(name, handler);
|
|
435
|
+
}),
|
|
436
|
+
server: {
|
|
437
|
+
getClientCapabilities: vi.fn(() => ({ sampling: {} })),
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
registerTriggerSamplingRequestTool(mockServer);
|
|
441
|
+
const handler = handlers.get('trigger-sampling-request');
|
|
442
|
+
const result = await handler({ prompt: 'Test prompt', maxTokens: 50 }, { sendRequest: mockSendRequest });
|
|
443
|
+
expect(mockSendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
444
|
+
method: 'sampling/createMessage',
|
|
445
|
+
params: expect.objectContaining({
|
|
446
|
+
maxTokens: 50,
|
|
447
|
+
}),
|
|
448
|
+
}), expect.anything());
|
|
449
|
+
expect(result.content[0].text).toContain('LLM sampling result');
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
describe('trigger-elicitation-request', () => {
|
|
453
|
+
it('should not register when client does not support elicitation', () => {
|
|
454
|
+
const { mockServer } = createMockServer();
|
|
455
|
+
registerTriggerElicitationRequestTool(mockServer);
|
|
456
|
+
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
it('should register when client supports elicitation', () => {
|
|
459
|
+
const handlers = new Map();
|
|
460
|
+
const mockServer = {
|
|
461
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
462
|
+
handlers.set(name, handler);
|
|
463
|
+
}),
|
|
464
|
+
server: {
|
|
465
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
registerTriggerElicitationRequestTool(mockServer);
|
|
469
|
+
expect(mockServer.registerTool).toHaveBeenCalledWith('trigger-elicitation-request', expect.objectContaining({
|
|
470
|
+
title: 'Trigger Elicitation Request Tool',
|
|
471
|
+
description: expect.stringContaining('Elicitation'),
|
|
472
|
+
}), expect.any(Function));
|
|
473
|
+
});
|
|
474
|
+
it('should handle accept action with user content', async () => {
|
|
475
|
+
const handlers = new Map();
|
|
476
|
+
const mockSendRequest = vi.fn().mockResolvedValue({
|
|
477
|
+
action: 'accept',
|
|
478
|
+
content: {
|
|
479
|
+
name: 'John Doe',
|
|
480
|
+
check: true,
|
|
481
|
+
email: 'john@example.com',
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
const mockServer = {
|
|
485
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
486
|
+
handlers.set(name, handler);
|
|
487
|
+
}),
|
|
488
|
+
server: {
|
|
489
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
registerTriggerElicitationRequestTool(mockServer);
|
|
493
|
+
const handler = handlers.get('trigger-elicitation-request');
|
|
494
|
+
const result = await handler({}, { sendRequest: mockSendRequest });
|
|
495
|
+
expect(result.content[0].text).toContain('✅');
|
|
496
|
+
expect(result.content[0].text).toContain('provided');
|
|
497
|
+
expect(result.content[1].text).toContain('John Doe');
|
|
498
|
+
});
|
|
499
|
+
it('should handle decline action', async () => {
|
|
500
|
+
const handlers = new Map();
|
|
501
|
+
const mockSendRequest = vi.fn().mockResolvedValue({
|
|
502
|
+
action: 'decline',
|
|
503
|
+
});
|
|
504
|
+
const mockServer = {
|
|
505
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
506
|
+
handlers.set(name, handler);
|
|
507
|
+
}),
|
|
508
|
+
server: {
|
|
509
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
registerTriggerElicitationRequestTool(mockServer);
|
|
513
|
+
const handler = handlers.get('trigger-elicitation-request');
|
|
514
|
+
const result = await handler({}, { sendRequest: mockSendRequest });
|
|
515
|
+
expect(result.content[0].text).toContain('❌');
|
|
516
|
+
expect(result.content[0].text).toContain('declined');
|
|
517
|
+
});
|
|
518
|
+
it('should handle cancel action', async () => {
|
|
519
|
+
const handlers = new Map();
|
|
520
|
+
const mockSendRequest = vi.fn().mockResolvedValue({
|
|
521
|
+
action: 'cancel',
|
|
522
|
+
});
|
|
523
|
+
const mockServer = {
|
|
524
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
525
|
+
handlers.set(name, handler);
|
|
526
|
+
}),
|
|
527
|
+
server: {
|
|
528
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: {} })),
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
registerTriggerElicitationRequestTool(mockServer);
|
|
532
|
+
const handler = handlers.get('trigger-elicitation-request');
|
|
533
|
+
const result = await handler({}, { sendRequest: mockSendRequest });
|
|
534
|
+
expect(result.content[0].text).toContain('⚠️');
|
|
535
|
+
expect(result.content[0].text).toContain('cancelled');
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
describe('trigger-url-elicitation', () => {
|
|
539
|
+
// The error-path marker is module-level state shared across cases; reset it
|
|
540
|
+
// so tests are independent of order and of each other's leftover keys.
|
|
541
|
+
beforeEach(() => {
|
|
542
|
+
__resetIssuedErrorPathElicitations();
|
|
543
|
+
});
|
|
544
|
+
it('should not register when client does not support URL elicitation', () => {
|
|
545
|
+
const handlers = new Map();
|
|
546
|
+
const mockServer = {
|
|
547
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
548
|
+
handlers.set(name, handler);
|
|
549
|
+
}),
|
|
550
|
+
server: {
|
|
551
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { form: {} } })),
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
555
|
+
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
|
556
|
+
});
|
|
557
|
+
it('should register when client supports URL elicitation', () => {
|
|
558
|
+
const handlers = new Map();
|
|
559
|
+
const mockServer = {
|
|
560
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
561
|
+
handlers.set(name, handler);
|
|
562
|
+
}),
|
|
563
|
+
server: {
|
|
564
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
568
|
+
expect(mockServer.registerTool).toHaveBeenCalledWith('trigger-url-elicitation', expect.objectContaining({
|
|
569
|
+
title: 'Trigger URL Elicitation Tool',
|
|
570
|
+
description: expect.stringContaining('URL elicitation'),
|
|
571
|
+
}), expect.any(Function));
|
|
572
|
+
});
|
|
573
|
+
it('should send URL-mode elicitation request when errorPath is false', async () => {
|
|
574
|
+
const handlers = new Map();
|
|
575
|
+
const mockSendRequest = vi.fn().mockResolvedValue({
|
|
576
|
+
action: 'accept',
|
|
577
|
+
});
|
|
578
|
+
const mockServer = {
|
|
579
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
580
|
+
handlers.set(name, handler);
|
|
581
|
+
}),
|
|
582
|
+
server: {
|
|
583
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
587
|
+
const handler = handlers.get('trigger-url-elicitation');
|
|
588
|
+
const result = await handler({
|
|
589
|
+
url: 'https://example.com/verify',
|
|
590
|
+
message: 'Open this page to verify your identity',
|
|
591
|
+
elicitationId: 'elicitation-123',
|
|
592
|
+
errorPath: false,
|
|
593
|
+
}, { sendRequest: mockSendRequest });
|
|
594
|
+
expect(mockSendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
595
|
+
method: 'elicitation/create',
|
|
596
|
+
params: expect.objectContaining({
|
|
597
|
+
mode: 'url',
|
|
598
|
+
url: 'https://example.com/verify',
|
|
599
|
+
message: 'Open this page to verify your identity',
|
|
600
|
+
elicitationId: 'elicitation-123',
|
|
601
|
+
}),
|
|
602
|
+
}), expect.anything(), expect.anything());
|
|
603
|
+
expect(result.content[0].text).toContain('✅ User completed the URL elicitation flow.');
|
|
604
|
+
});
|
|
605
|
+
it('should not register when client has no elicitation capability at all', () => {
|
|
606
|
+
const mockServer = {
|
|
607
|
+
registerTool: vi.fn(),
|
|
608
|
+
server: {
|
|
609
|
+
getClientCapabilities: vi.fn(() => ({})),
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
613
|
+
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
|
614
|
+
});
|
|
615
|
+
it('should not register when client capabilities are undefined', () => {
|
|
616
|
+
const mockServer = {
|
|
617
|
+
registerTool: vi.fn(),
|
|
618
|
+
server: {
|
|
619
|
+
getClientCapabilities: vi.fn(() => undefined),
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
623
|
+
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
|
624
|
+
});
|
|
625
|
+
it('should default the elicitationId to a random UUID when omitted', async () => {
|
|
626
|
+
const handlers = new Map();
|
|
627
|
+
const mockSendRequest = vi.fn().mockResolvedValue({
|
|
628
|
+
action: 'accept',
|
|
629
|
+
});
|
|
630
|
+
const mockServer = {
|
|
631
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
632
|
+
handlers.set(name, handler);
|
|
633
|
+
}),
|
|
634
|
+
server: {
|
|
635
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
639
|
+
const handler = handlers.get('trigger-url-elicitation');
|
|
640
|
+
await handler({
|
|
641
|
+
url: 'https://example.com/verify',
|
|
642
|
+
message: 'Open this page to verify your identity',
|
|
643
|
+
errorPath: false,
|
|
644
|
+
}, { sendRequest: mockSendRequest });
|
|
645
|
+
const sentParams = mockSendRequest.mock.calls[0][0].params;
|
|
646
|
+
expect(sentParams.elicitationId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
647
|
+
});
|
|
648
|
+
it('should report a declined URL elicitation', async () => {
|
|
649
|
+
const handlers = new Map();
|
|
650
|
+
const mockSendRequest = vi.fn().mockResolvedValue({ action: 'decline' });
|
|
651
|
+
const mockServer = {
|
|
652
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
653
|
+
handlers.set(name, handler);
|
|
654
|
+
}),
|
|
655
|
+
server: {
|
|
656
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
660
|
+
const handler = handlers.get('trigger-url-elicitation');
|
|
661
|
+
const result = await handler({
|
|
662
|
+
url: 'https://example.com/verify',
|
|
663
|
+
message: 'Open this page to verify your identity',
|
|
664
|
+
elicitationId: 'elicitation-123',
|
|
665
|
+
errorPath: false,
|
|
666
|
+
}, { sendRequest: mockSendRequest });
|
|
667
|
+
expect(result.content[0].text).toContain('❌ User declined to open the URL');
|
|
668
|
+
});
|
|
669
|
+
it('should report a cancelled URL elicitation', async () => {
|
|
670
|
+
const handlers = new Map();
|
|
671
|
+
const mockSendRequest = vi.fn().mockResolvedValue({ action: 'cancel' });
|
|
672
|
+
const mockServer = {
|
|
673
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
674
|
+
handlers.set(name, handler);
|
|
675
|
+
}),
|
|
676
|
+
server: {
|
|
677
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
681
|
+
const handler = handlers.get('trigger-url-elicitation');
|
|
682
|
+
const result = await handler({
|
|
683
|
+
url: 'https://example.com/verify',
|
|
684
|
+
message: 'Open this page to verify your identity',
|
|
685
|
+
elicitationId: 'elicitation-123',
|
|
686
|
+
errorPath: false,
|
|
687
|
+
}, { sendRequest: mockSendRequest });
|
|
688
|
+
expect(result.content[0].text).toContain('⚠️ User cancelled the URL elicitation');
|
|
689
|
+
});
|
|
690
|
+
it('should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true', async () => {
|
|
691
|
+
const handlers = new Map();
|
|
692
|
+
const mockServer = {
|
|
693
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
694
|
+
handlers.set(name, handler);
|
|
695
|
+
}),
|
|
696
|
+
server: {
|
|
697
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
701
|
+
const handler = handlers.get('trigger-url-elicitation');
|
|
702
|
+
expect.assertions(5);
|
|
703
|
+
try {
|
|
704
|
+
await handler({
|
|
705
|
+
url: 'https://example.com/connect',
|
|
706
|
+
message: 'Authorization is required to continue.',
|
|
707
|
+
elicitationId: 'elicitation-xyz',
|
|
708
|
+
errorPath: true,
|
|
709
|
+
}, {});
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
expect(error.code).toBe(-32042);
|
|
713
|
+
const prerequisite = error.data.elicitations[0];
|
|
714
|
+
expect(prerequisite.mode).toBe('url');
|
|
715
|
+
// The prerequisite must NOT reuse the failing URL, otherwise the client
|
|
716
|
+
// would complete it, retry, hit the same error, and loop forever.
|
|
717
|
+
expect(prerequisite.url).toBe('https://modelcontextprotocol.io');
|
|
718
|
+
expect(prerequisite.url).not.toBe('https://example.com/connect');
|
|
719
|
+
// It carries its own elicitation id for the prerequisite itself.
|
|
720
|
+
expect(typeof prerequisite.elicitationId).toBe('string');
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
it('should ignore errorPath and take the request path when the same call is retried after the prerequisite', async () => {
|
|
724
|
+
const handlers = new Map();
|
|
725
|
+
const mockSendRequest = vi.fn().mockResolvedValue({ action: 'accept' });
|
|
726
|
+
const mockServer = {
|
|
727
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
728
|
+
handlers.set(name, handler);
|
|
729
|
+
}),
|
|
730
|
+
server: {
|
|
731
|
+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
registerTriggerUrlElicitationTool(mockServer);
|
|
735
|
+
const handler = handlers.get('trigger-url-elicitation');
|
|
736
|
+
// A real client retries with the *same* arguments and does not echo the
|
|
737
|
+
// prerequisite's elicitationId. Note these args omit elicitationId, so the
|
|
738
|
+
// correlation must rely on stable inputs (session + url), not a per-call
|
|
739
|
+
// random id.
|
|
740
|
+
const args = {
|
|
741
|
+
url: 'https://example.com/connect',
|
|
742
|
+
message: 'Authorization is required to continue.',
|
|
743
|
+
errorPath: true,
|
|
744
|
+
};
|
|
745
|
+
const extra = { sessionId: 'session-1', sendRequest: mockSendRequest };
|
|
746
|
+
// First call: error path issues the prerequisite and throws -32042.
|
|
747
|
+
let prerequisiteUrl;
|
|
748
|
+
try {
|
|
749
|
+
await handler(args, extra);
|
|
750
|
+
throw new Error('expected first call to throw');
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
expect(error.code).toBe(-32042);
|
|
754
|
+
prerequisiteUrl = error.data.elicitations[0].url;
|
|
755
|
+
expect(prerequisiteUrl).toBe('https://modelcontextprotocol.io');
|
|
756
|
+
expect(mockSendRequest).not.toHaveBeenCalled();
|
|
757
|
+
}
|
|
758
|
+
// Plain retry with identical arguments: errorPath is ignored and the call
|
|
759
|
+
// proceeds via the request path instead of throwing the prerequisite again.
|
|
760
|
+
const result = await handler({ ...args }, extra);
|
|
761
|
+
expect(mockSendRequest).toHaveBeenCalledWith(expect.objectContaining({
|
|
762
|
+
method: 'elicitation/create',
|
|
763
|
+
params: expect.objectContaining({
|
|
764
|
+
mode: 'url',
|
|
765
|
+
url: 'https://example.com/connect',
|
|
766
|
+
}),
|
|
767
|
+
}), expect.anything(), expect.anything());
|
|
768
|
+
expect(result.content[0].text).toContain('✅ User completed the URL elicitation flow.');
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
describe('get-roots-list', () => {
|
|
772
|
+
it('should not register when client does not support roots', () => {
|
|
773
|
+
const { mockServer } = createMockServer();
|
|
774
|
+
registerGetRootsListTool(mockServer);
|
|
775
|
+
expect(mockServer.registerTool).not.toHaveBeenCalled();
|
|
776
|
+
});
|
|
777
|
+
it('should register when client supports roots', () => {
|
|
778
|
+
const handlers = new Map();
|
|
779
|
+
const mockServer = {
|
|
780
|
+
registerTool: vi.fn((name, config, handler) => {
|
|
781
|
+
handlers.set(name, handler);
|
|
782
|
+
}),
|
|
783
|
+
server: {
|
|
784
|
+
getClientCapabilities: vi.fn(() => ({ roots: {} })),
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
registerGetRootsListTool(mockServer);
|
|
788
|
+
expect(mockServer.registerTool).toHaveBeenCalledWith('get-roots-list', expect.objectContaining({
|
|
789
|
+
title: 'Get Roots List Tool',
|
|
790
|
+
description: expect.stringContaining('roots'),
|
|
791
|
+
}), expect.any(Function));
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
describe('simulate-research-query', () => {
|
|
795
|
+
function createMockServerWithTasks() {
|
|
796
|
+
const taskHandlers = {};
|
|
797
|
+
const mockServer = {
|
|
798
|
+
experimental: {
|
|
799
|
+
tasks: {
|
|
800
|
+
registerToolTask: vi.fn((_name, _config, handler) => {
|
|
801
|
+
Object.assign(taskHandlers, handler);
|
|
802
|
+
}),
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
server: { getClientCapabilities: vi.fn(() => ({ elicitation: {} })) },
|
|
806
|
+
};
|
|
807
|
+
return { mockServer, taskHandlers };
|
|
808
|
+
}
|
|
809
|
+
function createMockTaskStore(taskId) {
|
|
810
|
+
return {
|
|
811
|
+
createTask: vi.fn().mockResolvedValue({
|
|
812
|
+
taskId,
|
|
813
|
+
status: 'working',
|
|
814
|
+
createdAt: new Date().toISOString(),
|
|
815
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
816
|
+
ttl: 300000,
|
|
817
|
+
pollInterval: 1000,
|
|
818
|
+
}),
|
|
819
|
+
updateTaskStatus: vi.fn().mockResolvedValue(undefined),
|
|
820
|
+
storeTaskResult: vi.fn().mockResolvedValue(undefined),
|
|
821
|
+
getTask: vi.fn(),
|
|
822
|
+
getTaskResult: vi.fn(),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
it('should pass relatedTask to sendRequest when elicitation is triggered', async () => {
|
|
826
|
+
vi.useFakeTimers();
|
|
827
|
+
const { mockServer, taskHandlers } = createMockServerWithTasks();
|
|
828
|
+
registerSimulateResearchQueryTool(mockServer);
|
|
829
|
+
const mockTaskStore = createMockTaskStore('task-abc');
|
|
830
|
+
const mockSendRequest = vi.fn().mockResolvedValue({
|
|
831
|
+
action: 'accept',
|
|
832
|
+
content: { interpretation: 'technical' },
|
|
833
|
+
});
|
|
834
|
+
await taskHandlers.createTask({ topic: 'python', ambiguous: true }, { taskStore: mockTaskStore, sendRequest: mockSendRequest });
|
|
835
|
+
await vi.runAllTimersAsync();
|
|
836
|
+
vi.useRealTimers();
|
|
837
|
+
expect(mockSendRequest).toHaveBeenCalledWith(expect.objectContaining({ method: 'elicitation/create' }), expect.anything(), expect.objectContaining({ relatedTask: { taskId: 'task-abc' } }));
|
|
838
|
+
});
|
|
839
|
+
it('should complete without elicitation for non-ambiguous query', async () => {
|
|
840
|
+
vi.useFakeTimers();
|
|
841
|
+
const { mockServer, taskHandlers } = createMockServerWithTasks();
|
|
842
|
+
registerSimulateResearchQueryTool(mockServer);
|
|
843
|
+
const mockTaskStore = createMockTaskStore('task-def');
|
|
844
|
+
const mockSendRequest = vi.fn();
|
|
845
|
+
await taskHandlers.createTask({ topic: 'python', ambiguous: false }, { taskStore: mockTaskStore, sendRequest: mockSendRequest });
|
|
846
|
+
await vi.runAllTimersAsync();
|
|
847
|
+
vi.useRealTimers();
|
|
848
|
+
expect(mockSendRequest).not.toHaveBeenCalled();
|
|
849
|
+
expect(mockTaskStore.storeTaskResult).toHaveBeenCalledWith('task-def', 'completed', expect.anything());
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
describe('gzip-file-as-resource', () => {
|
|
853
|
+
it('should compress data URI and return resource link', async () => {
|
|
854
|
+
const registeredResources = [];
|
|
855
|
+
const mockServer = {
|
|
856
|
+
registerTool: vi.fn(),
|
|
857
|
+
registerResource: vi.fn((...args) => {
|
|
858
|
+
registeredResources.push(args);
|
|
859
|
+
}),
|
|
860
|
+
};
|
|
861
|
+
// Get the handler
|
|
862
|
+
let handler = null;
|
|
863
|
+
mockServer.registerTool.mockImplementation((name, config, h) => {
|
|
864
|
+
handler = h;
|
|
865
|
+
});
|
|
866
|
+
registerGZipFileAsResourceTool(mockServer);
|
|
867
|
+
// Create a data URI with test content
|
|
868
|
+
const testContent = 'Hello, World!';
|
|
869
|
+
const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`;
|
|
870
|
+
const result = await handler({ name: 'test.txt.gz', data: dataUri, outputType: 'resourceLink' });
|
|
871
|
+
expect(result.content[0].type).toBe('resource_link');
|
|
872
|
+
expect(result.content[0].uri).toContain('test.txt.gz');
|
|
873
|
+
});
|
|
874
|
+
it('should return resource directly when outputType is resource', async () => {
|
|
875
|
+
const mockServer = {
|
|
876
|
+
registerTool: vi.fn(),
|
|
877
|
+
registerResource: vi.fn(),
|
|
878
|
+
};
|
|
879
|
+
let handler = null;
|
|
880
|
+
mockServer.registerTool.mockImplementation((name, config, h) => {
|
|
881
|
+
handler = h;
|
|
882
|
+
});
|
|
883
|
+
registerGZipFileAsResourceTool(mockServer);
|
|
884
|
+
const testContent = 'Test content for compression';
|
|
885
|
+
const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`;
|
|
886
|
+
const result = await handler({ name: 'output.gz', data: dataUri, outputType: 'resource' });
|
|
887
|
+
expect(result.content[0].type).toBe('resource');
|
|
888
|
+
expect(result.content[0].resource.mimeType).toBe('application/gzip');
|
|
889
|
+
expect(result.content[0].resource.blob).toBeDefined();
|
|
890
|
+
});
|
|
891
|
+
it('should reject unsupported URL protocols', async () => {
|
|
892
|
+
const mockServer = {
|
|
893
|
+
registerTool: vi.fn(),
|
|
894
|
+
registerResource: vi.fn(),
|
|
895
|
+
};
|
|
896
|
+
let handler = null;
|
|
897
|
+
mockServer.registerTool.mockImplementation((name, config, h) => {
|
|
898
|
+
handler = h;
|
|
899
|
+
});
|
|
900
|
+
registerGZipFileAsResourceTool(mockServer);
|
|
901
|
+
await expect(handler({ name: 'test.gz', data: 'ftp://example.com/file.txt', outputType: 'resource' })).rejects.toThrow('Unsupported URL protocol');
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
});
|