@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.
- package/CHANGELOG.md +12 -0
- package/README.md +59 -9
- package/dist/cli/commands/config.d.ts +4 -4
- package/dist/cli/commands/mcp.d.ts +87 -0
- package/dist/cli/commands/mcp.js +1524 -0
- package/dist/cli/loop/optionsSchema.js +4 -0
- package/dist/core/modules/ToolsManager.js +29 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +27 -1
- package/dist/lib/core/modules/ToolsManager.js +29 -2
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.js +27 -1
- package/dist/lib/mcp/agentExposure.d.ts +228 -0
- package/dist/lib/mcp/agentExposure.js +357 -0
- package/dist/lib/mcp/batching/index.d.ts +11 -0
- package/dist/lib/mcp/batching/index.js +11 -0
- package/dist/lib/mcp/batching/requestBatcher.d.ts +202 -0
- package/dist/lib/mcp/batching/requestBatcher.js +442 -0
- package/dist/lib/mcp/caching/index.d.ts +11 -0
- package/dist/lib/mcp/caching/index.js +11 -0
- package/dist/lib/mcp/caching/toolCache.d.ts +221 -0
- package/dist/lib/mcp/caching/toolCache.js +434 -0
- package/dist/lib/mcp/elicitation/elicitationManager.d.ts +169 -0
- package/dist/lib/mcp/elicitation/elicitationManager.js +377 -0
- package/dist/lib/mcp/elicitation/index.d.ts +11 -0
- package/dist/lib/mcp/elicitation/index.js +12 -0
- package/dist/lib/mcp/elicitation/types.d.ts +278 -0
- package/dist/lib/mcp/elicitation/types.js +11 -0
- package/dist/lib/mcp/elicitationProtocol.d.ts +228 -0
- package/dist/lib/mcp/elicitationProtocol.js +376 -0
- package/dist/lib/mcp/enhancedToolDiscovery.d.ts +205 -0
- package/dist/lib/mcp/enhancedToolDiscovery.js +482 -0
- package/dist/lib/mcp/index.d.ts +38 -1
- package/dist/lib/mcp/index.js +36 -3
- package/dist/lib/mcp/mcpRegistryClient.d.ts +332 -0
- package/dist/lib/mcp/mcpRegistryClient.js +489 -0
- package/dist/lib/mcp/mcpServerBase.d.ts +227 -0
- package/dist/lib/mcp/mcpServerBase.js +374 -0
- package/dist/lib/mcp/multiServerManager.d.ts +310 -0
- package/dist/lib/mcp/multiServerManager.js +580 -0
- package/dist/lib/mcp/routing/index.d.ts +11 -0
- package/dist/lib/mcp/routing/index.js +11 -0
- package/dist/lib/mcp/routing/toolRouter.d.ts +219 -0
- package/dist/lib/mcp/routing/toolRouter.js +417 -0
- package/dist/lib/mcp/serverCapabilities.d.ts +341 -0
- package/dist/lib/mcp/serverCapabilities.js +503 -0
- package/dist/lib/mcp/toolAnnotations.d.ts +154 -0
- package/dist/lib/mcp/toolAnnotations.js +240 -0
- package/dist/lib/mcp/toolConverter.d.ts +178 -0
- package/dist/lib/mcp/toolConverter.js +259 -0
- package/dist/lib/mcp/toolIntegration.d.ts +136 -0
- package/dist/lib/mcp/toolIntegration.js +335 -0
- package/dist/lib/memory/hippocampusInitializer.d.ts +2 -2
- package/dist/lib/memory/hippocampusInitializer.js +1 -1
- package/dist/lib/neurolink.d.ts +275 -2
- package/dist/lib/neurolink.js +596 -56
- package/dist/lib/providers/litellm.d.ts +10 -0
- package/dist/lib/providers/litellm.js +104 -2
- package/dist/lib/types/configTypes.d.ts +56 -0
- package/dist/lib/types/conversation.d.ts +2 -2
- package/dist/lib/types/generateTypes.d.ts +4 -0
- package/dist/lib/types/index.d.ts +2 -1
- package/dist/lib/types/modelTypes.d.ts +6 -6
- package/dist/lib/types/streamTypes.d.ts +2 -0
- package/dist/lib/types/tools.d.ts +2 -0
- package/dist/lib/utils/pricing.js +177 -17
- package/dist/lib/utils/schemaConversion.d.ts +6 -1
- package/dist/lib/utils/schemaConversion.js +50 -28
- package/dist/lib/workflow/config.d.ts +16 -16
- package/dist/mcp/agentExposure.d.ts +228 -0
- package/dist/mcp/agentExposure.js +356 -0
- package/dist/mcp/batching/index.d.ts +11 -0
- package/dist/mcp/batching/index.js +10 -0
- package/dist/mcp/batching/requestBatcher.d.ts +202 -0
- package/dist/mcp/batching/requestBatcher.js +441 -0
- package/dist/mcp/caching/index.d.ts +11 -0
- package/dist/mcp/caching/index.js +10 -0
- package/dist/mcp/caching/toolCache.d.ts +221 -0
- package/dist/mcp/caching/toolCache.js +433 -0
- package/dist/mcp/elicitation/elicitationManager.d.ts +169 -0
- package/dist/mcp/elicitation/elicitationManager.js +376 -0
- package/dist/mcp/elicitation/index.d.ts +11 -0
- package/dist/mcp/elicitation/index.js +11 -0
- package/dist/mcp/elicitation/types.d.ts +278 -0
- package/dist/mcp/elicitation/types.js +10 -0
- package/dist/mcp/elicitationProtocol.d.ts +228 -0
- package/dist/mcp/elicitationProtocol.js +375 -0
- package/dist/mcp/enhancedToolDiscovery.d.ts +205 -0
- package/dist/mcp/enhancedToolDiscovery.js +481 -0
- package/dist/mcp/index.d.ts +38 -1
- package/dist/mcp/index.js +36 -3
- package/dist/mcp/mcpRegistryClient.d.ts +332 -0
- package/dist/mcp/mcpRegistryClient.js +488 -0
- package/dist/mcp/mcpServerBase.d.ts +227 -0
- package/dist/mcp/mcpServerBase.js +373 -0
- package/dist/mcp/multiServerManager.d.ts +310 -0
- package/dist/mcp/multiServerManager.js +579 -0
- package/dist/mcp/routing/index.d.ts +11 -0
- package/dist/mcp/routing/index.js +10 -0
- package/dist/mcp/routing/toolRouter.d.ts +219 -0
- package/dist/mcp/routing/toolRouter.js +416 -0
- package/dist/mcp/serverCapabilities.d.ts +341 -0
- package/dist/mcp/serverCapabilities.js +502 -0
- package/dist/mcp/toolAnnotations.d.ts +154 -0
- package/dist/mcp/toolAnnotations.js +239 -0
- package/dist/mcp/toolConverter.d.ts +178 -0
- package/dist/mcp/toolConverter.js +258 -0
- package/dist/mcp/toolIntegration.d.ts +136 -0
- package/dist/mcp/toolIntegration.js +334 -0
- package/dist/memory/hippocampusInitializer.d.ts +2 -2
- package/dist/memory/hippocampusInitializer.js +1 -1
- package/dist/neurolink.d.ts +275 -2
- package/dist/neurolink.js +596 -56
- package/dist/providers/litellm.d.ts +10 -0
- package/dist/providers/litellm.js +104 -2
- package/dist/types/configTypes.d.ts +56 -0
- package/dist/types/conversation.d.ts +2 -2
- package/dist/types/generateTypes.d.ts +4 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/streamTypes.d.ts +2 -0
- package/dist/types/tools.d.ts +2 -0
- package/dist/utils/pricing.js +177 -17
- package/dist/utils/schemaConversion.d.ts +6 -1
- package/dist/utils/schemaConversion.js +50 -28
- 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);
|