@claude-flow/mcp 3.0.0-alpha.1
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/.agentic-flow/intelligence.json +16 -0
- package/README.md +428 -0
- package/__tests__/integration.test.ts +449 -0
- package/__tests__/mcp.test.ts +641 -0
- package/dist/connection-pool.d.ts +36 -0
- package/dist/connection-pool.d.ts.map +1 -0
- package/dist/connection-pool.js +273 -0
- package/dist/connection-pool.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth.d.ts +146 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +318 -0
- package/dist/oauth.js.map +1 -0
- package/dist/prompt-registry.d.ts +90 -0
- package/dist/prompt-registry.d.ts.map +1 -0
- package/dist/prompt-registry.js +209 -0
- package/dist/prompt-registry.js.map +1 -0
- package/dist/rate-limiter.d.ts +86 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +197 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/resource-registry.d.ts +144 -0
- package/dist/resource-registry.d.ts.map +1 -0
- package/dist/resource-registry.js +405 -0
- package/dist/resource-registry.js.map +1 -0
- package/dist/sampling.d.ts +102 -0
- package/dist/sampling.d.ts.map +1 -0
- package/dist/sampling.js +268 -0
- package/dist/sampling.js.map +1 -0
- package/dist/schema-validator.d.ts +30 -0
- package/dist/schema-validator.d.ts.map +1 -0
- package/dist/schema-validator.js +182 -0
- package/dist/schema-validator.js.map +1 -0
- package/dist/server.d.ts +122 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +829 -0
- package/dist/server.js.map +1 -0
- package/dist/session-manager.d.ts +55 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +252 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/task-manager.d.ts +81 -0
- package/dist/task-manager.d.ts.map +1 -0
- package/dist/task-manager.js +337 -0
- package/dist/task-manager.js.map +1 -0
- package/dist/tool-registry.d.ts +88 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +353 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/transport/http.d.ts +55 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +446 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/index.d.ts +50 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +181 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/stdio.d.ts +43 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +194 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport/websocket.d.ts +65 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +314 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types.d.ts +473 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
- package/src/connection-pool.ts +344 -0
- package/src/index.ts +253 -0
- package/src/oauth.ts +447 -0
- package/src/prompt-registry.ts +296 -0
- package/src/rate-limiter.ts +266 -0
- package/src/resource-registry.ts +530 -0
- package/src/sampling.ts +363 -0
- package/src/schema-validator.ts +213 -0
- package/src/server.ts +1134 -0
- package/src/session-manager.ts +339 -0
- package/src/task-manager.ts +427 -0
- package/src/tool-registry.ts +475 -0
- package/src/transport/http.ts +532 -0
- package/src/transport/index.ts +233 -0
- package/src/transport/stdio.ts +252 -0
- package/src/transport/websocket.ts +396 -0
- package/src/types.ts +664 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @claude-flow/mcp - Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* End-to-end tests for MCP 2025-11-25 features
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
createMCPServer,
|
|
10
|
+
createTextResource,
|
|
11
|
+
definePrompt,
|
|
12
|
+
textMessage,
|
|
13
|
+
resourceMessage,
|
|
14
|
+
} from '../src/index.js';
|
|
15
|
+
import type { ILogger, MCPRequest, MCPResponse } from '../src/types.js';
|
|
16
|
+
|
|
17
|
+
const createMockLogger = (): ILogger => ({
|
|
18
|
+
debug: vi.fn(),
|
|
19
|
+
info: vi.fn(),
|
|
20
|
+
warn: vi.fn(),
|
|
21
|
+
error: vi.fn(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('MCP 2025-11-25 Integration', () => {
|
|
25
|
+
let server: ReturnType<typeof createMCPServer>;
|
|
26
|
+
let logger: ILogger;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
logger = createMockLogger();
|
|
30
|
+
server = createMCPServer(
|
|
31
|
+
{ name: 'Integration Test Server', transport: 'in-process' },
|
|
32
|
+
logger
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
await server.stop();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('Server Lifecycle', () => {
|
|
41
|
+
it('should start and report healthy', async () => {
|
|
42
|
+
await server.start();
|
|
43
|
+
const health = await server.getHealthStatus();
|
|
44
|
+
|
|
45
|
+
expect(health.healthy).toBe(true);
|
|
46
|
+
expect(health.metrics?.registeredTools).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should expose all registries', async () => {
|
|
50
|
+
await server.start();
|
|
51
|
+
|
|
52
|
+
expect(server.getResourceRegistry()).toBeDefined();
|
|
53
|
+
expect(server.getPromptRegistry()).toBeDefined();
|
|
54
|
+
expect(server.getTaskManager()).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Resources Integration', () => {
|
|
59
|
+
it('should register and read resources', async () => {
|
|
60
|
+
await server.start();
|
|
61
|
+
const registry = server.getResourceRegistry();
|
|
62
|
+
|
|
63
|
+
// Register multiple resources
|
|
64
|
+
const resources = [
|
|
65
|
+
createTextResource('file://config.json', 'Config', '{"key": "value"}', { mimeType: 'application/json' }),
|
|
66
|
+
createTextResource('file://readme.md', 'README', '# Hello World', { mimeType: 'text/markdown' }),
|
|
67
|
+
createTextResource('file://data.txt', 'Data', 'Some data content'),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const { resource, handler } of resources) {
|
|
71
|
+
registry.registerResource(resource, handler);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Verify listing
|
|
75
|
+
const list = registry.list();
|
|
76
|
+
expect(list.resources.length).toBe(3);
|
|
77
|
+
|
|
78
|
+
// Verify reading
|
|
79
|
+
const config = await registry.read('file://config.json');
|
|
80
|
+
expect(config.contents[0].text).toBe('{"key": "value"}');
|
|
81
|
+
expect(config.contents[0].mimeType).toBe('application/json');
|
|
82
|
+
|
|
83
|
+
const readme = await registry.read('file://readme.md');
|
|
84
|
+
expect(readme.contents[0].text).toBe('# Hello World');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle resource subscriptions', async () => {
|
|
88
|
+
await server.start();
|
|
89
|
+
const registry = server.getResourceRegistry();
|
|
90
|
+
|
|
91
|
+
const { resource, handler } = createTextResource('file://live.txt', 'Live', 'Initial');
|
|
92
|
+
registry.registerResource(resource, handler);
|
|
93
|
+
|
|
94
|
+
const callback = vi.fn();
|
|
95
|
+
const subId = registry.subscribe('file://live.txt', callback);
|
|
96
|
+
|
|
97
|
+
expect(subId).toBeDefined();
|
|
98
|
+
expect(registry.getSubscriptionCount('file://live.txt')).toBe(1);
|
|
99
|
+
|
|
100
|
+
// Unsubscribe
|
|
101
|
+
const unsubResult = registry.unsubscribe(subId);
|
|
102
|
+
expect(unsubResult).toBe(true);
|
|
103
|
+
expect(registry.getSubscriptionCount('file://live.txt')).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should cache resources', async () => {
|
|
107
|
+
await server.start();
|
|
108
|
+
const registry = server.getResourceRegistry();
|
|
109
|
+
|
|
110
|
+
let callCount = 0;
|
|
111
|
+
registry.registerResource(
|
|
112
|
+
{ uri: 'file://counter.txt', name: 'Counter', mimeType: 'text/plain' },
|
|
113
|
+
async () => {
|
|
114
|
+
callCount++;
|
|
115
|
+
return [{ uri: 'file://counter.txt', text: `Count: ${callCount}` }];
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// First read
|
|
120
|
+
await registry.read('file://counter.txt');
|
|
121
|
+
expect(callCount).toBe(1);
|
|
122
|
+
|
|
123
|
+
// Second read should be cached
|
|
124
|
+
await registry.read('file://counter.txt');
|
|
125
|
+
expect(callCount).toBe(1); // Still 1 due to cache
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('Prompts Integration', () => {
|
|
130
|
+
it('should register and execute prompts', async () => {
|
|
131
|
+
await server.start();
|
|
132
|
+
const registry = server.getPromptRegistry();
|
|
133
|
+
|
|
134
|
+
// Register code review prompt
|
|
135
|
+
registry.register(definePrompt(
|
|
136
|
+
'code_review',
|
|
137
|
+
'Review code quality',
|
|
138
|
+
async (args) => [
|
|
139
|
+
textMessage('user', `Review this ${args.language || 'code'}:\n\n${args.code}`),
|
|
140
|
+
],
|
|
141
|
+
{
|
|
142
|
+
title: 'Code Review',
|
|
143
|
+
arguments: [
|
|
144
|
+
{ name: 'code', required: true },
|
|
145
|
+
{ name: 'language', required: false },
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
));
|
|
149
|
+
|
|
150
|
+
// Register translation prompt
|
|
151
|
+
registry.register(definePrompt(
|
|
152
|
+
'translate',
|
|
153
|
+
'Translate text',
|
|
154
|
+
async (args) => [
|
|
155
|
+
textMessage('user', `Translate to ${args.target}: ${args.text}`),
|
|
156
|
+
],
|
|
157
|
+
{
|
|
158
|
+
arguments: [
|
|
159
|
+
{ name: 'text', required: true },
|
|
160
|
+
{ name: 'target', required: true },
|
|
161
|
+
],
|
|
162
|
+
}
|
|
163
|
+
));
|
|
164
|
+
|
|
165
|
+
// List prompts
|
|
166
|
+
const list = registry.list();
|
|
167
|
+
expect(list.prompts.length).toBe(2);
|
|
168
|
+
|
|
169
|
+
// Execute code review
|
|
170
|
+
const review = await registry.get('code_review', {
|
|
171
|
+
code: 'const x = 1;',
|
|
172
|
+
language: 'TypeScript',
|
|
173
|
+
});
|
|
174
|
+
expect(review.messages[0].content.type).toBe('text');
|
|
175
|
+
expect((review.messages[0].content as any).text).toContain('TypeScript');
|
|
176
|
+
|
|
177
|
+
// Execute translation
|
|
178
|
+
const translate = await registry.get('translate', {
|
|
179
|
+
text: 'Hello',
|
|
180
|
+
target: 'Spanish',
|
|
181
|
+
});
|
|
182
|
+
expect((translate.messages[0].content as any).text).toContain('Spanish');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should validate required arguments', async () => {
|
|
186
|
+
await server.start();
|
|
187
|
+
const registry = server.getPromptRegistry();
|
|
188
|
+
|
|
189
|
+
registry.register(definePrompt(
|
|
190
|
+
'greet',
|
|
191
|
+
'Greet someone',
|
|
192
|
+
async (args) => [textMessage('user', `Hello ${args.name}`)],
|
|
193
|
+
{ arguments: [{ name: 'name', required: true }] }
|
|
194
|
+
));
|
|
195
|
+
|
|
196
|
+
// Should throw without required argument
|
|
197
|
+
await expect(registry.get('greet', {})).rejects.toThrow('Missing required argument: name');
|
|
198
|
+
|
|
199
|
+
// Should work with argument
|
|
200
|
+
const result = await registry.get('greet', { name: 'World' });
|
|
201
|
+
expect((result.messages[0].content as any).text).toBe('Hello World');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('Tasks Integration', () => {
|
|
206
|
+
it('should create and complete tasks', async () => {
|
|
207
|
+
await server.start();
|
|
208
|
+
const taskManager = server.getTaskManager();
|
|
209
|
+
|
|
210
|
+
const taskId = taskManager.createTask(async (reportProgress) => {
|
|
211
|
+
reportProgress({ progress: 50, total: 100 });
|
|
212
|
+
return { success: true };
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = await taskManager.waitForTask(taskId, 5000);
|
|
216
|
+
expect(result.state).toBe('completed');
|
|
217
|
+
expect(result.result).toEqual({ success: true });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should track progress', async () => {
|
|
221
|
+
await server.start();
|
|
222
|
+
const taskManager = server.getTaskManager();
|
|
223
|
+
|
|
224
|
+
const progressUpdates: number[] = [];
|
|
225
|
+
|
|
226
|
+
taskManager.on('task:progress', (event: { taskId: string; progress: { progress: number } }) => {
|
|
227
|
+
progressUpdates.push(event.progress.progress);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const taskId = taskManager.createTask(async (reportProgress) => {
|
|
231
|
+
for (let i = 25; i <= 100; i += 25) {
|
|
232
|
+
reportProgress({ progress: i, total: 100 });
|
|
233
|
+
await new Promise(r => setTimeout(r, 10));
|
|
234
|
+
}
|
|
235
|
+
return 'done';
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await taskManager.waitForTask(taskId, 5000);
|
|
239
|
+
expect(progressUpdates.length).toBeGreaterThan(0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should cancel running tasks', async () => {
|
|
243
|
+
await server.start();
|
|
244
|
+
const taskManager = server.getTaskManager();
|
|
245
|
+
|
|
246
|
+
const taskId = taskManager.createTask(async (reportProgress, signal) => {
|
|
247
|
+
await new Promise((resolve, reject) => {
|
|
248
|
+
const timeout = setTimeout(resolve, 10000);
|
|
249
|
+
signal.addEventListener('abort', () => {
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
reject(new Error('Cancelled'));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Cancel immediately
|
|
257
|
+
const cancelled = taskManager.cancelTask(taskId, 'Test cancel');
|
|
258
|
+
expect(cancelled).toBe(true);
|
|
259
|
+
|
|
260
|
+
// Wait a bit and check status
|
|
261
|
+
await new Promise(r => setTimeout(r, 50));
|
|
262
|
+
const status = taskManager.getTask(taskId);
|
|
263
|
+
expect(status?.state).toBe('cancelled');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle concurrent tasks', async () => {
|
|
267
|
+
await server.start();
|
|
268
|
+
const taskManager = server.getTaskManager();
|
|
269
|
+
|
|
270
|
+
const taskIds = [];
|
|
271
|
+
for (let i = 0; i < 5; i++) {
|
|
272
|
+
const id = taskManager.createTask(async () => {
|
|
273
|
+
await new Promise(r => setTimeout(r, 10));
|
|
274
|
+
return `Task ${i} done`;
|
|
275
|
+
});
|
|
276
|
+
taskIds.push(id);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Wait for all
|
|
280
|
+
const results = await Promise.all(
|
|
281
|
+
taskIds.map(id => taskManager.waitForTask(id, 5000))
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(results.every(r => r.state === 'completed')).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('Full Server Flow', () => {
|
|
289
|
+
it('should handle complete MCP workflow', async () => {
|
|
290
|
+
await server.start();
|
|
291
|
+
|
|
292
|
+
// Setup resources
|
|
293
|
+
const resourceRegistry = server.getResourceRegistry();
|
|
294
|
+
const { resource, handler } = createTextResource(
|
|
295
|
+
'context://project',
|
|
296
|
+
'Project Context',
|
|
297
|
+
'This is a TypeScript project using MCP.'
|
|
298
|
+
);
|
|
299
|
+
resourceRegistry.registerResource(resource, handler);
|
|
300
|
+
|
|
301
|
+
// Setup prompts
|
|
302
|
+
const promptRegistry = server.getPromptRegistry();
|
|
303
|
+
promptRegistry.register(definePrompt(
|
|
304
|
+
'analyze',
|
|
305
|
+
'Analyze project',
|
|
306
|
+
async (args) => {
|
|
307
|
+
// Could include embedded resource here
|
|
308
|
+
return [
|
|
309
|
+
textMessage('user', `Analyze: ${args.focus}`),
|
|
310
|
+
];
|
|
311
|
+
},
|
|
312
|
+
{ arguments: [{ name: 'focus', required: true }] }
|
|
313
|
+
));
|
|
314
|
+
|
|
315
|
+
// Setup long-running task
|
|
316
|
+
const taskManager = server.getTaskManager();
|
|
317
|
+
const analysisTaskId = taskManager.createTask(async (reportProgress) => {
|
|
318
|
+
reportProgress({ progress: 0, message: 'Starting analysis' });
|
|
319
|
+
await new Promise(r => setTimeout(r, 20));
|
|
320
|
+
reportProgress({ progress: 50, message: 'Processing' });
|
|
321
|
+
await new Promise(r => setTimeout(r, 20));
|
|
322
|
+
reportProgress({ progress: 100, message: 'Complete' });
|
|
323
|
+
return { findings: ['All good!'] };
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Verify all components work together
|
|
327
|
+
const resourceList = resourceRegistry.list();
|
|
328
|
+
expect(resourceList.resources.length).toBe(1);
|
|
329
|
+
|
|
330
|
+
const promptList = promptRegistry.list();
|
|
331
|
+
expect(promptList.prompts.length).toBe(1);
|
|
332
|
+
|
|
333
|
+
const taskResult = await taskManager.waitForTask(analysisTaskId, 5000);
|
|
334
|
+
expect(taskResult.state).toBe('completed');
|
|
335
|
+
expect((taskResult.result as any).findings).toContain('All good!');
|
|
336
|
+
|
|
337
|
+
// Check metrics
|
|
338
|
+
const health = await server.getHealthStatus();
|
|
339
|
+
expect(health.healthy).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('Security Validation', () => {
|
|
344
|
+
it('should not expose internal state', async () => {
|
|
345
|
+
await server.start();
|
|
346
|
+
const metrics = server.getMetrics();
|
|
347
|
+
|
|
348
|
+
// Metrics should only contain safe data
|
|
349
|
+
expect(metrics).not.toHaveProperty('_internal');
|
|
350
|
+
expect(metrics).not.toHaveProperty('password');
|
|
351
|
+
expect(metrics).not.toHaveProperty('secret');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should validate tool inputs', async () => {
|
|
355
|
+
await server.start();
|
|
356
|
+
|
|
357
|
+
// Register a tool with schema
|
|
358
|
+
server.registerTool({
|
|
359
|
+
name: 'secure-tool',
|
|
360
|
+
description: 'A tool with input validation',
|
|
361
|
+
inputSchema: {
|
|
362
|
+
type: 'object',
|
|
363
|
+
properties: {
|
|
364
|
+
allowed: { type: 'string', maxLength: 100 },
|
|
365
|
+
},
|
|
366
|
+
required: ['allowed'],
|
|
367
|
+
},
|
|
368
|
+
handler: async (input) => ({ received: input }),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Tool registry should have it
|
|
372
|
+
const tools = server.getMetrics().toolInvocations;
|
|
373
|
+
expect(tools).toBeDefined();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should handle resource access errors gracefully', async () => {
|
|
377
|
+
await server.start();
|
|
378
|
+
const registry = server.getResourceRegistry();
|
|
379
|
+
|
|
380
|
+
// Try to read non-existent resource
|
|
381
|
+
await expect(registry.read('file://nonexistent.txt'))
|
|
382
|
+
.rejects.toThrow('Resource not found');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should prevent duplicate subscriptions overflow', async () => {
|
|
386
|
+
await server.start();
|
|
387
|
+
const registry = server.getResourceRegistry();
|
|
388
|
+
|
|
389
|
+
const { resource, handler } = createTextResource('file://test.txt', 'Test', 'content');
|
|
390
|
+
registry.registerResource(resource, handler);
|
|
391
|
+
|
|
392
|
+
// Subscribe many times (should be limited by maxSubscriptionsPerResource)
|
|
393
|
+
for (let i = 0; i < 100; i++) {
|
|
394
|
+
registry.subscribe('file://test.txt', vi.fn());
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
expect(registry.getSubscriptionCount('file://test.txt')).toBe(100);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should enforce cache size limits (CVE fix)', async () => {
|
|
401
|
+
await server.start();
|
|
402
|
+
const registry = server.getResourceRegistry();
|
|
403
|
+
|
|
404
|
+
// Register many resources to test cache eviction
|
|
405
|
+
for (let i = 0; i < 10; i++) {
|
|
406
|
+
const { resource, handler } = createTextResource(
|
|
407
|
+
`file://cache-test-${i}.txt`,
|
|
408
|
+
`Cache Test ${i}`,
|
|
409
|
+
`Content ${i}`
|
|
410
|
+
);
|
|
411
|
+
registry.registerResource(resource, handler);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Read all resources to populate cache
|
|
415
|
+
for (let i = 0; i < 10; i++) {
|
|
416
|
+
await registry.read(`file://cache-test-${i}.txt`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Cache should be limited (default 1000, but should work)
|
|
420
|
+
const stats = registry.getStats();
|
|
421
|
+
expect(stats.cacheSize).toBeLessThanOrEqual(1000);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('should escape regex in template matching (ReDoS fix)', async () => {
|
|
425
|
+
await server.start();
|
|
426
|
+
const registry = server.getResourceRegistry();
|
|
427
|
+
|
|
428
|
+
// Register a template with potentially dangerous regex chars
|
|
429
|
+
registry.registerTemplate(
|
|
430
|
+
{
|
|
431
|
+
uriTemplate: 'db://users/{id}',
|
|
432
|
+
name: 'User Data',
|
|
433
|
+
mimeType: 'application/json',
|
|
434
|
+
},
|
|
435
|
+
async (uri) => [{ uri, text: '{}' }]
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
// This should not cause ReDoS - test with various inputs
|
|
439
|
+
const startTime = Date.now();
|
|
440
|
+
expect(registry.hasResource('db://users/123')).toBe(true);
|
|
441
|
+
expect(registry.hasResource('db://users/abc')).toBe(true);
|
|
442
|
+
expect(registry.hasResource('invalid://path')).toBe(false);
|
|
443
|
+
const elapsed = Date.now() - startTime;
|
|
444
|
+
|
|
445
|
+
// Should complete quickly (< 100ms), not hang due to ReDoS
|
|
446
|
+
expect(elapsed).toBeLessThan(100);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|