@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,641 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @claude-flow/mcp - Test Suite
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
createMCPServer,
|
|
8
|
+
createToolRegistry,
|
|
9
|
+
createSessionManager,
|
|
10
|
+
createConnectionPool,
|
|
11
|
+
createResourceRegistry,
|
|
12
|
+
createPromptRegistry,
|
|
13
|
+
createTaskManager,
|
|
14
|
+
defineTool,
|
|
15
|
+
definePrompt,
|
|
16
|
+
textMessage,
|
|
17
|
+
createTextResource,
|
|
18
|
+
interpolate,
|
|
19
|
+
ErrorCodes,
|
|
20
|
+
MCPServerError,
|
|
21
|
+
VERSION,
|
|
22
|
+
MODULE_NAME,
|
|
23
|
+
} from '../src/index.js';
|
|
24
|
+
import type { ILogger, MCPTool } from '../src/types.js';
|
|
25
|
+
|
|
26
|
+
// Mock logger
|
|
27
|
+
const createMockLogger = (): ILogger => ({
|
|
28
|
+
debug: vi.fn(),
|
|
29
|
+
info: vi.fn(),
|
|
30
|
+
warn: vi.fn(),
|
|
31
|
+
error: vi.fn(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('@claude-flow/mcp', () => {
|
|
35
|
+
describe('Module exports', () => {
|
|
36
|
+
it('should export VERSION', () => {
|
|
37
|
+
expect(VERSION).toBe('3.0.0');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should export MODULE_NAME', () => {
|
|
41
|
+
expect(MODULE_NAME).toBe('@claude-flow/mcp');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should export ErrorCodes', () => {
|
|
45
|
+
expect(ErrorCodes.PARSE_ERROR).toBe(-32700);
|
|
46
|
+
expect(ErrorCodes.INVALID_REQUEST).toBe(-32600);
|
|
47
|
+
expect(ErrorCodes.METHOD_NOT_FOUND).toBe(-32601);
|
|
48
|
+
expect(ErrorCodes.INTERNAL_ERROR).toBe(-32603);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('MCPServerError', () => {
|
|
53
|
+
it('should create error with code', () => {
|
|
54
|
+
const error = new MCPServerError('Test error', ErrorCodes.INVALID_REQUEST);
|
|
55
|
+
expect(error.message).toBe('Test error');
|
|
56
|
+
expect(error.code).toBe(ErrorCodes.INVALID_REQUEST);
|
|
57
|
+
expect(error.name).toBe('MCPServerError');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should convert to MCP error format', () => {
|
|
61
|
+
const error = new MCPServerError('Test error', ErrorCodes.PARSE_ERROR, { extra: 'data' });
|
|
62
|
+
const mcpError = error.toMCPError();
|
|
63
|
+
expect(mcpError.code).toBe(ErrorCodes.PARSE_ERROR);
|
|
64
|
+
expect(mcpError.message).toBe('Test error');
|
|
65
|
+
expect(mcpError.data).toEqual({ extra: 'data' });
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('ToolRegistry', () => {
|
|
70
|
+
let registry: ReturnType<typeof createToolRegistry>;
|
|
71
|
+
let logger: ILogger;
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
logger = createMockLogger();
|
|
75
|
+
registry = createToolRegistry(logger);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should register a tool', () => {
|
|
79
|
+
const tool: MCPTool = {
|
|
80
|
+
name: 'test-tool',
|
|
81
|
+
description: 'A test tool',
|
|
82
|
+
inputSchema: { type: 'object', properties: {} },
|
|
83
|
+
handler: async () => ({ result: 'success' }),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const result = registry.register(tool);
|
|
87
|
+
expect(result).toBe(true);
|
|
88
|
+
expect(registry.hasTool('test-tool')).toBe(true);
|
|
89
|
+
expect(registry.getToolCount()).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should not register duplicate tools', () => {
|
|
93
|
+
const tool: MCPTool = {
|
|
94
|
+
name: 'test-tool',
|
|
95
|
+
description: 'A test tool',
|
|
96
|
+
inputSchema: { type: 'object', properties: {} },
|
|
97
|
+
handler: async () => ({ result: 'success' }),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
registry.register(tool);
|
|
101
|
+
const result = registry.register(tool);
|
|
102
|
+
expect(result).toBe(false);
|
|
103
|
+
expect(registry.getToolCount()).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should override tool with option', () => {
|
|
107
|
+
const tool1: MCPTool = {
|
|
108
|
+
name: 'test-tool',
|
|
109
|
+
description: 'First version',
|
|
110
|
+
inputSchema: { type: 'object', properties: {} },
|
|
111
|
+
handler: async () => ({ result: 'v1' }),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const tool2: MCPTool = {
|
|
115
|
+
name: 'test-tool',
|
|
116
|
+
description: 'Second version',
|
|
117
|
+
inputSchema: { type: 'object', properties: {} },
|
|
118
|
+
handler: async () => ({ result: 'v2' }),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
registry.register(tool1);
|
|
122
|
+
const result = registry.register(tool2, { override: true });
|
|
123
|
+
expect(result).toBe(true);
|
|
124
|
+
expect(registry.getTool('test-tool')?.description).toBe('Second version');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should unregister a tool', () => {
|
|
128
|
+
const tool: MCPTool = {
|
|
129
|
+
name: 'test-tool',
|
|
130
|
+
description: 'A test tool',
|
|
131
|
+
inputSchema: { type: 'object', properties: {} },
|
|
132
|
+
handler: async () => ({ result: 'success' }),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
registry.register(tool);
|
|
136
|
+
const result = registry.unregister('test-tool');
|
|
137
|
+
expect(result).toBe(true);
|
|
138
|
+
expect(registry.hasTool('test-tool')).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should execute a tool', async () => {
|
|
142
|
+
const tool: MCPTool = {
|
|
143
|
+
name: 'test-tool',
|
|
144
|
+
description: 'A test tool',
|
|
145
|
+
inputSchema: { type: 'object', properties: {} },
|
|
146
|
+
handler: async (input: unknown) => ({ received: input }),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
registry.register(tool);
|
|
150
|
+
const result = await registry.execute('test-tool', { test: 'data' });
|
|
151
|
+
expect(result.isError).toBe(false);
|
|
152
|
+
expect(result.content[0].type).toBe('text');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return error for unknown tool', async () => {
|
|
156
|
+
const result = await registry.execute('unknown-tool', {});
|
|
157
|
+
expect(result.isError).toBe(true);
|
|
158
|
+
expect(result.content[0].text).toContain('Tool not found');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should filter by category', () => {
|
|
162
|
+
registry.register({
|
|
163
|
+
name: 'tool-a',
|
|
164
|
+
description: 'Tool A',
|
|
165
|
+
inputSchema: { type: 'object' },
|
|
166
|
+
handler: async () => ({}),
|
|
167
|
+
category: 'category-1',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
registry.register({
|
|
171
|
+
name: 'tool-b',
|
|
172
|
+
description: 'Tool B',
|
|
173
|
+
inputSchema: { type: 'object' },
|
|
174
|
+
handler: async () => ({}),
|
|
175
|
+
category: 'category-2',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const tools = registry.getByCategory('category-1');
|
|
179
|
+
expect(tools.length).toBe(1);
|
|
180
|
+
expect(tools[0].name).toBe('tool-a');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should get stats', () => {
|
|
184
|
+
registry.register({
|
|
185
|
+
name: 'tool-a',
|
|
186
|
+
description: 'Tool A',
|
|
187
|
+
inputSchema: { type: 'object' },
|
|
188
|
+
handler: async () => ({}),
|
|
189
|
+
category: 'cat',
|
|
190
|
+
tags: ['tag1', 'tag2'],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const stats = registry.getStats();
|
|
194
|
+
expect(stats.totalTools).toBe(1);
|
|
195
|
+
expect(stats.totalCategories).toBe(1);
|
|
196
|
+
expect(stats.totalTags).toBe(2);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('defineTool', () => {
|
|
201
|
+
it('should create a tool definition', () => {
|
|
202
|
+
const tool = defineTool(
|
|
203
|
+
'my-tool',
|
|
204
|
+
'My tool description',
|
|
205
|
+
{ type: 'object', properties: { input: { type: 'string' } } },
|
|
206
|
+
async (input) => ({ result: input }),
|
|
207
|
+
{ category: 'test', tags: ['tag1'] }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(tool.name).toBe('my-tool');
|
|
211
|
+
expect(tool.description).toBe('My tool description');
|
|
212
|
+
expect(tool.category).toBe('test');
|
|
213
|
+
expect(tool.tags).toEqual(['tag1']);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('SessionManager', () => {
|
|
218
|
+
let manager: ReturnType<typeof createSessionManager>;
|
|
219
|
+
let logger: ILogger;
|
|
220
|
+
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
logger = createMockLogger();
|
|
223
|
+
manager = createSessionManager(logger, { sessionTimeout: 1000 });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
afterEach(() => {
|
|
227
|
+
manager.destroy();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should create a session', () => {
|
|
231
|
+
const session = manager.createSession('stdio');
|
|
232
|
+
expect(session.id).toBeDefined();
|
|
233
|
+
expect(session.state).toBe('created');
|
|
234
|
+
expect(session.transport).toBe('stdio');
|
|
235
|
+
expect(session.isInitialized).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should initialize a session', () => {
|
|
239
|
+
const session = manager.createSession('stdio');
|
|
240
|
+
manager.initializeSession(session.id, {
|
|
241
|
+
protocolVersion: { major: 2024, minor: 11, patch: 5 },
|
|
242
|
+
capabilities: { tools: { listChanged: true } },
|
|
243
|
+
clientInfo: { name: 'test-client', version: '1.0.0' },
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const updated = manager.getSession(session.id);
|
|
247
|
+
expect(updated?.isInitialized).toBe(true);
|
|
248
|
+
expect(updated?.state).toBe('ready');
|
|
249
|
+
expect(updated?.clientInfo?.name).toBe('test-client');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should close a session', () => {
|
|
253
|
+
const session = manager.createSession('stdio');
|
|
254
|
+
const result = manager.closeSession(session.id, 'test reason');
|
|
255
|
+
expect(result).toBe(true);
|
|
256
|
+
expect(manager.getSession(session.id)).toBeUndefined();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should get session metrics', () => {
|
|
260
|
+
manager.createSession('stdio');
|
|
261
|
+
manager.createSession('http');
|
|
262
|
+
|
|
263
|
+
const metrics = manager.getSessionMetrics();
|
|
264
|
+
expect(metrics.total).toBe(2);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should update session activity', () => {
|
|
268
|
+
const session = manager.createSession('stdio');
|
|
269
|
+
const originalTime = session.lastActivityAt;
|
|
270
|
+
|
|
271
|
+
// Small delay to ensure time difference
|
|
272
|
+
const result = manager.updateActivity(session.id);
|
|
273
|
+
expect(result).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('ConnectionPool', () => {
|
|
278
|
+
let pool: ReturnType<typeof createConnectionPool>;
|
|
279
|
+
let logger: ILogger;
|
|
280
|
+
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
logger = createMockLogger();
|
|
283
|
+
pool = createConnectionPool(
|
|
284
|
+
{ maxConnections: 5, minConnections: 0, idleTimeout: 100, evictionRunInterval: 60000 },
|
|
285
|
+
logger,
|
|
286
|
+
'in-process'
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
afterEach(async () => {
|
|
291
|
+
// Release all connections first to allow fast drain
|
|
292
|
+
for (const conn of pool.getConnections()) {
|
|
293
|
+
if (conn.state === 'busy') {
|
|
294
|
+
pool.release(conn);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
await pool.clear();
|
|
298
|
+
}, 5000);
|
|
299
|
+
|
|
300
|
+
it('should acquire a connection', async () => {
|
|
301
|
+
const connection = await pool.acquire();
|
|
302
|
+
expect(connection.id).toBeDefined();
|
|
303
|
+
expect(connection.state).toBe('busy');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should release a connection', async () => {
|
|
307
|
+
const connection = await pool.acquire();
|
|
308
|
+
pool.release(connection);
|
|
309
|
+
|
|
310
|
+
const stats = pool.getStats();
|
|
311
|
+
expect(stats.idleConnections).toBeGreaterThan(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should get stats', async () => {
|
|
315
|
+
const connection = await pool.acquire();
|
|
316
|
+
const stats = pool.getStats();
|
|
317
|
+
|
|
318
|
+
expect(stats.totalConnections).toBeGreaterThan(0);
|
|
319
|
+
expect(stats.totalAcquired).toBe(1);
|
|
320
|
+
|
|
321
|
+
// Release for cleanup
|
|
322
|
+
pool.release(connection);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should check health', () => {
|
|
326
|
+
// Pool is healthy when not shutting down and has >= minConnections (0 in this test)
|
|
327
|
+
expect(pool.isHealthy()).toBe(true);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('MCPServer', () => {
|
|
332
|
+
let server: ReturnType<typeof createMCPServer>;
|
|
333
|
+
let logger: ILogger;
|
|
334
|
+
|
|
335
|
+
beforeEach(() => {
|
|
336
|
+
logger = createMockLogger();
|
|
337
|
+
server = createMCPServer(
|
|
338
|
+
{ name: 'Test Server', transport: 'in-process' },
|
|
339
|
+
logger
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
afterEach(async () => {
|
|
344
|
+
await server.stop();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should create server with config', () => {
|
|
348
|
+
expect(server).toBeDefined();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should register a tool', () => {
|
|
352
|
+
const result = server.registerTool({
|
|
353
|
+
name: 'test-tool',
|
|
354
|
+
description: 'Test tool',
|
|
355
|
+
inputSchema: { type: 'object' },
|
|
356
|
+
handler: async () => ({}),
|
|
357
|
+
});
|
|
358
|
+
expect(result).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should register multiple tools', () => {
|
|
362
|
+
const result = server.registerTools([
|
|
363
|
+
{
|
|
364
|
+
name: 'tool-1',
|
|
365
|
+
description: 'Tool 1',
|
|
366
|
+
inputSchema: { type: 'object' },
|
|
367
|
+
handler: async () => ({}),
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'tool-2',
|
|
371
|
+
description: 'Tool 2',
|
|
372
|
+
inputSchema: { type: 'object' },
|
|
373
|
+
handler: async () => ({}),
|
|
374
|
+
},
|
|
375
|
+
]);
|
|
376
|
+
expect(result.registered).toBe(2);
|
|
377
|
+
expect(result.failed).toHaveLength(0);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should start and stop', async () => {
|
|
381
|
+
await server.start();
|
|
382
|
+
const health = await server.getHealthStatus();
|
|
383
|
+
expect(health.healthy).toBe(true);
|
|
384
|
+
|
|
385
|
+
await server.stop();
|
|
386
|
+
const healthAfter = await server.getHealthStatus();
|
|
387
|
+
expect(healthAfter.healthy).toBe(false);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should get metrics', async () => {
|
|
391
|
+
await server.start();
|
|
392
|
+
const metrics = server.getMetrics();
|
|
393
|
+
|
|
394
|
+
expect(metrics.totalRequests).toBeDefined();
|
|
395
|
+
expect(metrics.activeSessions).toBeDefined();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should expose resource registry', () => {
|
|
399
|
+
const registry = server.getResourceRegistry();
|
|
400
|
+
expect(registry).toBeDefined();
|
|
401
|
+
expect(typeof registry.registerResource).toBe('function');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should expose prompt registry', () => {
|
|
405
|
+
const registry = server.getPromptRegistry();
|
|
406
|
+
expect(registry).toBeDefined();
|
|
407
|
+
expect(typeof registry.register).toBe('function');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should expose task manager', () => {
|
|
411
|
+
const taskManager = server.getTaskManager();
|
|
412
|
+
expect(taskManager).toBeDefined();
|
|
413
|
+
expect(typeof taskManager.createTask).toBe('function');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// MCP 2025-11-25 Features
|
|
419
|
+
// ============================================================================
|
|
420
|
+
|
|
421
|
+
describe('ResourceRegistry (MCP 2025-11-25)', () => {
|
|
422
|
+
let registry: ReturnType<typeof createResourceRegistry>;
|
|
423
|
+
let logger: ILogger;
|
|
424
|
+
|
|
425
|
+
beforeEach(() => {
|
|
426
|
+
logger = createMockLogger();
|
|
427
|
+
registry = createResourceRegistry(logger);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should register a resource', () => {
|
|
431
|
+
const { resource, handler } = createTextResource(
|
|
432
|
+
'file://test.txt',
|
|
433
|
+
'Test File',
|
|
434
|
+
'Hello World'
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const result = registry.registerResource(resource, handler);
|
|
438
|
+
expect(result).toBe(true);
|
|
439
|
+
expect(registry.hasResource('file://test.txt')).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should list resources with pagination', () => {
|
|
443
|
+
for (let i = 0; i < 5; i++) {
|
|
444
|
+
const { resource, handler } = createTextResource(
|
|
445
|
+
`file://test${i}.txt`,
|
|
446
|
+
`Test File ${i}`,
|
|
447
|
+
`Content ${i}`
|
|
448
|
+
);
|
|
449
|
+
registry.registerResource(resource, handler);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const result = registry.list(undefined, 3);
|
|
453
|
+
expect(result.resources.length).toBe(3);
|
|
454
|
+
expect(result.nextCursor).toBeDefined();
|
|
455
|
+
|
|
456
|
+
const nextResult = registry.list(result.nextCursor, 3);
|
|
457
|
+
expect(nextResult.resources.length).toBe(2);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should read resource content', async () => {
|
|
461
|
+
const { resource, handler } = createTextResource(
|
|
462
|
+
'file://test.txt',
|
|
463
|
+
'Test File',
|
|
464
|
+
'Hello World'
|
|
465
|
+
);
|
|
466
|
+
registry.registerResource(resource, handler);
|
|
467
|
+
|
|
468
|
+
const result = await registry.read('file://test.txt');
|
|
469
|
+
expect(result.contents[0].text).toBe('Hello World');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should subscribe to resource updates', () => {
|
|
473
|
+
const { resource, handler } = createTextResource(
|
|
474
|
+
'file://test.txt',
|
|
475
|
+
'Test File',
|
|
476
|
+
'Hello World'
|
|
477
|
+
);
|
|
478
|
+
registry.registerResource(resource, handler);
|
|
479
|
+
|
|
480
|
+
const callback = vi.fn();
|
|
481
|
+
const subscriptionId = registry.subscribe('file://test.txt', callback);
|
|
482
|
+
expect(subscriptionId).toBeDefined();
|
|
483
|
+
expect(registry.getSubscriptionCount('file://test.txt')).toBe(1);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should get stats', () => {
|
|
487
|
+
const { resource, handler } = createTextResource(
|
|
488
|
+
'file://test.txt',
|
|
489
|
+
'Test File',
|
|
490
|
+
'Hello World'
|
|
491
|
+
);
|
|
492
|
+
registry.registerResource(resource, handler);
|
|
493
|
+
|
|
494
|
+
const stats = registry.getStats();
|
|
495
|
+
expect(stats.totalResources).toBe(1);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
describe('PromptRegistry (MCP 2025-11-25)', () => {
|
|
500
|
+
let registry: ReturnType<typeof createPromptRegistry>;
|
|
501
|
+
let logger: ILogger;
|
|
502
|
+
|
|
503
|
+
beforeEach(() => {
|
|
504
|
+
logger = createMockLogger();
|
|
505
|
+
registry = createPromptRegistry(logger);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should register a prompt', () => {
|
|
509
|
+
const prompt = definePrompt(
|
|
510
|
+
'code_review',
|
|
511
|
+
'Review code for quality',
|
|
512
|
+
async (args) => [textMessage('user', `Review: ${args.code}`)],
|
|
513
|
+
{ arguments: [{ name: 'code', required: true }] }
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const result = registry.register(prompt);
|
|
517
|
+
expect(result).toBe(true);
|
|
518
|
+
expect(registry.hasPrompt('code_review')).toBe(true);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should list prompts with pagination', () => {
|
|
522
|
+
for (let i = 0; i < 5; i++) {
|
|
523
|
+
const prompt = definePrompt(
|
|
524
|
+
`prompt_${i}`,
|
|
525
|
+
`Prompt ${i}`,
|
|
526
|
+
async () => [textMessage('user', `Message ${i}`)]
|
|
527
|
+
);
|
|
528
|
+
registry.register(prompt);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const result = registry.list(undefined, 3);
|
|
532
|
+
expect(result.prompts.length).toBe(3);
|
|
533
|
+
expect(result.nextCursor).toBeDefined();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('should get prompt with arguments', async () => {
|
|
537
|
+
const prompt = definePrompt(
|
|
538
|
+
'greeting',
|
|
539
|
+
'Greet a user',
|
|
540
|
+
async (args) => [textMessage('user', `Hello, ${args.name}!`)],
|
|
541
|
+
{ arguments: [{ name: 'name', required: true }] }
|
|
542
|
+
);
|
|
543
|
+
registry.register(prompt);
|
|
544
|
+
|
|
545
|
+
const result = await registry.get('greeting', { name: 'World' });
|
|
546
|
+
expect(result.messages[0].content.type).toBe('text');
|
|
547
|
+
expect((result.messages[0].content as any).text).toBe('Hello, World!');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should validate required arguments', async () => {
|
|
551
|
+
const prompt = definePrompt(
|
|
552
|
+
'greeting',
|
|
553
|
+
'Greet a user',
|
|
554
|
+
async (args) => [textMessage('user', `Hello, ${args.name}!`)],
|
|
555
|
+
{ arguments: [{ name: 'name', required: true }] }
|
|
556
|
+
);
|
|
557
|
+
registry.register(prompt);
|
|
558
|
+
|
|
559
|
+
await expect(registry.get('greeting', {})).rejects.toThrow('Missing required argument');
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should interpolate template strings', () => {
|
|
563
|
+
const template = 'Hello, {name}! Your score is {score}.';
|
|
564
|
+
const result = interpolate(template, { name: 'Alice', score: '100' });
|
|
565
|
+
expect(result).toBe('Hello, Alice! Your score is 100.');
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
describe('TaskManager (MCP 2025-11-25)', () => {
|
|
570
|
+
let taskManager: ReturnType<typeof createTaskManager>;
|
|
571
|
+
let logger: ILogger;
|
|
572
|
+
|
|
573
|
+
beforeEach(() => {
|
|
574
|
+
logger = createMockLogger();
|
|
575
|
+
taskManager = createTaskManager(logger, { cleanupInterval: 60000 });
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
afterEach(() => {
|
|
579
|
+
taskManager.destroy();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('should create a task', () => {
|
|
583
|
+
const taskId = taskManager.createTask(async (reportProgress, signal) => {
|
|
584
|
+
return { result: 'success' };
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
expect(taskId).toBeDefined();
|
|
588
|
+
expect(taskId.startsWith('task-')).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should get task status', () => {
|
|
592
|
+
const taskId = taskManager.createTask(async () => 'done');
|
|
593
|
+
const status = taskManager.getTask(taskId);
|
|
594
|
+
|
|
595
|
+
expect(status).toBeDefined();
|
|
596
|
+
expect(status?.taskId).toBe(taskId);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should track task progress', async () => {
|
|
600
|
+
const taskId = taskManager.createTask(async (reportProgress) => {
|
|
601
|
+
reportProgress({ progress: 50, total: 100, message: 'Halfway' });
|
|
602
|
+
return 'done';
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Wait for task to complete
|
|
606
|
+
const result = await taskManager.waitForTask(taskId, 5000);
|
|
607
|
+
expect(result.state).toBe('completed');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('should cancel a task', () => {
|
|
611
|
+
const taskId = taskManager.createTask(async (reportProgress, signal) => {
|
|
612
|
+
// Long running task
|
|
613
|
+
await new Promise((resolve, reject) => {
|
|
614
|
+
const timeout = setTimeout(resolve, 10000);
|
|
615
|
+
signal.addEventListener('abort', () => {
|
|
616
|
+
clearTimeout(timeout);
|
|
617
|
+
reject(new Error('Cancelled'));
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const cancelled = taskManager.cancelTask(taskId, 'User requested');
|
|
623
|
+
expect(cancelled).toBe(true);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should get all tasks', () => {
|
|
627
|
+
taskManager.createTask(async () => 'task1');
|
|
628
|
+
taskManager.createTask(async () => 'task2');
|
|
629
|
+
|
|
630
|
+
const tasks = taskManager.getAllTasks();
|
|
631
|
+
expect(tasks.length).toBe(2);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('should get stats', () => {
|
|
635
|
+
taskManager.createTask(async () => 'done');
|
|
636
|
+
|
|
637
|
+
const stats = taskManager.getStats();
|
|
638
|
+
expect(stats.totalTasks).toBeGreaterThan(0);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @claude-flow/mcp - Connection Pool
|
|
3
|
+
*
|
|
4
|
+
* High-performance connection pooling
|
|
5
|
+
*/
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import type { PooledConnection, ConnectionPoolStats, ConnectionPoolConfig, IConnectionPool, ILogger, TransportType } from './types.js';
|
|
8
|
+
export declare class ConnectionPool extends EventEmitter implements IConnectionPool {
|
|
9
|
+
private readonly logger;
|
|
10
|
+
private readonly transportType;
|
|
11
|
+
private readonly config;
|
|
12
|
+
private readonly connections;
|
|
13
|
+
private readonly waitingClients;
|
|
14
|
+
private evictionTimer?;
|
|
15
|
+
private connectionCounter;
|
|
16
|
+
private isShuttingDown;
|
|
17
|
+
private stats;
|
|
18
|
+
constructor(config: Partial<ConnectionPoolConfig> | undefined, logger: ILogger, transportType?: TransportType);
|
|
19
|
+
private initializeMinConnections;
|
|
20
|
+
private createConnection;
|
|
21
|
+
acquire(): Promise<PooledConnection>;
|
|
22
|
+
private waitForConnection;
|
|
23
|
+
release(connection: PooledConnection): void;
|
|
24
|
+
destroy(connection: PooledConnection): void;
|
|
25
|
+
getStats(): ConnectionPoolStats;
|
|
26
|
+
drain(): Promise<void>;
|
|
27
|
+
clear(): Promise<void>;
|
|
28
|
+
private startEvictionTimer;
|
|
29
|
+
private stopEvictionTimer;
|
|
30
|
+
private evictIdleConnections;
|
|
31
|
+
private recordAcquireTime;
|
|
32
|
+
getConnections(): PooledConnection[];
|
|
33
|
+
isHealthy(): boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function createConnectionPool(config: Partial<ConnectionPoolConfig> | undefined, logger: ILogger, transportType?: TransportType): ConnectionPool;
|
|
36
|
+
//# sourceMappingURL=connection-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-pool.d.ts","sourceRoot":"","sources":["../src/connection-pool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACnB,oBAAoB,EAEpB,eAAe,EACf,OAAO,EACP,aAAa,EACd,MAAM,YAAY,CAAC;AAoDpB,qBAAa,cAAe,SAAQ,YAAa,YAAW,eAAe;IAmBvE,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAnBhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6C;IACzE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAuB;IACtD,OAAO,CAAC,aAAa,CAAC,CAAiB;IACvC,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,cAAc,CAAkB;IAExC,OAAO,CAAC,KAAK,CAOX;gBAGA,MAAM,EAAE,OAAO,CAAC,oBAAoB,CAAC,YAAK,EACzB,MAAM,EAAE,OAAO,EACf,aAAa,GAAE,aAA4B;YAQhD,wBAAwB;YAWxB,gBAAgB;IAaxB,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAiC1C,OAAO,CAAC,iBAAiB;IA4BzB,OAAO,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI;IAuB3C,OAAO,CAAC,UAAU,EAAE,gBAAgB,GAAG,IAAI;IAoB3C,QAAQ,IAAI,mBAAmB;IAwBzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BtB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,OAAO,CAAC,kBAAkB;IAM1B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,oBAAoB;IAwB5B,OAAO,CAAC,iBAAiB;IAMzB,cAAc,IAAI,gBAAgB,EAAE;IAIpC,SAAS,IAAI,OAAO;CAGrB;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,CAAC,oBAAoB,CAAC,YAAK,EAC1C,MAAM,EAAE,OAAO,EACf,aAAa,GAAE,aAA4B,GAC1C,cAAc,CAEhB"}
|