@juspay/neurolink 9.26.2 → 9.28.0

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 (125) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +59 -9
  3. package/dist/cli/commands/config.d.ts +4 -4
  4. package/dist/cli/commands/mcp.d.ts +87 -0
  5. package/dist/cli/commands/mcp.js +1524 -0
  6. package/dist/cli/loop/optionsSchema.js +4 -0
  7. package/dist/core/modules/ToolsManager.js +29 -2
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.js +27 -1
  10. package/dist/lib/core/modules/ToolsManager.js +29 -2
  11. package/dist/lib/index.d.ts +2 -1
  12. package/dist/lib/index.js +27 -1
  13. package/dist/lib/mcp/agentExposure.d.ts +228 -0
  14. package/dist/lib/mcp/agentExposure.js +357 -0
  15. package/dist/lib/mcp/batching/index.d.ts +11 -0
  16. package/dist/lib/mcp/batching/index.js +11 -0
  17. package/dist/lib/mcp/batching/requestBatcher.d.ts +202 -0
  18. package/dist/lib/mcp/batching/requestBatcher.js +442 -0
  19. package/dist/lib/mcp/caching/index.d.ts +11 -0
  20. package/dist/lib/mcp/caching/index.js +11 -0
  21. package/dist/lib/mcp/caching/toolCache.d.ts +221 -0
  22. package/dist/lib/mcp/caching/toolCache.js +434 -0
  23. package/dist/lib/mcp/elicitation/elicitationManager.d.ts +169 -0
  24. package/dist/lib/mcp/elicitation/elicitationManager.js +377 -0
  25. package/dist/lib/mcp/elicitation/index.d.ts +11 -0
  26. package/dist/lib/mcp/elicitation/index.js +12 -0
  27. package/dist/lib/mcp/elicitation/types.d.ts +278 -0
  28. package/dist/lib/mcp/elicitation/types.js +11 -0
  29. package/dist/lib/mcp/elicitationProtocol.d.ts +228 -0
  30. package/dist/lib/mcp/elicitationProtocol.js +376 -0
  31. package/dist/lib/mcp/enhancedToolDiscovery.d.ts +205 -0
  32. package/dist/lib/mcp/enhancedToolDiscovery.js +482 -0
  33. package/dist/lib/mcp/index.d.ts +38 -1
  34. package/dist/lib/mcp/index.js +36 -3
  35. package/dist/lib/mcp/mcpRegistryClient.d.ts +332 -0
  36. package/dist/lib/mcp/mcpRegistryClient.js +489 -0
  37. package/dist/lib/mcp/mcpServerBase.d.ts +227 -0
  38. package/dist/lib/mcp/mcpServerBase.js +374 -0
  39. package/dist/lib/mcp/multiServerManager.d.ts +310 -0
  40. package/dist/lib/mcp/multiServerManager.js +580 -0
  41. package/dist/lib/mcp/routing/index.d.ts +11 -0
  42. package/dist/lib/mcp/routing/index.js +11 -0
  43. package/dist/lib/mcp/routing/toolRouter.d.ts +219 -0
  44. package/dist/lib/mcp/routing/toolRouter.js +417 -0
  45. package/dist/lib/mcp/serverCapabilities.d.ts +341 -0
  46. package/dist/lib/mcp/serverCapabilities.js +503 -0
  47. package/dist/lib/mcp/toolAnnotations.d.ts +154 -0
  48. package/dist/lib/mcp/toolAnnotations.js +240 -0
  49. package/dist/lib/mcp/toolConverter.d.ts +178 -0
  50. package/dist/lib/mcp/toolConverter.js +259 -0
  51. package/dist/lib/mcp/toolIntegration.d.ts +136 -0
  52. package/dist/lib/mcp/toolIntegration.js +335 -0
  53. package/dist/lib/memory/hippocampusInitializer.d.ts +2 -2
  54. package/dist/lib/memory/hippocampusInitializer.js +1 -1
  55. package/dist/lib/neurolink.d.ts +275 -2
  56. package/dist/lib/neurolink.js +596 -56
  57. package/dist/lib/providers/litellm.d.ts +10 -0
  58. package/dist/lib/providers/litellm.js +104 -2
  59. package/dist/lib/types/configTypes.d.ts +56 -0
  60. package/dist/lib/types/conversation.d.ts +2 -2
  61. package/dist/lib/types/generateTypes.d.ts +4 -0
  62. package/dist/lib/types/index.d.ts +2 -1
  63. package/dist/lib/types/modelTypes.d.ts +6 -6
  64. package/dist/lib/types/streamTypes.d.ts +2 -0
  65. package/dist/lib/types/tools.d.ts +2 -0
  66. package/dist/lib/utils/pricing.js +177 -17
  67. package/dist/lib/utils/schemaConversion.d.ts +6 -1
  68. package/dist/lib/utils/schemaConversion.js +50 -28
  69. package/dist/lib/workflow/config.d.ts +16 -16
  70. package/dist/mcp/agentExposure.d.ts +228 -0
  71. package/dist/mcp/agentExposure.js +356 -0
  72. package/dist/mcp/batching/index.d.ts +11 -0
  73. package/dist/mcp/batching/index.js +10 -0
  74. package/dist/mcp/batching/requestBatcher.d.ts +202 -0
  75. package/dist/mcp/batching/requestBatcher.js +441 -0
  76. package/dist/mcp/caching/index.d.ts +11 -0
  77. package/dist/mcp/caching/index.js +10 -0
  78. package/dist/mcp/caching/toolCache.d.ts +221 -0
  79. package/dist/mcp/caching/toolCache.js +433 -0
  80. package/dist/mcp/elicitation/elicitationManager.d.ts +169 -0
  81. package/dist/mcp/elicitation/elicitationManager.js +376 -0
  82. package/dist/mcp/elicitation/index.d.ts +11 -0
  83. package/dist/mcp/elicitation/index.js +11 -0
  84. package/dist/mcp/elicitation/types.d.ts +278 -0
  85. package/dist/mcp/elicitation/types.js +10 -0
  86. package/dist/mcp/elicitationProtocol.d.ts +228 -0
  87. package/dist/mcp/elicitationProtocol.js +375 -0
  88. package/dist/mcp/enhancedToolDiscovery.d.ts +205 -0
  89. package/dist/mcp/enhancedToolDiscovery.js +481 -0
  90. package/dist/mcp/index.d.ts +38 -1
  91. package/dist/mcp/index.js +36 -3
  92. package/dist/mcp/mcpRegistryClient.d.ts +332 -0
  93. package/dist/mcp/mcpRegistryClient.js +488 -0
  94. package/dist/mcp/mcpServerBase.d.ts +227 -0
  95. package/dist/mcp/mcpServerBase.js +373 -0
  96. package/dist/mcp/multiServerManager.d.ts +310 -0
  97. package/dist/mcp/multiServerManager.js +579 -0
  98. package/dist/mcp/routing/index.d.ts +11 -0
  99. package/dist/mcp/routing/index.js +10 -0
  100. package/dist/mcp/routing/toolRouter.d.ts +219 -0
  101. package/dist/mcp/routing/toolRouter.js +416 -0
  102. package/dist/mcp/serverCapabilities.d.ts +341 -0
  103. package/dist/mcp/serverCapabilities.js +502 -0
  104. package/dist/mcp/toolAnnotations.d.ts +154 -0
  105. package/dist/mcp/toolAnnotations.js +239 -0
  106. package/dist/mcp/toolConverter.d.ts +178 -0
  107. package/dist/mcp/toolConverter.js +258 -0
  108. package/dist/mcp/toolIntegration.d.ts +136 -0
  109. package/dist/mcp/toolIntegration.js +334 -0
  110. package/dist/memory/hippocampusInitializer.d.ts +2 -2
  111. package/dist/memory/hippocampusInitializer.js +1 -1
  112. package/dist/neurolink.d.ts +275 -2
  113. package/dist/neurolink.js +596 -56
  114. package/dist/providers/litellm.d.ts +10 -0
  115. package/dist/providers/litellm.js +104 -2
  116. package/dist/types/configTypes.d.ts +56 -0
  117. package/dist/types/conversation.d.ts +2 -2
  118. package/dist/types/generateTypes.d.ts +4 -0
  119. package/dist/types/index.d.ts +2 -1
  120. package/dist/types/streamTypes.d.ts +2 -0
  121. package/dist/types/tools.d.ts +2 -0
  122. package/dist/utils/pricing.js +177 -17
  123. package/dist/utils/schemaConversion.d.ts +6 -1
  124. package/dist/utils/schemaConversion.js +50 -28
  125. package/package.json +2 -2
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Tool Router - Routes tool calls to appropriate MCP servers
3
+ * Based on tool categories, annotations, and server capabilities
4
+ *
5
+ * Provides intelligent routing strategies for multi-server MCP environments:
6
+ * - Round-robin for even distribution
7
+ * - Least-loaded for optimal performance
8
+ * - Capability-based for specialized servers
9
+ * - Affinity-based for session consistency
10
+ */
11
+ import { EventEmitter } from "events";
12
+ import type { ToolInfo } from "../../types/tools.js";
13
+ import type { MCPToolAnnotations } from "../mcpServerBase.js";
14
+ /**
15
+ * Routing strategy types
16
+ */
17
+ export type RoutingStrategy = "round-robin" | "least-loaded" | "capability-based" | "affinity" | "priority" | "random";
18
+ /**
19
+ * Server routing weight configuration
20
+ */
21
+ export type ServerWeight = {
22
+ serverId: string;
23
+ weight: number;
24
+ capabilities?: string[];
25
+ };
26
+ /**
27
+ * Category to server mapping
28
+ */
29
+ export type CategoryMapping = {
30
+ category: string;
31
+ serverIds: string[];
32
+ priority?: number;
33
+ };
34
+ /**
35
+ * Affinity rule for session-based routing
36
+ */
37
+ export type AffinityRule = {
38
+ key: string;
39
+ serverId: string;
40
+ expiresAt?: number;
41
+ };
42
+ /**
43
+ * Tool Router configuration
44
+ */
45
+ export type ToolRouterConfig = {
46
+ /**
47
+ * Primary routing strategy
48
+ */
49
+ strategy: RoutingStrategy;
50
+ /**
51
+ * Enable session/user affinity for consistent routing
52
+ */
53
+ enableAffinity?: boolean;
54
+ /**
55
+ * Category to server mapping for capability-based routing
56
+ */
57
+ categoryMapping?: Record<string, string[]>;
58
+ /**
59
+ * Server weights for priority-based routing
60
+ */
61
+ serverWeights?: ServerWeight[];
62
+ /**
63
+ * Fallback strategy if primary fails
64
+ */
65
+ fallbackStrategy?: RoutingStrategy;
66
+ /**
67
+ * Maximum retries for failed routes
68
+ */
69
+ maxRetries?: number;
70
+ /**
71
+ * Health check interval in milliseconds
72
+ */
73
+ healthCheckInterval?: number;
74
+ /**
75
+ * Affinity TTL in milliseconds (default: 30 minutes)
76
+ */
77
+ affinityTtl?: number;
78
+ };
79
+ /**
80
+ * Routing decision result
81
+ */
82
+ export type RoutingDecision = {
83
+ serverId: string;
84
+ strategy: RoutingStrategy;
85
+ confidence: number;
86
+ alternates?: string[];
87
+ reason?: string;
88
+ };
89
+ /**
90
+ * Tool Router events
91
+ */
92
+ export type ToolRouterEvents = {
93
+ routeDecision: {
94
+ toolName: string;
95
+ decision: RoutingDecision;
96
+ };
97
+ routeFailed: {
98
+ toolName: string;
99
+ error: Error;
100
+ attemptedServers: string[];
101
+ };
102
+ affinitySet: {
103
+ key: string;
104
+ serverId: string;
105
+ };
106
+ affinityExpired: {
107
+ key: string;
108
+ };
109
+ healthUpdate: {
110
+ serverId: string;
111
+ healthy: boolean;
112
+ };
113
+ };
114
+ /**
115
+ * MCP Tool type with annotations
116
+ */
117
+ export type MCPTool = ToolInfo & {
118
+ annotations?: MCPToolAnnotations;
119
+ serverId?: string;
120
+ category?: string;
121
+ };
122
+ /**
123
+ * Default router configuration for common use cases
124
+ */
125
+ export declare const DEFAULT_ROUTER_CONFIG: ToolRouterConfig;
126
+ /**
127
+ * Tool Router - Intelligent routing for MCP tool calls
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * const router = new ToolRouter({
132
+ * strategy: 'least-loaded',
133
+ * enableAffinity: true,
134
+ * categoryMapping: {
135
+ * 'database': ['db-server-1', 'db-server-2'],
136
+ * 'ai': ['ai-server-primary', 'ai-server-secondary'],
137
+ * },
138
+ * });
139
+ *
140
+ * const decision = router.route(tool, { sessionId: 'user-123' });
141
+ * console.log(`Routing to: ${decision.serverId}`);
142
+ * ```
143
+ */
144
+ export declare class ToolRouter extends EventEmitter {
145
+ private config;
146
+ private roundRobinIndex;
147
+ private serverLoads;
148
+ private affinityRules;
149
+ private healthStatus;
150
+ private availableServers;
151
+ private affinityCleanupTimer?;
152
+ constructor(config?: ToolRouterConfig);
153
+ destroy(): void;
154
+ private cleanupExpiredAffinities;
155
+ /**
156
+ * Register a server as available for routing
157
+ */
158
+ registerServer(serverId: string, capabilities?: string[]): void;
159
+ /**
160
+ * Unregister a server from routing
161
+ */
162
+ unregisterServer(serverId: string): void;
163
+ /**
164
+ * Route a tool call to the best server
165
+ */
166
+ route(tool: MCPTool, context?: {
167
+ sessionId?: string;
168
+ userId?: string;
169
+ }): RoutingDecision;
170
+ /**
171
+ * Route by tool category
172
+ */
173
+ routeByCategory(tool: MCPTool, category: string): string[];
174
+ /**
175
+ * Route by tool annotation hints
176
+ */
177
+ routeByAnnotation(tool: MCPTool): string[];
178
+ /**
179
+ * Route by required capabilities
180
+ */
181
+ routeByCapability(tool: MCPTool, requiredCapabilities: string[]): string[];
182
+ /**
183
+ * Update server load for least-loaded routing
184
+ */
185
+ updateServerLoad(serverId: string, delta: number): void;
186
+ /**
187
+ * Update server health status
188
+ */
189
+ updateHealthStatus(serverId: string, healthy: boolean): void;
190
+ /**
191
+ * Set session/user affinity
192
+ */
193
+ setAffinity(key: string, serverId: string): void;
194
+ /**
195
+ * Clear affinity for a key
196
+ */
197
+ clearAffinity(key: string): void;
198
+ /**
199
+ * Get current routing statistics
200
+ */
201
+ getStats(): {
202
+ availableServers: number;
203
+ healthyServers: number;
204
+ activeAffinities: number;
205
+ serverLoads: Record<string, number>;
206
+ };
207
+ private getCandidateServers;
208
+ private applyStrategy;
209
+ private roundRobinSelect;
210
+ private leastLoadedSelect;
211
+ private capabilityBasedSelect;
212
+ private prioritySelect;
213
+ private randomSelect;
214
+ private isServerHealthy;
215
+ }
216
+ /**
217
+ * Factory function to create a ToolRouter instance
218
+ */
219
+ export declare const createToolRouter: (config: ToolRouterConfig) => ToolRouter;
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Tool Router - Routes tool calls to appropriate MCP servers
3
+ * Based on tool categories, annotations, and server capabilities
4
+ *
5
+ * Provides intelligent routing strategies for multi-server MCP environments:
6
+ * - Round-robin for even distribution
7
+ * - Least-loaded for optimal performance
8
+ * - Capability-based for specialized servers
9
+ * - Affinity-based for session consistency
10
+ */
11
+ import { EventEmitter } from "events";
12
+ import { ErrorFactory } from "../../utils/errorHandling.js";
13
+ /**
14
+ * Default router configuration for common use cases
15
+ */
16
+ export const DEFAULT_ROUTER_CONFIG = {
17
+ strategy: "least-loaded",
18
+ enableAffinity: false,
19
+ maxRetries: 3,
20
+ healthCheckInterval: 30000,
21
+ affinityTtl: 30 * 60 * 1000,
22
+ };
23
+ /**
24
+ * Tool Router - Intelligent routing for MCP tool calls
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const router = new ToolRouter({
29
+ * strategy: 'least-loaded',
30
+ * enableAffinity: true,
31
+ * categoryMapping: {
32
+ * 'database': ['db-server-1', 'db-server-2'],
33
+ * 'ai': ['ai-server-primary', 'ai-server-secondary'],
34
+ * },
35
+ * });
36
+ *
37
+ * const decision = router.route(tool, { sessionId: 'user-123' });
38
+ * console.log(`Routing to: ${decision.serverId}`);
39
+ * ```
40
+ */
41
+ export class ToolRouter extends EventEmitter {
42
+ config;
43
+ roundRobinIndex = new Map();
44
+ serverLoads = new Map();
45
+ affinityRules = new Map();
46
+ healthStatus = new Map();
47
+ availableServers = new Set();
48
+ affinityCleanupTimer;
49
+ constructor(config = DEFAULT_ROUTER_CONFIG) {
50
+ super();
51
+ this.config = {
52
+ strategy: config.strategy ?? "least-loaded",
53
+ enableAffinity: config.enableAffinity ?? false,
54
+ categoryMapping: config.categoryMapping ?? {},
55
+ serverWeights: config.serverWeights ?? [],
56
+ fallbackStrategy: config.fallbackStrategy ?? "round-robin",
57
+ maxRetries: config.maxRetries ?? 3,
58
+ healthCheckInterval: config.healthCheckInterval ?? 30000,
59
+ affinityTtl: config.affinityTtl ?? 30 * 60 * 1000, // 30 minutes
60
+ };
61
+ if (this.config.enableAffinity) {
62
+ this.affinityCleanupTimer = setInterval(() => {
63
+ this.cleanupExpiredAffinities();
64
+ }, this.config.healthCheckInterval);
65
+ if (this.affinityCleanupTimer.unref) {
66
+ this.affinityCleanupTimer.unref();
67
+ }
68
+ }
69
+ }
70
+ destroy() {
71
+ if (this.affinityCleanupTimer) {
72
+ clearInterval(this.affinityCleanupTimer);
73
+ this.affinityCleanupTimer = undefined;
74
+ }
75
+ this.affinityRules.clear();
76
+ }
77
+ cleanupExpiredAffinities() {
78
+ const now = Date.now();
79
+ for (const [key, rule] of this.affinityRules) {
80
+ if (rule.expiresAt && rule.expiresAt <= now) {
81
+ this.affinityRules.delete(key);
82
+ this.emit("affinityExpired", { key });
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Register a server as available for routing
88
+ */
89
+ registerServer(serverId, capabilities) {
90
+ this.availableServers.add(serverId);
91
+ this.healthStatus.set(serverId, true);
92
+ this.serverLoads.set(serverId, 0);
93
+ // Update category mapping if capabilities provided
94
+ if (capabilities) {
95
+ for (const capability of capabilities) {
96
+ if (!this.config.categoryMapping[capability]) {
97
+ this.config.categoryMapping[capability] = [];
98
+ }
99
+ if (!this.config.categoryMapping[capability].includes(serverId)) {
100
+ this.config.categoryMapping[capability].push(serverId);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ /**
106
+ * Unregister a server from routing
107
+ */
108
+ unregisterServer(serverId) {
109
+ this.availableServers.delete(serverId);
110
+ this.healthStatus.delete(serverId);
111
+ this.serverLoads.delete(serverId);
112
+ // Reset all round-robin indices since any tool may have been
113
+ // routed to the removed server. Keys are `rr-${toolName}`.
114
+ this.roundRobinIndex.clear();
115
+ // Remove from category mappings
116
+ for (const category of Object.keys(this.config.categoryMapping)) {
117
+ const servers = this.config.categoryMapping[category];
118
+ const index = servers.indexOf(serverId);
119
+ if (index !== -1) {
120
+ servers.splice(index, 1);
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Route a tool call to the best server
126
+ */
127
+ route(tool, context) {
128
+ // Check affinity first if enabled
129
+ if (this.config.enableAffinity && context) {
130
+ const affinityKey = context.sessionId ?? context.userId;
131
+ if (affinityKey) {
132
+ const affinityRule = this.affinityRules.get(affinityKey);
133
+ if (affinityRule && this.isServerHealthy(affinityRule.serverId)) {
134
+ if (!affinityRule.expiresAt || affinityRule.expiresAt > Date.now()) {
135
+ return {
136
+ serverId: affinityRule.serverId,
137
+ strategy: "affinity",
138
+ confidence: 1.0,
139
+ reason: `Affinity match for ${affinityKey}`,
140
+ };
141
+ }
142
+ else {
143
+ this.affinityRules.delete(affinityKey);
144
+ this.emit("affinityExpired", { key: affinityKey });
145
+ }
146
+ }
147
+ }
148
+ }
149
+ // Get candidate servers
150
+ const candidates = this.getCandidateServers(tool);
151
+ if (candidates.length === 0) {
152
+ const routeError = ErrorFactory.toolExecutionFailed(tool.name, new Error(`No healthy servers available (strategy: ${this.config.strategy}, registered: ${this.availableServers.size})`));
153
+ this.emit("routeFailed", {
154
+ toolName: tool.name,
155
+ error: routeError,
156
+ attemptedServers: Array.from(this.availableServers),
157
+ });
158
+ throw routeError;
159
+ }
160
+ // Apply routing strategy
161
+ const decision = this.applyStrategy(this.config.strategy, tool, candidates);
162
+ // Set affinity if enabled
163
+ if (this.config.enableAffinity && context) {
164
+ const affinityKey = context.sessionId ?? context.userId;
165
+ if (affinityKey) {
166
+ this.setAffinity(affinityKey, decision.serverId);
167
+ }
168
+ }
169
+ this.emit("routeDecision", { toolName: tool.name, decision });
170
+ return decision;
171
+ }
172
+ /**
173
+ * Route by tool category
174
+ */
175
+ routeByCategory(tool, category) {
176
+ const servers = this.config.categoryMapping[category] ?? [];
177
+ return servers.filter((s) => this.isServerHealthy(s));
178
+ }
179
+ /**
180
+ * Route by tool annotation hints
181
+ */
182
+ routeByAnnotation(tool) {
183
+ if (!tool.annotations) {
184
+ return Array.from(this.availableServers).filter((s) => this.isServerHealthy(s));
185
+ }
186
+ // Route destructive tools to primary servers only (check before readOnlyHint
187
+ // so that a tool with both flags is still restricted to primary servers)
188
+ if (tool.annotations.destructiveHint) {
189
+ const primaryServers = this.config.serverWeights
190
+ .filter((sw) => sw.weight >= 50)
191
+ .map((sw) => sw.serverId)
192
+ .filter((s) => this.isServerHealthy(s));
193
+ if (primaryServers.length > 0) {
194
+ return primaryServers;
195
+ }
196
+ }
197
+ // Route read-only tools to any healthy server
198
+ if (tool.annotations.readOnlyHint) {
199
+ return Array.from(this.availableServers).filter((s) => this.isServerHealthy(s));
200
+ }
201
+ // Route idempotent tools preferring cached servers
202
+ if (tool.annotations.idempotentHint) {
203
+ const cachedServers = this.config.categoryMapping["caching"] ?? [];
204
+ const healthyCached = cachedServers.filter((s) => this.isServerHealthy(s));
205
+ if (healthyCached.length > 0) {
206
+ return healthyCached;
207
+ }
208
+ }
209
+ return Array.from(this.availableServers).filter((s) => this.isServerHealthy(s));
210
+ }
211
+ /**
212
+ * Route by required capabilities
213
+ */
214
+ routeByCapability(tool, requiredCapabilities) {
215
+ const matchingServers = [];
216
+ for (const serverId of this.availableServers) {
217
+ if (!this.isServerHealthy(serverId)) {
218
+ continue;
219
+ }
220
+ // Check if server has all required capabilities
221
+ let hasAll = true;
222
+ for (const capability of requiredCapabilities) {
223
+ const serversWithCapability = this.config.categoryMapping[capability] ?? [];
224
+ if (!serversWithCapability.includes(serverId)) {
225
+ hasAll = false;
226
+ break;
227
+ }
228
+ }
229
+ if (hasAll) {
230
+ matchingServers.push(serverId);
231
+ }
232
+ }
233
+ return matchingServers;
234
+ }
235
+ /**
236
+ * Update server load for least-loaded routing
237
+ */
238
+ updateServerLoad(serverId, delta) {
239
+ const currentLoad = this.serverLoads.get(serverId) ?? 0;
240
+ this.serverLoads.set(serverId, Math.max(0, currentLoad + delta));
241
+ }
242
+ /**
243
+ * Update server health status
244
+ */
245
+ updateHealthStatus(serverId, healthy) {
246
+ const previousStatus = this.healthStatus.get(serverId);
247
+ this.healthStatus.set(serverId, healthy);
248
+ if (previousStatus !== healthy) {
249
+ this.emit("healthUpdate", { serverId, healthy });
250
+ }
251
+ }
252
+ /**
253
+ * Set session/user affinity
254
+ */
255
+ setAffinity(key, serverId) {
256
+ this.affinityRules.set(key, {
257
+ key,
258
+ serverId,
259
+ expiresAt: Date.now() + this.config.affinityTtl,
260
+ });
261
+ this.emit("affinitySet", { key, serverId });
262
+ }
263
+ /**
264
+ * Clear affinity for a key
265
+ */
266
+ clearAffinity(key) {
267
+ this.affinityRules.delete(key);
268
+ }
269
+ /**
270
+ * Get current routing statistics
271
+ */
272
+ getStats() {
273
+ const healthyCount = Array.from(this.healthStatus.values()).filter((h) => h).length;
274
+ return {
275
+ availableServers: this.availableServers.size,
276
+ healthyServers: healthyCount,
277
+ activeAffinities: this.affinityRules.size,
278
+ serverLoads: Object.fromEntries(this.serverLoads),
279
+ };
280
+ }
281
+ // ==================== Private Methods ====================
282
+ getCandidateServers(tool) {
283
+ // If tool has a specific server, use only that
284
+ if (tool.serverId && this.isServerHealthy(tool.serverId)) {
285
+ return [tool.serverId];
286
+ }
287
+ // Check category mapping
288
+ if (tool.category) {
289
+ const categoryServers = this.routeByCategory(tool, tool.category);
290
+ if (categoryServers.length > 0) {
291
+ return categoryServers;
292
+ }
293
+ }
294
+ // Check annotation-based routing
295
+ const annotationServers = this.routeByAnnotation(tool);
296
+ if (annotationServers.length > 0) {
297
+ return annotationServers;
298
+ }
299
+ // Fall back to all healthy servers
300
+ return Array.from(this.availableServers).filter((s) => this.isServerHealthy(s));
301
+ }
302
+ applyStrategy(strategy, tool, candidates) {
303
+ switch (strategy) {
304
+ case "round-robin":
305
+ return this.roundRobinSelect(tool.name, candidates);
306
+ case "least-loaded":
307
+ return this.leastLoadedSelect(candidates);
308
+ case "capability-based":
309
+ return this.capabilityBasedSelect(tool, candidates);
310
+ case "priority":
311
+ return this.prioritySelect(candidates);
312
+ case "random":
313
+ return this.randomSelect(candidates);
314
+ case "affinity":
315
+ // Affinity is handled at the top of route(), fall back to round-robin
316
+ return this.roundRobinSelect(tool.name, candidates);
317
+ default:
318
+ return this.roundRobinSelect(tool.name, candidates);
319
+ }
320
+ }
321
+ roundRobinSelect(toolName, candidates) {
322
+ const key = `rr-${toolName}`;
323
+ const currentIndex = this.roundRobinIndex.get(key) ?? 0;
324
+ const selectedIndex = currentIndex % candidates.length;
325
+ this.roundRobinIndex.set(key, currentIndex + 1);
326
+ return {
327
+ serverId: candidates[selectedIndex],
328
+ strategy: "round-robin",
329
+ confidence: 0.8,
330
+ alternates: candidates.filter((_, i) => i !== selectedIndex),
331
+ reason: `Round-robin selection (index ${selectedIndex})`,
332
+ };
333
+ }
334
+ leastLoadedSelect(candidates) {
335
+ let minLoad = Infinity;
336
+ let selectedServer = candidates[0];
337
+ for (const serverId of candidates) {
338
+ const load = this.serverLoads.get(serverId) ?? 0;
339
+ if (load < minLoad) {
340
+ minLoad = load;
341
+ selectedServer = serverId;
342
+ }
343
+ }
344
+ return {
345
+ serverId: selectedServer,
346
+ strategy: "least-loaded",
347
+ confidence: 0.9,
348
+ alternates: candidates.filter((s) => s !== selectedServer),
349
+ reason: `Least loaded server (load: ${minLoad})`,
350
+ };
351
+ }
352
+ capabilityBasedSelect(tool, candidates) {
353
+ // Score each candidate based on capability match
354
+ const scores = [];
355
+ for (const serverId of candidates) {
356
+ let score = 1;
357
+ // Check weight
358
+ const weight = this.config.serverWeights.find((sw) => sw.serverId === serverId);
359
+ if (weight) {
360
+ score += weight.weight / 100;
361
+ }
362
+ // Check capability match
363
+ if (tool.category) {
364
+ const categoryServers = this.config.categoryMapping[tool.category];
365
+ if (categoryServers?.includes(serverId)) {
366
+ score += 0.5;
367
+ }
368
+ }
369
+ scores.push({ serverId, score });
370
+ }
371
+ // Sort by score descending
372
+ scores.sort((a, b) => b.score - a.score);
373
+ return {
374
+ serverId: scores[0].serverId,
375
+ strategy: "capability-based",
376
+ confidence: Math.min(1, scores[0].score / 2),
377
+ alternates: scores.slice(1).map((s) => s.serverId),
378
+ reason: `Capability score: ${scores[0].score.toFixed(2)}`,
379
+ };
380
+ }
381
+ prioritySelect(candidates) {
382
+ // Sort by weight
383
+ const weighted = candidates
384
+ .map((serverId) => {
385
+ const weight = this.config.serverWeights.find((sw) => sw.serverId === serverId)
386
+ ?.weight ?? 50;
387
+ return { serverId, weight };
388
+ })
389
+ .sort((a, b) => b.weight - a.weight);
390
+ return {
391
+ serverId: weighted[0].serverId,
392
+ strategy: "priority",
393
+ confidence: weighted[0].weight / 100,
394
+ alternates: weighted.slice(1).map((w) => w.serverId),
395
+ reason: `Priority weight: ${weighted[0].weight}`,
396
+ };
397
+ }
398
+ randomSelect(candidates) {
399
+ const randomIndex = Math.floor(Math.random() * candidates.length);
400
+ return {
401
+ serverId: candidates[randomIndex],
402
+ strategy: "random",
403
+ confidence: 0.5,
404
+ alternates: candidates.filter((_, i) => i !== randomIndex),
405
+ reason: "Random selection",
406
+ };
407
+ }
408
+ isServerHealthy(serverId) {
409
+ return (this.availableServers.has(serverId) &&
410
+ (this.healthStatus.get(serverId) ?? false));
411
+ }
412
+ }
413
+ /**
414
+ * Factory function to create a ToolRouter instance
415
+ */
416
+ export const createToolRouter = (config) => new ToolRouter(config);