@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.
Files changed (92) hide show
  1. package/.agentic-flow/intelligence.json +16 -0
  2. package/README.md +428 -0
  3. package/__tests__/integration.test.ts +449 -0
  4. package/__tests__/mcp.test.ts +641 -0
  5. package/dist/connection-pool.d.ts +36 -0
  6. package/dist/connection-pool.d.ts.map +1 -0
  7. package/dist/connection-pool.js +273 -0
  8. package/dist/connection-pool.js.map +1 -0
  9. package/dist/index.d.ts +75 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +85 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/oauth.d.ts +146 -0
  14. package/dist/oauth.d.ts.map +1 -0
  15. package/dist/oauth.js +318 -0
  16. package/dist/oauth.js.map +1 -0
  17. package/dist/prompt-registry.d.ts +90 -0
  18. package/dist/prompt-registry.d.ts.map +1 -0
  19. package/dist/prompt-registry.js +209 -0
  20. package/dist/prompt-registry.js.map +1 -0
  21. package/dist/rate-limiter.d.ts +86 -0
  22. package/dist/rate-limiter.d.ts.map +1 -0
  23. package/dist/rate-limiter.js +197 -0
  24. package/dist/rate-limiter.js.map +1 -0
  25. package/dist/resource-registry.d.ts +144 -0
  26. package/dist/resource-registry.d.ts.map +1 -0
  27. package/dist/resource-registry.js +405 -0
  28. package/dist/resource-registry.js.map +1 -0
  29. package/dist/sampling.d.ts +102 -0
  30. package/dist/sampling.d.ts.map +1 -0
  31. package/dist/sampling.js +268 -0
  32. package/dist/sampling.js.map +1 -0
  33. package/dist/schema-validator.d.ts +30 -0
  34. package/dist/schema-validator.d.ts.map +1 -0
  35. package/dist/schema-validator.js +182 -0
  36. package/dist/schema-validator.js.map +1 -0
  37. package/dist/server.d.ts +122 -0
  38. package/dist/server.d.ts.map +1 -0
  39. package/dist/server.js +829 -0
  40. package/dist/server.js.map +1 -0
  41. package/dist/session-manager.d.ts +55 -0
  42. package/dist/session-manager.d.ts.map +1 -0
  43. package/dist/session-manager.js +252 -0
  44. package/dist/session-manager.js.map +1 -0
  45. package/dist/task-manager.d.ts +81 -0
  46. package/dist/task-manager.d.ts.map +1 -0
  47. package/dist/task-manager.js +337 -0
  48. package/dist/task-manager.js.map +1 -0
  49. package/dist/tool-registry.d.ts +88 -0
  50. package/dist/tool-registry.d.ts.map +1 -0
  51. package/dist/tool-registry.js +353 -0
  52. package/dist/tool-registry.js.map +1 -0
  53. package/dist/transport/http.d.ts +55 -0
  54. package/dist/transport/http.d.ts.map +1 -0
  55. package/dist/transport/http.js +446 -0
  56. package/dist/transport/http.js.map +1 -0
  57. package/dist/transport/index.d.ts +50 -0
  58. package/dist/transport/index.d.ts.map +1 -0
  59. package/dist/transport/index.js +181 -0
  60. package/dist/transport/index.js.map +1 -0
  61. package/dist/transport/stdio.d.ts +43 -0
  62. package/dist/transport/stdio.d.ts.map +1 -0
  63. package/dist/transport/stdio.js +194 -0
  64. package/dist/transport/stdio.js.map +1 -0
  65. package/dist/transport/websocket.d.ts +65 -0
  66. package/dist/transport/websocket.d.ts.map +1 -0
  67. package/dist/transport/websocket.js +314 -0
  68. package/dist/transport/websocket.js.map +1 -0
  69. package/dist/types.d.ts +473 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +40 -0
  72. package/dist/types.js.map +1 -0
  73. package/package.json +42 -0
  74. package/src/connection-pool.ts +344 -0
  75. package/src/index.ts +253 -0
  76. package/src/oauth.ts +447 -0
  77. package/src/prompt-registry.ts +296 -0
  78. package/src/rate-limiter.ts +266 -0
  79. package/src/resource-registry.ts +530 -0
  80. package/src/sampling.ts +363 -0
  81. package/src/schema-validator.ts +213 -0
  82. package/src/server.ts +1134 -0
  83. package/src/session-manager.ts +339 -0
  84. package/src/task-manager.ts +427 -0
  85. package/src/tool-registry.ts +475 -0
  86. package/src/transport/http.ts +532 -0
  87. package/src/transport/index.ts +233 -0
  88. package/src/transport/stdio.ts +252 -0
  89. package/src/transport/websocket.ts +396 -0
  90. package/src/types.ts +664 -0
  91. package/tsconfig.json +20 -0
  92. package/vitest.config.ts +13 -0
@@ -0,0 +1,530 @@
1
+ /**
2
+ * @claude-flow/mcp - Resource Registry
3
+ *
4
+ * MCP 2025-11-25 compliant resource management
5
+ * Supports: list, read, subscribe, templates, pagination
6
+ */
7
+
8
+ import { EventEmitter } from 'events';
9
+ import type {
10
+ MCPResource,
11
+ ResourceContent,
12
+ ResourceTemplate,
13
+ ResourceListResult,
14
+ ResourceReadResult,
15
+ ILogger,
16
+ ContentAnnotations,
17
+ } from './types.js';
18
+
19
+ export type ResourceHandler = (uri: string) => Promise<ResourceContent[]>;
20
+ export type SubscriptionCallback = (uri: string, content: ResourceContent[]) => void;
21
+
22
+ export interface ResourceRegistryOptions {
23
+ enableSubscriptions?: boolean;
24
+ maxSubscriptionsPerResource?: number;
25
+ cacheEnabled?: boolean;
26
+ cacheTTL?: number;
27
+ maxCacheSize?: number; // SECURITY: Prevent unbounded cache growth
28
+ }
29
+
30
+ interface CachedResource {
31
+ content: ResourceContent[];
32
+ cachedAt: number;
33
+ ttl: number;
34
+ }
35
+
36
+ interface Subscription {
37
+ id: string;
38
+ uri: string;
39
+ callback: SubscriptionCallback;
40
+ createdAt: Date;
41
+ }
42
+
43
+ export class ResourceRegistry extends EventEmitter {
44
+ private resources: Map<string, MCPResource> = new Map();
45
+ private templates: Map<string, ResourceTemplate> = new Map();
46
+ private handlers: Map<string, ResourceHandler> = new Map();
47
+ private subscriptions: Map<string, Subscription[]> = new Map();
48
+ private cache: Map<string, CachedResource> = new Map();
49
+ private subscriptionCounter = 0;
50
+
51
+ private readonly options: Required<ResourceRegistryOptions>;
52
+
53
+ constructor(
54
+ private readonly logger: ILogger,
55
+ options: ResourceRegistryOptions = {}
56
+ ) {
57
+ super();
58
+ this.options = {
59
+ enableSubscriptions: options.enableSubscriptions ?? true,
60
+ maxSubscriptionsPerResource: options.maxSubscriptionsPerResource ?? 100,
61
+ cacheEnabled: options.cacheEnabled ?? true,
62
+ cacheTTL: options.cacheTTL ?? 60000, // 1 minute default
63
+ maxCacheSize: options.maxCacheSize ?? 1000, // SECURITY: Default max 1000 entries
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Register a static resource
69
+ */
70
+ registerResource(resource: MCPResource, handler: ResourceHandler): boolean {
71
+ if (this.resources.has(resource.uri)) {
72
+ this.logger.warn('Resource already registered', { uri: resource.uri });
73
+ return false;
74
+ }
75
+
76
+ this.resources.set(resource.uri, resource);
77
+ this.handlers.set(resource.uri, handler);
78
+
79
+ this.logger.debug('Resource registered', { uri: resource.uri, name: resource.name });
80
+ this.emit('resource:registered', { uri: resource.uri });
81
+ this.emitListChanged();
82
+
83
+ return true;
84
+ }
85
+
86
+ /**
87
+ * Register a resource template (dynamic URIs)
88
+ */
89
+ registerTemplate(template: ResourceTemplate, handler: ResourceHandler): boolean {
90
+ if (this.templates.has(template.uriTemplate)) {
91
+ this.logger.warn('Template already registered', { template: template.uriTemplate });
92
+ return false;
93
+ }
94
+
95
+ this.templates.set(template.uriTemplate, template);
96
+ this.handlers.set(template.uriTemplate, handler);
97
+
98
+ this.logger.debug('Resource template registered', { template: template.uriTemplate });
99
+ this.emit('template:registered', { template: template.uriTemplate });
100
+
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * Unregister a resource
106
+ */
107
+ unregisterResource(uri: string): boolean {
108
+ if (!this.resources.has(uri)) {
109
+ return false;
110
+ }
111
+
112
+ this.resources.delete(uri);
113
+ this.handlers.delete(uri);
114
+ this.cache.delete(uri);
115
+
116
+ // Cancel subscriptions for this resource
117
+ const subs = this.subscriptions.get(uri) || [];
118
+ for (const sub of subs) {
119
+ this.emit('subscription:cancelled', { subscriptionId: sub.id, uri });
120
+ }
121
+ this.subscriptions.delete(uri);
122
+
123
+ this.logger.debug('Resource unregistered', { uri });
124
+ this.emit('resource:unregistered', { uri });
125
+ this.emitListChanged();
126
+
127
+ return true;
128
+ }
129
+
130
+ /**
131
+ * List resources with pagination
132
+ */
133
+ list(cursor?: string, pageSize: number = 50): ResourceListResult {
134
+ const allResources = Array.from(this.resources.values());
135
+
136
+ let startIndex = 0;
137
+ if (cursor) {
138
+ const decoded = this.decodeCursor(cursor);
139
+ startIndex = decoded.offset;
140
+ }
141
+
142
+ const endIndex = Math.min(startIndex + pageSize, allResources.length);
143
+ const resources = allResources.slice(startIndex, endIndex);
144
+
145
+ const result: ResourceListResult = { resources };
146
+
147
+ if (endIndex < allResources.length) {
148
+ result.nextCursor = this.encodeCursor({ offset: endIndex });
149
+ }
150
+
151
+ return result;
152
+ }
153
+
154
+ /**
155
+ * Read resource content
156
+ */
157
+ async read(uri: string): Promise<ResourceReadResult> {
158
+ // Check cache first
159
+ if (this.options.cacheEnabled) {
160
+ const cached = this.cache.get(uri);
161
+ if (cached && Date.now() - cached.cachedAt < cached.ttl) {
162
+ this.logger.debug('Resource cache hit', { uri });
163
+ return { contents: cached.content };
164
+ }
165
+ }
166
+
167
+ // Find handler (exact match or template match)
168
+ let handler = this.handlers.get(uri);
169
+ if (!handler) {
170
+ handler = this.findTemplateHandler(uri);
171
+ }
172
+
173
+ if (!handler) {
174
+ throw new Error(`Resource not found: ${uri}`);
175
+ }
176
+
177
+ const contents = await handler(uri);
178
+
179
+ // Cache the result with size limit (LRU eviction)
180
+ if (this.options.cacheEnabled) {
181
+ // SECURITY: Enforce max cache size to prevent memory exhaustion
182
+ if (this.cache.size >= this.options.maxCacheSize) {
183
+ // Remove oldest entry (first entry in Map iteration order)
184
+ const oldestKey = this.cache.keys().next().value;
185
+ if (oldestKey) {
186
+ this.cache.delete(oldestKey);
187
+ this.logger.debug('Cache evicted oldest entry', { uri: oldestKey });
188
+ }
189
+ }
190
+
191
+ this.cache.set(uri, {
192
+ content: contents,
193
+ cachedAt: Date.now(),
194
+ ttl: this.options.cacheTTL,
195
+ });
196
+ }
197
+
198
+ this.emit('resource:read', { uri, contentCount: contents.length });
199
+ return { contents };
200
+ }
201
+
202
+ /**
203
+ * Subscribe to resource updates
204
+ */
205
+ subscribe(uri: string, callback: SubscriptionCallback): string {
206
+ if (!this.options.enableSubscriptions) {
207
+ throw new Error('Subscriptions are disabled');
208
+ }
209
+
210
+ const existingSubs = this.subscriptions.get(uri) || [];
211
+ if (existingSubs.length >= this.options.maxSubscriptionsPerResource) {
212
+ throw new Error(`Maximum subscriptions reached for resource: ${uri}`);
213
+ }
214
+
215
+ const subscriptionId = `sub-${++this.subscriptionCounter}-${Date.now()}`;
216
+ const subscription: Subscription = {
217
+ id: subscriptionId,
218
+ uri,
219
+ callback,
220
+ createdAt: new Date(),
221
+ };
222
+
223
+ existingSubs.push(subscription);
224
+ this.subscriptions.set(uri, existingSubs);
225
+
226
+ this.logger.debug('Subscription created', { subscriptionId, uri });
227
+ this.emit('subscription:created', { subscriptionId, uri });
228
+
229
+ return subscriptionId;
230
+ }
231
+
232
+ /**
233
+ * Unsubscribe from resource updates
234
+ */
235
+ unsubscribe(subscriptionId: string): boolean {
236
+ for (const [uri, subs] of this.subscriptions) {
237
+ const index = subs.findIndex((s) => s.id === subscriptionId);
238
+ if (index !== -1) {
239
+ subs.splice(index, 1);
240
+ if (subs.length === 0) {
241
+ this.subscriptions.delete(uri);
242
+ }
243
+ this.logger.debug('Subscription removed', { subscriptionId, uri });
244
+ this.emit('subscription:removed', { subscriptionId, uri });
245
+ return true;
246
+ }
247
+ }
248
+ return false;
249
+ }
250
+
251
+ /**
252
+ * Notify subscribers of resource update
253
+ */
254
+ async notifyUpdate(uri: string): Promise<void> {
255
+ const subs = this.subscriptions.get(uri);
256
+ if (!subs || subs.length === 0) {
257
+ return;
258
+ }
259
+
260
+ // Invalidate cache
261
+ this.cache.delete(uri);
262
+
263
+ // Read fresh content
264
+ const { contents } = await this.read(uri);
265
+
266
+ // Notify all subscribers
267
+ for (const sub of subs) {
268
+ try {
269
+ sub.callback(uri, contents);
270
+ } catch (error) {
271
+ this.logger.error('Subscription callback error', { subscriptionId: sub.id, error });
272
+ }
273
+ }
274
+
275
+ this.emit('resource:updated', { uri, subscriberCount: subs.length });
276
+ }
277
+
278
+ /**
279
+ * Get resource by URI
280
+ */
281
+ getResource(uri: string): MCPResource | undefined {
282
+ return this.resources.get(uri);
283
+ }
284
+
285
+ /**
286
+ * Check if resource exists
287
+ */
288
+ hasResource(uri: string): boolean {
289
+ return this.resources.has(uri) || this.matchesTemplate(uri);
290
+ }
291
+
292
+ /**
293
+ * Get resource count
294
+ */
295
+ getResourceCount(): number {
296
+ return this.resources.size;
297
+ }
298
+
299
+ /**
300
+ * Get all templates
301
+ */
302
+ getTemplates(): ResourceTemplate[] {
303
+ return Array.from(this.templates.values());
304
+ }
305
+
306
+ /**
307
+ * Get subscription count for a resource
308
+ */
309
+ getSubscriptionCount(uri: string): number {
310
+ return this.subscriptions.get(uri)?.length || 0;
311
+ }
312
+
313
+ /**
314
+ * Get stats
315
+ */
316
+ getStats(): {
317
+ totalResources: number;
318
+ totalTemplates: number;
319
+ totalSubscriptions: number;
320
+ cacheSize: number;
321
+ } {
322
+ let totalSubscriptions = 0;
323
+ for (const subs of this.subscriptions.values()) {
324
+ totalSubscriptions += subs.length;
325
+ }
326
+
327
+ return {
328
+ totalResources: this.resources.size,
329
+ totalTemplates: this.templates.size,
330
+ totalSubscriptions,
331
+ cacheSize: this.cache.size,
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Clear cache
337
+ */
338
+ clearCache(): void {
339
+ this.cache.clear();
340
+ this.logger.debug('Resource cache cleared');
341
+ }
342
+
343
+ /**
344
+ * Find handler for template URI
345
+ */
346
+ private findTemplateHandler(uri: string): ResourceHandler | undefined {
347
+ for (const [template, handler] of this.handlers) {
348
+ if (this.matchesTemplate(uri, template)) {
349
+ return handler;
350
+ }
351
+ }
352
+ return undefined;
353
+ }
354
+
355
+ /**
356
+ * Escape regex metacharacters to prevent ReDoS attacks
357
+ * SECURITY: Critical for preventing regex denial of service
358
+ */
359
+ private escapeRegex(str: string): string {
360
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
361
+ }
362
+
363
+ /**
364
+ * Check if URI matches any template
365
+ * SECURITY: Uses escaped regex to prevent ReDoS
366
+ */
367
+ private matchesTemplate(uri: string, template?: string): boolean {
368
+ if (template) {
369
+ // SECURITY: Escape regex metacharacters before converting template
370
+ // First extract placeholders, escape the rest, then add placeholder pattern
371
+ const escaped = this.escapeRegex(template);
372
+ // Replace escaped placeholder braces with the pattern
373
+ const pattern = escaped.replace(/\\\{[^}]+\\\}/g, '[^/]+');
374
+ try {
375
+ const regex = new RegExp('^' + pattern + '$');
376
+ return regex.test(uri);
377
+ } catch {
378
+ // Invalid regex pattern - return false safely
379
+ return false;
380
+ }
381
+ }
382
+
383
+ for (const t of this.templates.keys()) {
384
+ const escaped = this.escapeRegex(t);
385
+ const pattern = escaped.replace(/\\\{[^}]+\\\}/g, '[^/]+');
386
+ try {
387
+ const regex = new RegExp('^' + pattern + '$');
388
+ if (regex.test(uri)) {
389
+ return true;
390
+ }
391
+ } catch {
392
+ // Skip invalid patterns
393
+ continue;
394
+ }
395
+ }
396
+ return false;
397
+ }
398
+
399
+ /**
400
+ * Encode cursor for pagination
401
+ */
402
+ private encodeCursor(data: { offset: number }): string {
403
+ return Buffer.from(JSON.stringify(data)).toString('base64');
404
+ }
405
+
406
+ /**
407
+ * Decode cursor for pagination
408
+ */
409
+ private decodeCursor(cursor: string): { offset: number } {
410
+ try {
411
+ return JSON.parse(Buffer.from(cursor, 'base64').toString('utf-8'));
412
+ } catch {
413
+ return { offset: 0 };
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Emit listChanged notification
419
+ */
420
+ private emitListChanged(): void {
421
+ this.emit('resources:listChanged');
422
+ }
423
+ }
424
+
425
+ export function createResourceRegistry(
426
+ logger: ILogger,
427
+ options?: ResourceRegistryOptions
428
+ ): ResourceRegistry {
429
+ return new ResourceRegistry(logger, options);
430
+ }
431
+
432
+ /**
433
+ * Helper to create a static text resource
434
+ */
435
+ export function createTextResource(
436
+ uri: string,
437
+ name: string,
438
+ text: string,
439
+ options?: {
440
+ description?: string;
441
+ mimeType?: string;
442
+ annotations?: ContentAnnotations;
443
+ }
444
+ ): { resource: MCPResource; handler: ResourceHandler } {
445
+ const resource: MCPResource = {
446
+ uri,
447
+ name,
448
+ description: options?.description,
449
+ mimeType: options?.mimeType || 'text/plain',
450
+ annotations: options?.annotations,
451
+ };
452
+
453
+ const handler: ResourceHandler = async () => [
454
+ {
455
+ uri,
456
+ mimeType: options?.mimeType || 'text/plain',
457
+ text,
458
+ },
459
+ ];
460
+
461
+ return { resource, handler };
462
+ }
463
+
464
+ /**
465
+ * Helper to create a file resource
466
+ * SECURITY: Validates path to prevent path traversal attacks
467
+ */
468
+ export function createFileResource(
469
+ uri: string,
470
+ name: string,
471
+ filePath: string,
472
+ options?: {
473
+ description?: string;
474
+ mimeType?: string;
475
+ allowedBasePaths?: string[]; // Security: restrict to these base paths
476
+ }
477
+ ): { resource: MCPResource; handler: ResourceHandler } {
478
+ const resource: MCPResource = {
479
+ uri,
480
+ name,
481
+ description: options?.description,
482
+ mimeType: options?.mimeType || 'application/octet-stream',
483
+ };
484
+
485
+ const handler: ResourceHandler = async () => {
486
+ const fs = await import('fs/promises');
487
+ const path = await import('path');
488
+
489
+ // SECURITY: Normalize and validate the path
490
+ const normalizedPath = path.normalize(filePath);
491
+
492
+ // Prevent path traversal
493
+ if (normalizedPath.includes('..') || normalizedPath.includes('\0')) {
494
+ throw new Error('Invalid file path: path traversal detected');
495
+ }
496
+
497
+ // Prevent access to sensitive system paths
498
+ const blockedPaths = ['/etc/', '/proc/', '/sys/', '/dev/', '/root/', '/var/log/'];
499
+ const lowerPath = normalizedPath.toLowerCase();
500
+ for (const blocked of blockedPaths) {
501
+ if (lowerPath.startsWith(blocked) || lowerPath.includes('/.')) {
502
+ throw new Error('Access to system paths is not allowed');
503
+ }
504
+ }
505
+
506
+ // If allowedBasePaths specified, validate against them
507
+ if (options?.allowedBasePaths && options.allowedBasePaths.length > 0) {
508
+ const resolvedPath = path.resolve(normalizedPath);
509
+ const isAllowed = options.allowedBasePaths.some((basePath) => {
510
+ const resolvedBase = path.resolve(basePath);
511
+ return resolvedPath.startsWith(resolvedBase);
512
+ });
513
+
514
+ if (!isAllowed) {
515
+ throw new Error('File path is outside allowed directories');
516
+ }
517
+ }
518
+
519
+ const content = await fs.readFile(normalizedPath);
520
+ return [
521
+ {
522
+ uri,
523
+ mimeType: options?.mimeType || 'application/octet-stream',
524
+ blob: content.toString('base64'),
525
+ },
526
+ ];
527
+ };
528
+
529
+ return { resource, handler };
530
+ }