@wundr.io/mcp-registry 1.0.3

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 ADDED
@@ -0,0 +1,890 @@
1
+ # @wundr.io/mcp-registry
2
+
3
+ MCP Server Registry and Discovery with Super MCP aggregator pattern for unified tool routing across multiple MCP servers.
4
+
5
+ ## Overview
6
+
7
+ The `@wundr.io/mcp-registry` package provides a comprehensive solution for managing multiple MCP (Model Context Protocol) servers. It implements the **Super MCP aggregator pattern**, which acts as a unified entry point for tool invocations, automatically routing requests to the appropriate servers based on capabilities, health status, and configurable routing strategies.
8
+
9
+ ### Key Features
10
+
11
+ - **Server Registration & Lifecycle Management** - Register, update, and unregister MCP servers with full capability tracking
12
+ - **Capability-Based Discovery** - Find servers by tools, capabilities, tags, and health status
13
+ - **Tool Aggregation** - Unified tool routing across multiple MCP servers with intelligent server selection
14
+ - **Health Monitoring** - Continuous health checks with automatic status updates
15
+ - **Circuit Breaker Pattern** - Fault tolerance with automatic recovery
16
+ - **Multiple Routing Strategies** - Priority, round-robin, least-latency, random, and health-aware routing
17
+ - **Event-Driven Architecture** - Subscribe to registry, aggregator, and health events
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @wundr.io/mcp-registry
23
+ ```
24
+
25
+ ### Peer Dependencies
26
+
27
+ ```bash
28
+ npm install @wundr.io/mcp-server # Optional: for direct server integration
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import {
35
+ MCPServerRegistry,
36
+ MCPAggregator,
37
+ ServerHealthMonitor,
38
+ createServerDiscoveryService,
39
+ } from '@wundr.io/mcp-registry';
40
+
41
+ // Create registry
42
+ const registry = new MCPServerRegistry();
43
+
44
+ // Register servers
45
+ await registry.register({
46
+ name: 'wundr-mcp',
47
+ version: '1.0.0',
48
+ transport: { type: 'stdio', command: 'npx', args: ['@wundr.io/mcp-server'] },
49
+ });
50
+
51
+ // Create aggregator
52
+ const aggregator = new MCPAggregator(registry, {
53
+ defaultStrategy: 'health-aware',
54
+ enableRetries: true,
55
+ });
56
+
57
+ // Start health monitoring
58
+ const monitor = new ServerHealthMonitor(registry);
59
+ await monitor.start();
60
+
61
+ // Invoke tools
62
+ const response = await aggregator.invoke({
63
+ name: 'drift_detection',
64
+ arguments: { action: 'detect' },
65
+ });
66
+ ```
67
+
68
+ ### Using the Factory Function
69
+
70
+ For a simpler setup, use the `createMCPRegistrySystem` factory:
71
+
72
+ ```typescript
73
+ import { createMCPRegistrySystem } from '@wundr.io/mcp-registry';
74
+
75
+ const { registry, discovery, aggregator, monitor } = await createMCPRegistrySystem({
76
+ aggregator: { defaultStrategy: 'health-aware' },
77
+ monitor: { checkInterval: 10000 },
78
+ });
79
+
80
+ await monitor.start();
81
+ ```
82
+
83
+ ## API Reference
84
+
85
+ ### MCPServerRegistry
86
+
87
+ The core registry class for managing MCP server registrations.
88
+
89
+ #### Server Registration
90
+
91
+ ```typescript
92
+ const registry = new MCPServerRegistry();
93
+
94
+ // Register a server
95
+ const server = await registry.register({
96
+ name: 'my-mcp-server',
97
+ version: '1.0.0',
98
+ description: 'My custom MCP server',
99
+ transport: {
100
+ type: 'stdio',
101
+ command: 'node',
102
+ args: ['server.js'],
103
+ env: { NODE_ENV: 'production' },
104
+ cwd: '/path/to/server',
105
+ timeout: 30000,
106
+ autoReconnect: true,
107
+ },
108
+ priority: 10, // Higher = preferred
109
+ tags: ['production', 'tools'],
110
+ metadata: { owner: 'team-a' },
111
+ });
112
+
113
+ // Unregister a server
114
+ await registry.unregister(server.id);
115
+ ```
116
+
117
+ #### Server Discovery
118
+
119
+ ```typescript
120
+ // Get server by ID
121
+ const server = registry.get(serverId);
122
+
123
+ // Get server by name
124
+ const server = registry.getByName('my-mcp-server');
125
+
126
+ // Get all servers
127
+ const allServers = registry.getAll();
128
+
129
+ // Find by capability category
130
+ const toolServers = registry.findByCapability('tools');
131
+
132
+ // Find by tool name
133
+ const driftServers = registry.findByTool('drift_detection');
134
+
135
+ // Find by tag
136
+ const productionServers = registry.findByTag('production');
137
+
138
+ // Find by health status
139
+ const healthyServers = registry.findByHealthStatus('healthy');
140
+
141
+ // Get all tool names
142
+ const toolNames = registry.getAllToolNames();
143
+
144
+ // Get all tags
145
+ const tags = registry.getAllTags();
146
+ ```
147
+
148
+ #### Capability Management
149
+
150
+ ```typescript
151
+ // Update server capabilities
152
+ await registry.updateCapabilities(serverId, [
153
+ { category: 'tools', name: 'drift_detection', enabled: true },
154
+ { category: 'resources', name: 'config-files', enabled: true },
155
+ ]);
156
+
157
+ // Update server tools
158
+ await registry.updateTools(serverId, [
159
+ {
160
+ name: 'drift_detection',
161
+ description: 'Monitor code quality drift',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ action: { type: 'string', enum: ['detect', 'baseline'] },
166
+ },
167
+ required: ['action'],
168
+ },
169
+ category: 'governance',
170
+ tags: ['quality', 'monitoring'],
171
+ },
172
+ ]);
173
+
174
+ // Update server resources
175
+ await registry.updateResources(serverId, [
176
+ {
177
+ uri: 'file:///config/*.json',
178
+ name: 'Configuration Files',
179
+ mimeType: 'application/json',
180
+ subscribable: true,
181
+ },
182
+ ]);
183
+
184
+ // Update server prompts
185
+ await registry.updatePrompts(serverId, [
186
+ {
187
+ name: 'code-review',
188
+ description: 'Generate code review comments',
189
+ arguments: [{ name: 'diff', description: 'Git diff to review', required: true }],
190
+ },
191
+ ]);
192
+ ```
193
+
194
+ #### Health Status
195
+
196
+ ```typescript
197
+ // Get health status for a server
198
+ const health = registry.getHealthStatus(serverId);
199
+
200
+ // Update health status
201
+ registry.updateHealthStatus(serverId, {
202
+ status: 'healthy',
203
+ connected: true,
204
+ latencyMs: 50,
205
+ });
206
+
207
+ // Get all healthy servers
208
+ const healthyServers = registry.getHealthyServers();
209
+ ```
210
+
211
+ #### Registry Events
212
+
213
+ ```typescript
214
+ registry.on('server:registered', event => {
215
+ console.log('Server registered:', event.serverId);
216
+ });
217
+
218
+ registry.on('server:unregistered', event => {
219
+ console.log('Server unregistered:', event.serverId);
220
+ });
221
+
222
+ registry.on('server:health-changed', event => {
223
+ console.log('Health changed:', event.data?.previousStatus, '->', event.data?.newStatus);
224
+ });
225
+
226
+ registry.on('tool:added', event => {
227
+ console.log('Tool added:', event.data?.toolName);
228
+ });
229
+
230
+ registry.on('tool:removed', event => {
231
+ console.log('Tool removed:', event.data?.toolName);
232
+ });
233
+ ```
234
+
235
+ #### Export/Import
236
+
237
+ ```typescript
238
+ // Export registry for persistence
239
+ const exported = registry.export();
240
+ await fs.writeFile('registry.json', JSON.stringify(exported));
241
+
242
+ // Import registry
243
+ const data = JSON.parse(await fs.readFile('registry.json', 'utf-8'));
244
+ await registry.import(data);
245
+ ```
246
+
247
+ ### MCPAggregator
248
+
249
+ The Super MCP aggregator for routing tool invocations.
250
+
251
+ #### Configuration
252
+
253
+ ```typescript
254
+ const aggregator = new MCPAggregator(registry, {
255
+ // Routing strategy: 'priority' | 'round-robin' | 'least-latency' | 'random' | 'health-aware'
256
+ defaultStrategy: 'health-aware',
257
+
258
+ // Request timeout in milliseconds
259
+ requestTimeout: 30000,
260
+
261
+ // Retry configuration
262
+ enableRetries: true,
263
+ maxRetries: 3,
264
+ retryDelay: 1000,
265
+
266
+ // Circuit breaker configuration
267
+ enableCircuitBreaker: true,
268
+ circuitBreakerThreshold: 5, // Failures before opening
269
+ circuitBreakerResetTimeout: 60000, // Time before trying again
270
+ });
271
+ ```
272
+
273
+ #### Tool Invocation
274
+
275
+ ```typescript
276
+ // Invoke with default strategy
277
+ const response = await aggregator.invoke({
278
+ name: 'drift_detection',
279
+ arguments: { action: 'detect' },
280
+ timeout: 10000, // Override default timeout
281
+ preferredServer: serverId, // Optional server preference
282
+ metadata: { requestId: 'abc-123' },
283
+ });
284
+
285
+ // Response structure
286
+ console.log(response.result); // Tool result
287
+ console.log(response.serverId); // Server that handled the request
288
+ console.log(response.latencyMs); // Request latency
289
+ console.log(response.retried); // Whether request was retried
290
+ console.log(response.retryAttempts); // Number of retry attempts
291
+ ```
292
+
293
+ #### Routing Strategies
294
+
295
+ ```typescript
296
+ // Use specific routing strategy
297
+ const response = await aggregator.invokeWithStrategy(
298
+ { name: 'my-tool', arguments: {} },
299
+ 'least-latency'
300
+ );
301
+
302
+ // Available strategies:
303
+ // - 'priority': Select server with highest priority
304
+ // - 'round-robin': Distribute requests evenly across servers
305
+ // - 'least-latency': Select server with lowest average latency
306
+ // - 'random': Random server selection
307
+ // - 'health-aware': Score-based selection considering health, latency, and priority
308
+ ```
309
+
310
+ #### Parallel and Sequential Invocation
311
+
312
+ ```typescript
313
+ // Invoke multiple tools in parallel
314
+ const responses = await aggregator.invokeParallel([
315
+ { name: 'tool-a', arguments: { param: 1 } },
316
+ { name: 'tool-b', arguments: { param: 2 } },
317
+ { name: 'tool-c', arguments: { param: 3 } },
318
+ ]);
319
+
320
+ // Invoke tools sequentially
321
+ const results = await aggregator.invokeSequential([
322
+ { name: 'step-1', arguments: {} },
323
+ { name: 'step-2', arguments: {} },
324
+ { name: 'step-3', arguments: {} },
325
+ ]);
326
+ ```
327
+
328
+ #### Direct Tool Handlers
329
+
330
+ Register local tool handlers for direct execution:
331
+
332
+ ```typescript
333
+ // Register a local tool handler
334
+ aggregator.registerToolHandler('my-local-tool', async args => {
335
+ const result = await processLocally(args);
336
+ return {
337
+ content: [{ type: 'text', text: JSON.stringify(result) }],
338
+ isError: false,
339
+ };
340
+ });
341
+
342
+ // Unregister handler
343
+ aggregator.unregisterToolHandler('my-local-tool');
344
+ ```
345
+
346
+ #### Circuit Breaker
347
+
348
+ ```typescript
349
+ // Check if circuit is open for a server
350
+ const isOpen = aggregator.isCircuitOpen(serverId);
351
+
352
+ // Get circuit status
353
+ const status = aggregator.getCircuitStatus(serverId);
354
+ console.log(status.state); // 'closed' | 'open' | 'half-open'
355
+ console.log(status.failureCount);
356
+ console.log(status.lastFailure);
357
+ console.log(status.timeUntilClose);
358
+
359
+ // Get all circuit statuses
360
+ const allStatuses = aggregator.getAllCircuitStatuses();
361
+
362
+ // Manually reset a circuit
363
+ aggregator.resetCircuit(serverId);
364
+
365
+ // Manually open a circuit
366
+ aggregator.openCircuit(serverId);
367
+ ```
368
+
369
+ #### Aggregator Events
370
+
371
+ ```typescript
372
+ aggregator.on('request:started', event => {
373
+ console.log('Request started:', event.requestId, event.toolName);
374
+ });
375
+
376
+ aggregator.on('request:completed', event => {
377
+ console.log('Request completed:', event.requestId, event.durationMs, 'ms');
378
+ });
379
+
380
+ aggregator.on('request:failed', event => {
381
+ console.log('Request failed:', event.requestId, event.error);
382
+ });
383
+
384
+ aggregator.on('request:retried', event => {
385
+ console.log('Request retried:', event.requestId, 'attempt', event.retryAttempt);
386
+ });
387
+
388
+ aggregator.on('circuit:opened', event => {
389
+ console.log('Circuit opened:', event.serverId);
390
+ });
391
+
392
+ aggregator.on('circuit:closed', event => {
393
+ console.log('Circuit closed:', event.serverId);
394
+ });
395
+
396
+ aggregator.on('circuit:half-open', event => {
397
+ console.log('Circuit half-open:', event.serverId);
398
+ });
399
+ ```
400
+
401
+ ### ServerDiscoveryService
402
+
403
+ Advanced server discovery with query building and recommendations.
404
+
405
+ #### Query Builder
406
+
407
+ ```typescript
408
+ const discovery = new ServerDiscoveryService(registry);
409
+
410
+ // Build complex queries with fluent API
411
+ const query = discovery
412
+ .queryBuilder()
413
+ .withCategory('tools')
414
+ .withTools(['drift_detection', 'governance_report'])
415
+ .withTags(['production'])
416
+ .withMinPriority(5)
417
+ .withHealthStatus(['healthy', 'degraded'])
418
+ .build();
419
+
420
+ const result = await discovery.discover(query, {
421
+ limit: 10,
422
+ sortBy: 'priority',
423
+ sortOrder: 'desc',
424
+ includeUnknownHealth: false,
425
+ });
426
+
427
+ console.log(result.servers); // Matching servers
428
+ console.log(result.matchCount); // Number of matches
429
+ console.log(result.totalSearched); // Total servers searched
430
+ ```
431
+
432
+ #### Finding Servers
433
+
434
+ ```typescript
435
+ // Find best server for a specific tool
436
+ const bestServer = await discovery.findBestServerForTool('drift_detection');
437
+
438
+ // Find servers that provide ALL specified tools
439
+ const servers = await discovery.findServersForTools([
440
+ 'drift_detection',
441
+ 'governance_report',
442
+ 'test_baseline',
443
+ ]);
444
+ ```
445
+
446
+ #### Server Recommendations
447
+
448
+ ```typescript
449
+ const recommendations = await discovery.getRecommendations({
450
+ preferredTools: ['drift_detection'],
451
+ preferredTags: ['production'],
452
+ requiredCapabilities: ['governance'],
453
+ });
454
+
455
+ for (const rec of recommendations) {
456
+ console.log(rec.server.name);
457
+ console.log('Score:', rec.score);
458
+ console.log('Reasons:', rec.reasons);
459
+ console.log('Health:', rec.healthStatus);
460
+ }
461
+ ```
462
+
463
+ ### ServerHealthMonitor
464
+
465
+ Continuous health monitoring with custom checks.
466
+
467
+ #### Configuration
468
+
469
+ ```typescript
470
+ const monitor = new ServerHealthMonitor(registry, {
471
+ // Health check interval in milliseconds
472
+ checkInterval: 30000,
473
+
474
+ // Ping timeout in milliseconds
475
+ pingTimeout: 5000,
476
+
477
+ // Failures before marking unhealthy
478
+ failureThreshold: 3,
479
+
480
+ // Successes needed to recover from unhealthy
481
+ recoveryThreshold: 2,
482
+
483
+ // Latency threshold for degraded status (ms)
484
+ degradedLatencyThreshold: 1000,
485
+
486
+ // Enable automatic reconnection
487
+ autoReconnect: true,
488
+
489
+ // Maximum reconnection attempts
490
+ maxReconnectAttempts: 5,
491
+ });
492
+ ```
493
+
494
+ #### Lifecycle Management
495
+
496
+ ```typescript
497
+ // Start monitoring
498
+ await monitor.start();
499
+
500
+ // Check if active
501
+ const isActive = monitor.isActive();
502
+
503
+ // Stop monitoring
504
+ await monitor.stop();
505
+
506
+ // Reset all monitoring state
507
+ monitor.reset();
508
+ ```
509
+
510
+ #### Health Checks
511
+
512
+ ```typescript
513
+ // Force health check for a specific server
514
+ const health = await monitor.checkServer(serverId);
515
+
516
+ // Check all servers
517
+ await monitor.checkAllServers();
518
+
519
+ // Get health status
520
+ const health = monitor.getHealth(serverId);
521
+
522
+ // Get all health statuses
523
+ const allHealth = monitor.getAllHealth();
524
+ ```
525
+
526
+ #### Custom Health Checks
527
+
528
+ ```typescript
529
+ // Register custom health check
530
+ monitor.registerCheck({
531
+ name: 'memory-usage',
532
+ check: async server => {
533
+ const memUsage = await checkMemoryUsage(server);
534
+ return {
535
+ name: 'memory-usage',
536
+ status: memUsage > 90 ? 'unhealthy' : memUsage > 70 ? 'degraded' : 'healthy',
537
+ message: `Memory usage: ${memUsage}%`,
538
+ durationMs: 10,
539
+ timestamp: new Date(),
540
+ data: { usage: memUsage },
541
+ };
542
+ },
543
+ critical: false, // Non-critical checks don't affect overall status
544
+ timeout: 5000,
545
+ });
546
+
547
+ // Unregister check
548
+ monitor.unregisterCheck('memory-usage');
549
+
550
+ // Get registered checks
551
+ const checks = monitor.getRegisteredChecks();
552
+ ```
553
+
554
+ #### Request Tracking
555
+
556
+ ```typescript
557
+ // Record request metrics for a server
558
+ monitor.recordRequest(serverId, true, 150); // success, 150ms latency
559
+ monitor.recordRequest(serverId, false, 5000); // failure, 5000ms latency
560
+ ```
561
+
562
+ #### Health Events
563
+
564
+ ```typescript
565
+ monitor.on('health:checked', event => {
566
+ console.log('Health check completed:', event.serverId);
567
+ console.log('Status:', event.status.status);
568
+ console.log('Checks:', event.checks);
569
+ });
570
+
571
+ monitor.on('health:changed', event => {
572
+ console.log('Health changed:', event.previousStatus, '->', event.newStatus);
573
+ });
574
+
575
+ monitor.on('health:degraded', event => {
576
+ console.log('Server degraded:', event.serverId);
577
+ });
578
+
579
+ monitor.on('health:recovered', event => {
580
+ console.log('Server recovered:', event.serverId);
581
+ });
582
+
583
+ monitor.on('health:failed', event => {
584
+ console.log('Server failed:', event.serverId);
585
+ });
586
+
587
+ monitor.on('server:connected', event => {
588
+ console.log('Server connected:', event.serverId);
589
+ });
590
+
591
+ monitor.on('server:disconnected', event => {
592
+ console.log('Server disconnected:', event.serverId);
593
+ });
594
+
595
+ monitor.on('monitor:started', () => {
596
+ console.log('Monitoring started');
597
+ });
598
+
599
+ monitor.on('monitor:stopped', () => {
600
+ console.log('Monitoring stopped');
601
+ });
602
+ ```
603
+
604
+ ## Integration with VP Daemon
605
+
606
+ The mcp-registry integrates with the VP (Virtual Process) daemon for centralized MCP server management in production environments.
607
+
608
+ ### Daemon Integration Pattern
609
+
610
+ ```typescript
611
+ import { createMCPRegistrySystem } from '@wundr.io/mcp-registry';
612
+
613
+ class VPDaemon {
614
+ private registrySystem?: Awaited<ReturnType<typeof createMCPRegistrySystem>>;
615
+
616
+ async start() {
617
+ // Initialize registry system
618
+ this.registrySystem = await createMCPRegistrySystem({
619
+ aggregator: {
620
+ defaultStrategy: 'health-aware',
621
+ enableCircuitBreaker: true,
622
+ circuitBreakerThreshold: 3,
623
+ },
624
+ monitor: {
625
+ checkInterval: 15000,
626
+ failureThreshold: 2,
627
+ autoReconnect: true,
628
+ },
629
+ });
630
+
631
+ // Start health monitoring
632
+ await this.registrySystem.monitor.start();
633
+
634
+ // Register event handlers
635
+ this.setupEventHandlers();
636
+
637
+ // Load server configurations
638
+ await this.loadServerConfigs();
639
+ }
640
+
641
+ private setupEventHandlers() {
642
+ const { registry, monitor, aggregator } = this.registrySystem!;
643
+
644
+ // Handle server health changes
645
+ monitor.on('health:failed', async event => {
646
+ console.log(`Server ${event.serverId} failed, attempting recovery...`);
647
+ // Implement recovery logic
648
+ });
649
+
650
+ // Handle circuit breaker events
651
+ aggregator.on('circuit:opened', event => {
652
+ console.log(`Circuit opened for ${event.serverId}, routing to alternatives`);
653
+ });
654
+ }
655
+
656
+ private async loadServerConfigs() {
657
+ const { registry } = this.registrySystem!;
658
+
659
+ // Load from configuration file or database
660
+ const configs = await this.getServerConfigs();
661
+
662
+ for (const config of configs) {
663
+ await registry.register(config);
664
+ }
665
+ }
666
+
667
+ async invokeToolOnBestServer(toolName: string, args: Record<string, unknown>) {
668
+ const { aggregator } = this.registrySystem!;
669
+ return aggregator.invoke({ name: toolName, arguments: args });
670
+ }
671
+
672
+ async stop() {
673
+ if (this.registrySystem) {
674
+ await this.registrySystem.monitor.stop();
675
+ }
676
+ }
677
+ }
678
+ ```
679
+
680
+ ### Dynamic Server Loading
681
+
682
+ ```typescript
683
+ import { MCPServerRegistry, ServerRegistrationOptions } from '@wundr.io/mcp-registry';
684
+
685
+ async function loadServersFromConfig(registry: MCPServerRegistry, configPath: string) {
686
+ const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
687
+
688
+ for (const serverConfig of config.servers) {
689
+ await registry.register(serverConfig as ServerRegistrationOptions);
690
+ }
691
+ }
692
+
693
+ // Watch for configuration changes
694
+ fs.watch(configPath, async () => {
695
+ const newConfig = JSON.parse(await fs.readFile(configPath, 'utf-8'));
696
+ await syncServers(registry, newConfig.servers);
697
+ });
698
+
699
+ async function syncServers(registry: MCPServerRegistry, desiredConfigs: ServerRegistrationOptions[]) {
700
+ const currentServers = registry.getAll();
701
+ const desiredNames = new Set(desiredConfigs.map(c => c.name));
702
+
703
+ // Remove servers not in config
704
+ for (const server of currentServers) {
705
+ if (!desiredNames.has(server.name)) {
706
+ await registry.unregister(server.id);
707
+ }
708
+ }
709
+
710
+ // Add/update servers from config
711
+ for (const config of desiredConfigs) {
712
+ if (!registry.hasName(config.name)) {
713
+ await registry.register(config);
714
+ }
715
+ }
716
+ }
717
+ ```
718
+
719
+ ## Access Control and Permissions
720
+
721
+ While the registry itself doesn't enforce access control, it provides the foundation for implementing permission systems:
722
+
723
+ ### Server-Level Permissions
724
+
725
+ ```typescript
726
+ interface ServerPermissions {
727
+ allowedTools: string[];
728
+ allowedClients: string[];
729
+ rateLimit: number;
730
+ }
731
+
732
+ // Store permissions in server metadata
733
+ await registry.register({
734
+ name: 'restricted-server',
735
+ version: '1.0.0',
736
+ transport: { type: 'stdio', command: 'node', args: ['server.js'] },
737
+ metadata: {
738
+ permissions: {
739
+ allowedTools: ['safe-tool-1', 'safe-tool-2'],
740
+ allowedClients: ['client-a', 'client-b'],
741
+ rateLimit: 100,
742
+ } as ServerPermissions,
743
+ },
744
+ });
745
+
746
+ // Check permissions before invocation
747
+ async function invokeWithPermissions(
748
+ aggregator: MCPAggregator,
749
+ registry: MCPServerRegistry,
750
+ clientId: string,
751
+ request: ToolInvocationRequest
752
+ ) {
753
+ const servers = registry.findByTool(request.name);
754
+
755
+ const authorizedServer = servers.find(server => {
756
+ const perms = server.metadata?.permissions as ServerPermissions | undefined;
757
+ if (!perms) return true; // No restrictions
758
+ return perms.allowedClients.includes(clientId) && perms.allowedTools.includes(request.name);
759
+ });
760
+
761
+ if (!authorizedServer) {
762
+ throw new Error('Access denied');
763
+ }
764
+
765
+ return aggregator.invoke({
766
+ ...request,
767
+ preferredServer: authorizedServer.id,
768
+ });
769
+ }
770
+ ```
771
+
772
+ ## Error Handling
773
+
774
+ The package exports typed error classes for handling specific failure scenarios:
775
+
776
+ ```typescript
777
+ import {
778
+ // Registry errors
779
+ ServerNotFoundError,
780
+ ServerAlreadyExistsError,
781
+ RegistrationValidationError,
782
+
783
+ // Discovery errors
784
+ NoServersFoundError,
785
+ InvalidQueryError,
786
+
787
+ // Aggregator errors
788
+ NoServerAvailableError,
789
+ ToolInvocationTimeoutError,
790
+ CircuitBreakerOpenError,
791
+ RetryExhaustedError,
792
+
793
+ // Health errors
794
+ HealthCheckError,
795
+ } from '@wundr.io/mcp-registry';
796
+
797
+ try {
798
+ await aggregator.invoke({ name: 'unknown-tool' });
799
+ } catch (error) {
800
+ if (error instanceof NoServerAvailableError) {
801
+ console.log(`No server provides tool: ${error.toolName}`);
802
+ } else if (error instanceof ToolInvocationTimeoutError) {
803
+ console.log(`Tool timed out after ${error.timeoutMs}ms`);
804
+ } else if (error instanceof CircuitBreakerOpenError) {
805
+ console.log(`Server ${error.serverId} circuit is open`);
806
+ } else if (error instanceof RetryExhaustedError) {
807
+ console.log(`All ${error.attempts} retries failed: ${error.lastError.message}`);
808
+ }
809
+ }
810
+ ```
811
+
812
+ ## Runtime Validation
813
+
814
+ The package includes Zod schemas for runtime validation:
815
+
816
+ ```typescript
817
+ import {
818
+ TransportConfigSchema,
819
+ ServerRegistrationOptionsSchema,
820
+ ToolInvocationRequestSchema,
821
+ CapabilityQuerySchema,
822
+ AggregatorConfigSchema,
823
+ HealthMonitorConfigSchema,
824
+ } from '@wundr.io/mcp-registry';
825
+
826
+ // Validate configuration
827
+ const result = AggregatorConfigSchema.safeParse(userConfig);
828
+ if (!result.success) {
829
+ console.error('Invalid config:', result.error.errors);
830
+ }
831
+ ```
832
+
833
+ ## Type Definitions
834
+
835
+ ### Transport Types
836
+
837
+ ```typescript
838
+ type TransportType = 'stdio' | 'http' | 'websocket' | 'ipc';
839
+
840
+ interface TransportConfig {
841
+ type: TransportType;
842
+ command?: string; // For stdio
843
+ args?: string[];
844
+ env?: Record<string, string>;
845
+ cwd?: string;
846
+ url?: string; // For http/websocket
847
+ timeout?: number;
848
+ autoReconnect?: boolean;
849
+ reconnectDelay?: number;
850
+ maxReconnectAttempts?: number;
851
+ }
852
+ ```
853
+
854
+ ### Health Types
855
+
856
+ ```typescript
857
+ type HealthLevel = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
858
+
859
+ interface HealthStatus {
860
+ serverId: string;
861
+ status: HealthLevel;
862
+ connected: boolean;
863
+ lastPing?: Date;
864
+ latencyMs?: number;
865
+ avgLatencyMs?: number;
866
+ consecutiveFailures: number;
867
+ totalRequests: number;
868
+ successfulRequests: number;
869
+ errorRate: number;
870
+ checks: HealthCheckResult[];
871
+ updatedAt: Date;
872
+ }
873
+ ```
874
+
875
+ ### Routing Types
876
+
877
+ ```typescript
878
+ type RoutingStrategy = 'priority' | 'round-robin' | 'least-latency' | 'random' | 'health-aware';
879
+
880
+ type CircuitBreakerState = 'closed' | 'open' | 'half-open';
881
+ ```
882
+
883
+ ## Requirements
884
+
885
+ - Node.js >= 18.0.0
886
+ - TypeScript >= 5.0 (for development)
887
+
888
+ ## License
889
+
890
+ MIT