@wundr.io/mcp-registry 1.0.2-dev.20260530174250.ef0ec927
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 +890 -0
- package/package.json +74 -0
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 Orchestrator Daemon
|
|
605
|
+
|
|
606
|
+
The mcp-registry integrates with the Orchestrator (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
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wundr.io/mcp-registry",
|
|
3
|
+
"version": "1.0.2-dev.20260530174250.ef0ec927",
|
|
4
|
+
"description": "MCP Server registry and discovery with Super MCP aggregator pattern for unified tool routing",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"build:watch": "tsc --watch",
|
|
10
|
+
"clean": "rm -rf dist",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test:watch": "jest --watch",
|
|
14
|
+
"test:coverage": "jest --coverage",
|
|
15
|
+
"lint": "eslint src/**/*.ts",
|
|
16
|
+
"lint:fix": "eslint src/**/*.ts --fix",
|
|
17
|
+
"format": "prettier --write src/**/*.ts",
|
|
18
|
+
"format:check": "prettier --check src/**/*.ts",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"eventemitter3": "^5.0.1",
|
|
23
|
+
"uuid": "^11.0.3",
|
|
24
|
+
"zod": "^3.25.76"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20.9.0",
|
|
28
|
+
"@types/uuid": "^10.0.0",
|
|
29
|
+
"eslint": "^8.57.1",
|
|
30
|
+
"jest": "^29.7.0",
|
|
31
|
+
"prettier": "^3.3.3",
|
|
32
|
+
"ts-jest": "^29.4.1",
|
|
33
|
+
"typescript": "^5.2.2"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@wundr.io/mcp-server": "^1.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@wundr.io/mcp-server": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"mcp",
|
|
45
|
+
"model-context-protocol",
|
|
46
|
+
"registry",
|
|
47
|
+
"discovery",
|
|
48
|
+
"aggregator",
|
|
49
|
+
"super-mcp",
|
|
50
|
+
"claude-code",
|
|
51
|
+
"wundr"
|
|
52
|
+
],
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/adapticai/wundr.git",
|
|
56
|
+
"directory": "packages/@wundr/mcp-registry"
|
|
57
|
+
},
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/adapticai/wundr/issues"
|
|
60
|
+
},
|
|
61
|
+
"homepage": "https://wundr.io",
|
|
62
|
+
"license": "MIT",
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
},
|
|
66
|
+
"files": [
|
|
67
|
+
"dist",
|
|
68
|
+
"README.md"
|
|
69
|
+
],
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public",
|
|
72
|
+
"registry": "https://registry.npmjs.org/"
|
|
73
|
+
}
|
|
74
|
+
}
|