@koi-language/koi 1.0.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/QUICKSTART.md +89 -0
- package/README.md +545 -0
- package/examples/actions-demo.koi +177 -0
- package/examples/cache-test.koi +29 -0
- package/examples/calculator.koi +61 -0
- package/examples/clear-registry.js +33 -0
- package/examples/clear-registry.koi +30 -0
- package/examples/code-introspection-test.koi +149 -0
- package/examples/counter.koi +132 -0
- package/examples/delegation-test.koi +52 -0
- package/examples/directory-import-test.koi +84 -0
- package/examples/hello-world-claude.koi +52 -0
- package/examples/hello-world.koi +52 -0
- package/examples/hello.koi +24 -0
- package/examples/mcp-example.koi +70 -0
- package/examples/multi-event-handler-test.koi +144 -0
- package/examples/new-import-test.koi +89 -0
- package/examples/pipeline.koi +162 -0
- package/examples/registry-demo.koi +184 -0
- package/examples/registry-playbook-demo.koi +162 -0
- package/examples/registry-playbook-email-compositor-2.koi +140 -0
- package/examples/registry-playbook-email-compositor.koi +140 -0
- package/examples/sentiment.koi +90 -0
- package/examples/simple.koi +48 -0
- package/examples/skill-import-test.koi +76 -0
- package/examples/skills/advanced/index.koi +95 -0
- package/examples/skills/math-operations.koi +69 -0
- package/examples/skills/string-operations.koi +56 -0
- package/examples/task-chaining-demo.koi +244 -0
- package/examples/test-await.koi +22 -0
- package/examples/test-crypto-sha256.koi +196 -0
- package/examples/test-delegation.koi +41 -0
- package/examples/test-multi-team-routing.koi +258 -0
- package/examples/test-no-handler.koi +35 -0
- package/examples/test-npm-import.koi +67 -0
- package/examples/test-parse.koi +10 -0
- package/examples/test-peers-with-team.koi +59 -0
- package/examples/test-permissions-fail.koi +20 -0
- package/examples/test-permissions.koi +36 -0
- package/examples/test-simple-registry.koi +31 -0
- package/examples/test-typescript-import.koi +64 -0
- package/examples/test-uses-team-syntax.koi +25 -0
- package/examples/test-uses-team.koi +31 -0
- package/examples/utils/calculator.test.ts +144 -0
- package/examples/utils/calculator.ts +56 -0
- package/examples/utils/math-helpers.js +50 -0
- package/examples/utils/math-helpers.ts +55 -0
- package/examples/web-delegation-demo.koi +165 -0
- package/package.json +78 -0
- package/src/cli/koi.js +793 -0
- package/src/compiler/build-optimizer.js +447 -0
- package/src/compiler/cache-manager.js +274 -0
- package/src/compiler/import-resolver.js +369 -0
- package/src/compiler/parser.js +7542 -0
- package/src/compiler/transpiler.js +1105 -0
- package/src/compiler/typescript-transpiler.js +148 -0
- package/src/grammar/koi.pegjs +767 -0
- package/src/runtime/action-registry.js +172 -0
- package/src/runtime/actions/call-skill.js +45 -0
- package/src/runtime/actions/format.js +115 -0
- package/src/runtime/actions/print.js +42 -0
- package/src/runtime/actions/registry-delete.js +37 -0
- package/src/runtime/actions/registry-get.js +37 -0
- package/src/runtime/actions/registry-keys.js +33 -0
- package/src/runtime/actions/registry-search.js +34 -0
- package/src/runtime/actions/registry-set.js +50 -0
- package/src/runtime/actions/return.js +31 -0
- package/src/runtime/actions/send-message.js +58 -0
- package/src/runtime/actions/update-state.js +36 -0
- package/src/runtime/agent.js +1368 -0
- package/src/runtime/cli-logger.js +205 -0
- package/src/runtime/incremental-json-parser.js +201 -0
- package/src/runtime/index.js +33 -0
- package/src/runtime/llm-provider.js +1372 -0
- package/src/runtime/mcp-client.js +1171 -0
- package/src/runtime/planner.js +273 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
- package/src/runtime/registry-backends/local.js +260 -0
- package/src/runtime/registry.js +162 -0
- package/src/runtime/role.js +14 -0
- package/src/runtime/router.js +395 -0
- package/src/runtime/runtime.js +113 -0
- package/src/runtime/skill-selector.js +173 -0
- package/src/runtime/skill.js +25 -0
- package/src/runtime/team.js +162 -0
|
@@ -0,0 +1,1171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) Client - Full Implementation
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - WebSocket and HTTP/2 protocol support
|
|
6
|
+
* - Authentication and authorization
|
|
7
|
+
* - Server discovery
|
|
8
|
+
* - Connection pooling and load balancing
|
|
9
|
+
* - Retry logic and failover
|
|
10
|
+
* - Streaming responses
|
|
11
|
+
* - MCP tools integration
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
import WebSocket from 'ws';
|
|
16
|
+
import fetch from 'node-fetch';
|
|
17
|
+
|
|
18
|
+
// Configuration constants
|
|
19
|
+
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
20
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
21
|
+
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
|
22
|
+
const DEFAULT_POOL_SIZE = 5;
|
|
23
|
+
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
|
24
|
+
const CONNECTION_TIMEOUT = 10000; // 10 seconds
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Main MCP Client - manages connections, authentication, and discovery
|
|
28
|
+
*/
|
|
29
|
+
export class MCPClient extends EventEmitter {
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
// Basic configuration
|
|
34
|
+
this.config = {
|
|
35
|
+
timeout: config.timeout || DEFAULT_TIMEOUT,
|
|
36
|
+
maxRetries: config.maxRetries || DEFAULT_MAX_RETRIES,
|
|
37
|
+
retryDelay: config.retryDelay || DEFAULT_RETRY_DELAY,
|
|
38
|
+
poolSize: config.poolSize || DEFAULT_POOL_SIZE,
|
|
39
|
+
auth: config.auth || {},
|
|
40
|
+
registry: config.registry || null,
|
|
41
|
+
enableStreaming: config.enableStreaming !== false,
|
|
42
|
+
enableLoadBalancing: config.enableLoadBalancing !== false,
|
|
43
|
+
...config
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Connection management
|
|
47
|
+
this.connections = new Map(); // server -> MCPConnectionPool
|
|
48
|
+
this.cache = new Map(); // address -> resolved resource
|
|
49
|
+
this.registry = null; // Server registry for discovery
|
|
50
|
+
this.tools = new Map(); // Available MCP tools
|
|
51
|
+
|
|
52
|
+
// Load balancing
|
|
53
|
+
this.serverHealth = new Map(); // server -> health metrics
|
|
54
|
+
this.loadBalancer = new LoadBalancer(this);
|
|
55
|
+
|
|
56
|
+
// Authentication manager
|
|
57
|
+
this.authManager = new AuthenticationManager(this.config.auth);
|
|
58
|
+
|
|
59
|
+
// Initialize registry if configured
|
|
60
|
+
if (this.config.registry) {
|
|
61
|
+
this.initRegistry(this.config.registry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initialize server registry for discovery
|
|
67
|
+
*/
|
|
68
|
+
async initRegistry(registryUrl) {
|
|
69
|
+
try {
|
|
70
|
+
this.registry = new ServerRegistry(registryUrl);
|
|
71
|
+
await this.registry.connect();
|
|
72
|
+
console.log(`[MCP] Connected to registry: ${registryUrl}`);
|
|
73
|
+
this.emit('registry:connected', registryUrl);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(`[MCP] Failed to connect to registry:`, error.message);
|
|
76
|
+
this.emit('registry:error', error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse an MCP address
|
|
82
|
+
*/
|
|
83
|
+
parseAddress(address) {
|
|
84
|
+
if (typeof address === 'string' && address.startsWith('mcp://')) {
|
|
85
|
+
const url = new URL(address);
|
|
86
|
+
return {
|
|
87
|
+
server: url.host,
|
|
88
|
+
path: url.pathname.slice(1),
|
|
89
|
+
query: url.searchParams
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (address && address.type === 'MCPAddress') {
|
|
94
|
+
return {
|
|
95
|
+
server: address.server,
|
|
96
|
+
path: address.path,
|
|
97
|
+
query: null
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new Error(`Invalid MCP address: ${address}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve an MCP address to a resource
|
|
106
|
+
*/
|
|
107
|
+
async resolve(address, options = {}) {
|
|
108
|
+
const { server, path } = this.parseAddress(address);
|
|
109
|
+
const fullAddress = `mcp://${server}/${path}`;
|
|
110
|
+
|
|
111
|
+
// Check cache first (unless refresh is requested)
|
|
112
|
+
if (!options.refresh && this.cache.has(fullAddress)) {
|
|
113
|
+
return this.cache.get(fullAddress);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`[MCP] Resolving: ${fullAddress}`);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Get connection pool for server
|
|
120
|
+
const pool = await this.getConnectionPool(server);
|
|
121
|
+
|
|
122
|
+
// Get a connection from the pool
|
|
123
|
+
const connection = await pool.acquire();
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Resolve the resource
|
|
127
|
+
const resource = await connection.getResource(path);
|
|
128
|
+
|
|
129
|
+
// Cache the result
|
|
130
|
+
this.cache.set(fullAddress, resource);
|
|
131
|
+
|
|
132
|
+
return resource;
|
|
133
|
+
} finally {
|
|
134
|
+
// Release connection back to pool
|
|
135
|
+
pool.release(connection);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`[MCP] Failed to resolve ${fullAddress}:`, error.message);
|
|
139
|
+
|
|
140
|
+
// Try failover if configured
|
|
141
|
+
if (options.failover) {
|
|
142
|
+
return await this.resolveWithFailover(address, options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw new Error(`MCP resolution failed for ${fullAddress}: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve with automatic failover to alternative servers
|
|
151
|
+
*/
|
|
152
|
+
async resolveWithFailover(address, options = {}) {
|
|
153
|
+
const { path } = this.parseAddress(address);
|
|
154
|
+
|
|
155
|
+
// Get alternative servers from registry
|
|
156
|
+
const alternatives = await this.discoverServers(path);
|
|
157
|
+
|
|
158
|
+
for (const altServer of alternatives) {
|
|
159
|
+
try {
|
|
160
|
+
console.log(`[MCP] Trying failover server: ${altServer}`);
|
|
161
|
+
const altAddress = `mcp://${altServer}/${path}`;
|
|
162
|
+
return await this.resolve(altAddress, { ...options, failover: false });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn(`[MCP] Failover to ${altServer} failed:`, error.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw new Error(`All failover attempts failed for ${path}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get or create connection pool for a server
|
|
173
|
+
*/
|
|
174
|
+
async getConnectionPool(server) {
|
|
175
|
+
if (this.connections.has(server)) {
|
|
176
|
+
return this.connections.get(server);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`[MCP] Creating connection pool for: ${server}`);
|
|
180
|
+
|
|
181
|
+
// Get authentication credentials for this server
|
|
182
|
+
const auth = await this.authManager.getCredentials(server);
|
|
183
|
+
|
|
184
|
+
// Create connection pool
|
|
185
|
+
const pool = new MCPConnectionPool(server, {
|
|
186
|
+
size: this.config.poolSize,
|
|
187
|
+
auth,
|
|
188
|
+
timeout: this.config.timeout,
|
|
189
|
+
maxRetries: this.config.maxRetries,
|
|
190
|
+
retryDelay: this.config.retryDelay
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await pool.initialize();
|
|
194
|
+
this.connections.set(server, pool);
|
|
195
|
+
this.emit('pool:created', server);
|
|
196
|
+
|
|
197
|
+
return pool;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Send a message to an MCP address with retry logic
|
|
202
|
+
*/
|
|
203
|
+
async send(address, event, data, options = {}) {
|
|
204
|
+
const maxRetries = options.maxRetries || this.config.maxRetries;
|
|
205
|
+
let lastError;
|
|
206
|
+
|
|
207
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
208
|
+
try {
|
|
209
|
+
if (attempt > 0) {
|
|
210
|
+
const delay = this.config.retryDelay * Math.pow(2, attempt - 1); // Exponential backoff
|
|
211
|
+
console.log(`[MCP] Retry ${attempt}/${maxRetries} after ${delay}ms`);
|
|
212
|
+
await this.sleep(delay);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const resource = await this.resolve(address, options);
|
|
216
|
+
console.log(`[MCP] Sending ${event} to ${address}`);
|
|
217
|
+
|
|
218
|
+
// Handle streaming if enabled and supported
|
|
219
|
+
if (options.stream && this.config.enableStreaming) {
|
|
220
|
+
return await this.sendStreaming(resource, event, data, options);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Regular send
|
|
224
|
+
if (typeof resource.send === 'function') {
|
|
225
|
+
return await resource.send(event, data, options);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof resource[event] === 'function') {
|
|
229
|
+
return await resource[event](data);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
throw new Error(`Resource at ${address} does not handle event: ${event}`);
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
lastError = error;
|
|
236
|
+
console.warn(`[MCP] Send attempt ${attempt + 1} failed:`, error.message);
|
|
237
|
+
|
|
238
|
+
if (attempt === maxRetries) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(`Failed after ${maxRetries} retries: ${lastError.message}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Send with streaming response
|
|
249
|
+
*/
|
|
250
|
+
async sendStreaming(resource, event, data, options = {}) {
|
|
251
|
+
if (!resource.sendStream) {
|
|
252
|
+
throw new Error('Resource does not support streaming');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const stream = await resource.sendStream(event, data, options);
|
|
256
|
+
|
|
257
|
+
// Return async iterator for streaming
|
|
258
|
+
return {
|
|
259
|
+
[Symbol.asyncIterator]: async function* () {
|
|
260
|
+
for await (const chunk of stream) {
|
|
261
|
+
yield chunk;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Discover servers that provide a specific capability/resource
|
|
269
|
+
*/
|
|
270
|
+
async discoverServers(resourcePath, options = {}) {
|
|
271
|
+
if (!this.registry) {
|
|
272
|
+
console.warn('[MCP] No registry configured for server discovery');
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const servers = await this.registry.discover(resourcePath, options);
|
|
278
|
+
console.log(`[MCP] Discovered ${servers.length} servers for ${resourcePath}`);
|
|
279
|
+
return servers;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('[MCP] Server discovery failed:', error.message);
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get available tools from an MCP server
|
|
288
|
+
*/
|
|
289
|
+
async getTools(server) {
|
|
290
|
+
const cacheKey = `tools:${server}`;
|
|
291
|
+
|
|
292
|
+
if (this.tools.has(cacheKey)) {
|
|
293
|
+
return this.tools.get(cacheKey);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const pool = await this.getConnectionPool(server);
|
|
298
|
+
const connection = await pool.acquire();
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const tools = await connection.listTools();
|
|
302
|
+
this.tools.set(cacheKey, tools);
|
|
303
|
+
return tools;
|
|
304
|
+
} finally {
|
|
305
|
+
pool.release(connection);
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error(`[MCP] Failed to get tools from ${server}:`, error.message);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Invoke an MCP tool
|
|
315
|
+
*/
|
|
316
|
+
async invokeTool(server, toolName, args) {
|
|
317
|
+
console.log(`[MCP] Invoking tool ${toolName} on ${server}`);
|
|
318
|
+
|
|
319
|
+
const pool = await this.getConnectionPool(server);
|
|
320
|
+
const connection = await pool.acquire();
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
return await connection.invokeTool(toolName, args);
|
|
324
|
+
} finally {
|
|
325
|
+
pool.release(connection);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get health metrics for a server
|
|
331
|
+
*/
|
|
332
|
+
getServerHealth(server) {
|
|
333
|
+
return this.serverHealth.get(server) || {
|
|
334
|
+
status: 'unknown',
|
|
335
|
+
latency: null,
|
|
336
|
+
successRate: null,
|
|
337
|
+
lastCheck: null
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Update health metrics for a server
|
|
343
|
+
*/
|
|
344
|
+
updateServerHealth(server, metrics) {
|
|
345
|
+
const existing = this.serverHealth.get(server) || {};
|
|
346
|
+
this.serverHealth.set(server, {
|
|
347
|
+
...existing,
|
|
348
|
+
...metrics,
|
|
349
|
+
lastCheck: Date.now()
|
|
350
|
+
});
|
|
351
|
+
this.emit('health:updated', server, metrics);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Disconnect from a server
|
|
356
|
+
*/
|
|
357
|
+
async disconnect(server) {
|
|
358
|
+
const pool = this.connections.get(server);
|
|
359
|
+
if (pool) {
|
|
360
|
+
await pool.destroy();
|
|
361
|
+
this.connections.delete(server);
|
|
362
|
+
this.emit('disconnected', server);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Disconnect from all servers
|
|
368
|
+
*/
|
|
369
|
+
async disconnectAll() {
|
|
370
|
+
const servers = Array.from(this.connections.keys());
|
|
371
|
+
await Promise.all(servers.map(server => this.disconnect(server)));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Clear the resolution cache
|
|
376
|
+
*/
|
|
377
|
+
clearCache() {
|
|
378
|
+
this.cache.clear();
|
|
379
|
+
this.tools.clear();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Utility: sleep
|
|
384
|
+
*/
|
|
385
|
+
sleep(ms) {
|
|
386
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Connection Pool - manages multiple connections to a single server
|
|
392
|
+
*/
|
|
393
|
+
class MCPConnectionPool extends EventEmitter {
|
|
394
|
+
constructor(server, options = {}) {
|
|
395
|
+
super();
|
|
396
|
+
this.server = server;
|
|
397
|
+
this.options = options;
|
|
398
|
+
this.size = options.size || DEFAULT_POOL_SIZE;
|
|
399
|
+
this.connections = [];
|
|
400
|
+
this.available = [];
|
|
401
|
+
this.waiting = [];
|
|
402
|
+
this.initialized = false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async initialize() {
|
|
406
|
+
console.log(`[MCP:Pool] Initializing pool for ${this.server} (size: ${this.size})`);
|
|
407
|
+
|
|
408
|
+
// Create initial connections
|
|
409
|
+
for (let i = 0; i < this.size; i++) {
|
|
410
|
+
try {
|
|
411
|
+
const connection = await this.createConnection();
|
|
412
|
+
this.connections.push(connection);
|
|
413
|
+
this.available.push(connection);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error(`[MCP:Pool] Failed to create connection ${i + 1}:`, error.message);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (this.available.length === 0) {
|
|
420
|
+
throw new Error(`Failed to create any connections to ${this.server}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this.initialized = true;
|
|
424
|
+
this.emit('initialized', this.server);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async createConnection() {
|
|
428
|
+
const connection = new MCPConnection(this.server, this.options);
|
|
429
|
+
await connection.connect();
|
|
430
|
+
|
|
431
|
+
// Set up connection event handlers
|
|
432
|
+
connection.on('close', () => this.handleConnectionClose(connection));
|
|
433
|
+
connection.on('error', (error) => this.handleConnectionError(connection, error));
|
|
434
|
+
|
|
435
|
+
return connection;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async acquire() {
|
|
439
|
+
if (!this.initialized) {
|
|
440
|
+
throw new Error('Pool not initialized');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// If a connection is available, return it immediately
|
|
444
|
+
if (this.available.length > 0) {
|
|
445
|
+
return this.available.shift();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Otherwise, wait for one to become available
|
|
449
|
+
return new Promise((resolve, reject) => {
|
|
450
|
+
const timeout = setTimeout(() => {
|
|
451
|
+
const index = this.waiting.indexOf(waiter);
|
|
452
|
+
if (index > -1) {
|
|
453
|
+
this.waiting.splice(index, 1);
|
|
454
|
+
}
|
|
455
|
+
reject(new Error('Timeout waiting for connection'));
|
|
456
|
+
}, CONNECTION_TIMEOUT);
|
|
457
|
+
|
|
458
|
+
const waiter = { resolve, reject, timeout };
|
|
459
|
+
this.waiting.push(waiter);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
release(connection) {
|
|
464
|
+
// If there are waiters, give them the connection
|
|
465
|
+
if (this.waiting.length > 0) {
|
|
466
|
+
const waiter = this.waiting.shift();
|
|
467
|
+
clearTimeout(waiter.timeout);
|
|
468
|
+
waiter.resolve(connection);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Otherwise, add it back to available pool
|
|
473
|
+
if (!this.available.includes(connection)) {
|
|
474
|
+
this.available.push(connection);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
handleConnectionClose(connection) {
|
|
479
|
+
console.log(`[MCP:Pool] Connection closed in pool for ${this.server}`);
|
|
480
|
+
this.removeConnection(connection);
|
|
481
|
+
|
|
482
|
+
// Try to replace the connection
|
|
483
|
+
this.createConnection()
|
|
484
|
+
.then(newConnection => {
|
|
485
|
+
this.connections.push(newConnection);
|
|
486
|
+
this.available.push(newConnection);
|
|
487
|
+
})
|
|
488
|
+
.catch(error => {
|
|
489
|
+
console.error(`[MCP:Pool] Failed to replace connection:`, error.message);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
handleConnectionError(connection, error) {
|
|
494
|
+
console.error(`[MCP:Pool] Connection error:`, error.message);
|
|
495
|
+
this.emit('error', error);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
removeConnection(connection) {
|
|
499
|
+
const connIndex = this.connections.indexOf(connection);
|
|
500
|
+
if (connIndex > -1) {
|
|
501
|
+
this.connections.splice(connIndex, 1);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const availIndex = this.available.indexOf(connection);
|
|
505
|
+
if (availIndex > -1) {
|
|
506
|
+
this.available.splice(availIndex, 1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async destroy() {
|
|
511
|
+
console.log(`[MCP:Pool] Destroying pool for ${this.server}`);
|
|
512
|
+
|
|
513
|
+
// Reject all waiting requests
|
|
514
|
+
this.waiting.forEach(waiter => {
|
|
515
|
+
clearTimeout(waiter.timeout);
|
|
516
|
+
waiter.reject(new Error('Pool destroyed'));
|
|
517
|
+
});
|
|
518
|
+
this.waiting = [];
|
|
519
|
+
|
|
520
|
+
// Close all connections
|
|
521
|
+
await Promise.all(
|
|
522
|
+
this.connections.map(conn => conn.disconnect().catch(err => {
|
|
523
|
+
console.error('[MCP:Pool] Error closing connection:', err);
|
|
524
|
+
}))
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
this.connections = [];
|
|
528
|
+
this.available = [];
|
|
529
|
+
this.initialized = false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Single MCP Connection - handles protocol communication
|
|
535
|
+
*/
|
|
536
|
+
class MCPConnection extends EventEmitter {
|
|
537
|
+
constructor(server, options = {}) {
|
|
538
|
+
super();
|
|
539
|
+
this.server = server;
|
|
540
|
+
this.options = options;
|
|
541
|
+
this.connected = false;
|
|
542
|
+
this.resources = new Map();
|
|
543
|
+
this.ws = null;
|
|
544
|
+
this.mode = null;
|
|
545
|
+
this.messageId = 0;
|
|
546
|
+
this.pendingRequests = new Map();
|
|
547
|
+
this.heartbeatInterval = null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async connect() {
|
|
551
|
+
// Determine connection mode based on server address
|
|
552
|
+
if (this.server === 'localhost' || this.server.endsWith('.local')) {
|
|
553
|
+
await this.connectLocal();
|
|
554
|
+
} else if (this.server.startsWith('ws://') || this.server.startsWith('wss://')) {
|
|
555
|
+
await this.connectWebSocket();
|
|
556
|
+
} else {
|
|
557
|
+
await this.connectHTTP();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
this.startHeartbeat();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async connectLocal() {
|
|
564
|
+
this.mode = 'local';
|
|
565
|
+
this.connected = true;
|
|
566
|
+
console.log(`[MCP] Connected to ${this.server} (local simulation mode)`);
|
|
567
|
+
this.emit('connected');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async connectWebSocket() {
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
const wsUrl = this.server.startsWith('ws') ? this.server : `wss://${this.server}`;
|
|
573
|
+
|
|
574
|
+
const headers = {};
|
|
575
|
+
if (this.options.auth && this.options.auth.token) {
|
|
576
|
+
headers['Authorization'] = `Bearer ${this.options.auth.token}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
this.ws = new WebSocket(wsUrl, { headers });
|
|
580
|
+
|
|
581
|
+
const timeout = setTimeout(() => {
|
|
582
|
+
reject(new Error(`WebSocket connection timeout: ${this.server}`));
|
|
583
|
+
this.ws.close();
|
|
584
|
+
}, CONNECTION_TIMEOUT);
|
|
585
|
+
|
|
586
|
+
this.ws.on('open', () => {
|
|
587
|
+
clearTimeout(timeout);
|
|
588
|
+
this.mode = 'websocket';
|
|
589
|
+
this.connected = true;
|
|
590
|
+
console.log(`[MCP] Connected to ${this.server} (WebSocket)`);
|
|
591
|
+
this.emit('connected');
|
|
592
|
+
resolve();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
this.ws.on('message', (data) => {
|
|
596
|
+
this.handleMessage(data);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
this.ws.on('error', (error) => {
|
|
600
|
+
clearTimeout(timeout);
|
|
601
|
+
console.error(`[MCP] WebSocket error:`, error.message);
|
|
602
|
+
this.emit('error', error);
|
|
603
|
+
if (!this.connected) {
|
|
604
|
+
reject(error);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
this.ws.on('close', () => {
|
|
609
|
+
this.connected = false;
|
|
610
|
+
this.stopHeartbeat();
|
|
611
|
+
console.log(`[MCP] WebSocket connection closed: ${this.server}`);
|
|
612
|
+
this.emit('close');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
this.ws.on('ping', () => {
|
|
616
|
+
this.ws.pong();
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async connectHTTP() {
|
|
622
|
+
this.mode = 'http';
|
|
623
|
+
this.baseUrl = this.server.startsWith('http') ? this.server : `https://${this.server}`;
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
// Test connection with a ping
|
|
627
|
+
const response = await fetch(`${this.baseUrl}/mcp/v1/ping`, {
|
|
628
|
+
method: 'GET',
|
|
629
|
+
headers: this.getHTTPHeaders(),
|
|
630
|
+
timeout: CONNECTION_TIMEOUT
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (!response.ok) {
|
|
634
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
this.connected = true;
|
|
638
|
+
console.log(`[MCP] Connected to ${this.server} (HTTP/2)`);
|
|
639
|
+
this.emit('connected');
|
|
640
|
+
|
|
641
|
+
} catch (error) {
|
|
642
|
+
throw new Error(`Failed to connect via HTTP: ${error.message}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
getHTTPHeaders() {
|
|
647
|
+
const headers = {
|
|
648
|
+
'Content-Type': 'application/json',
|
|
649
|
+
'Accept': 'application/json'
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
if (this.options.auth && this.options.auth.token) {
|
|
653
|
+
headers['Authorization'] = `Bearer ${this.options.auth.token}`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return headers;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
handleMessage(data) {
|
|
660
|
+
try {
|
|
661
|
+
const message = JSON.parse(data.toString());
|
|
662
|
+
|
|
663
|
+
if (message.id && this.pendingRequests.has(message.id)) {
|
|
664
|
+
const { resolve, reject } = this.pendingRequests.get(message.id);
|
|
665
|
+
this.pendingRequests.delete(message.id);
|
|
666
|
+
|
|
667
|
+
if (message.error) {
|
|
668
|
+
reject(new Error(message.error));
|
|
669
|
+
} else {
|
|
670
|
+
resolve(message.result);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
// Handle server-initiated messages (events, etc.)
|
|
674
|
+
this.emit('message', message);
|
|
675
|
+
}
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.error('[MCP] Failed to parse message:', error.message);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async sendRequest(method, params) {
|
|
682
|
+
if (!this.connected) {
|
|
683
|
+
throw new Error('Not connected to MCP server');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (this.mode === 'websocket') {
|
|
687
|
+
return this.sendWebSocketRequest(method, params);
|
|
688
|
+
} else if (this.mode === 'http') {
|
|
689
|
+
return this.sendHTTPRequest(method, params);
|
|
690
|
+
} else {
|
|
691
|
+
throw new Error(`Unsupported mode: ${this.mode}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async sendWebSocketRequest(method, params) {
|
|
696
|
+
return new Promise((resolve, reject) => {
|
|
697
|
+
const id = ++this.messageId;
|
|
698
|
+
const message = JSON.stringify({ id, method, params });
|
|
699
|
+
|
|
700
|
+
const timeout = setTimeout(() => {
|
|
701
|
+
this.pendingRequests.delete(id);
|
|
702
|
+
reject(new Error(`Request timeout: ${method}`));
|
|
703
|
+
}, this.options.timeout || DEFAULT_TIMEOUT);
|
|
704
|
+
|
|
705
|
+
this.pendingRequests.set(id, {
|
|
706
|
+
resolve: (result) => {
|
|
707
|
+
clearTimeout(timeout);
|
|
708
|
+
resolve(result);
|
|
709
|
+
},
|
|
710
|
+
reject: (error) => {
|
|
711
|
+
clearTimeout(timeout);
|
|
712
|
+
reject(error);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
this.ws.send(message);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async sendHTTPRequest(method, params) {
|
|
721
|
+
const response = await fetch(`${this.baseUrl}/mcp/v1/call`, {
|
|
722
|
+
method: 'POST',
|
|
723
|
+
headers: this.getHTTPHeaders(),
|
|
724
|
+
body: JSON.stringify({ method, params }),
|
|
725
|
+
timeout: this.options.timeout || DEFAULT_TIMEOUT
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
if (!response.ok) {
|
|
729
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const data = await response.json();
|
|
733
|
+
|
|
734
|
+
if (data.error) {
|
|
735
|
+
throw new Error(data.error);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return data.result;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async getResource(path) {
|
|
742
|
+
if (!this.connected) {
|
|
743
|
+
throw new Error('Not connected to MCP server');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Check cache
|
|
747
|
+
if (this.resources.has(path)) {
|
|
748
|
+
return this.resources.get(path);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (this.mode === 'local') {
|
|
752
|
+
return this.getLocalResource(path);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Request resource metadata from server
|
|
756
|
+
const metadata = await this.sendRequest('resource.get', { path });
|
|
757
|
+
|
|
758
|
+
// Create resource proxy
|
|
759
|
+
const resource = new MCPResource(this, path, metadata);
|
|
760
|
+
this.resources.set(path, resource);
|
|
761
|
+
|
|
762
|
+
return resource;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
getLocalResource(path) {
|
|
766
|
+
const resource = {
|
|
767
|
+
server: this.server,
|
|
768
|
+
path,
|
|
769
|
+
type: 'agent',
|
|
770
|
+
mode: 'local',
|
|
771
|
+
|
|
772
|
+
async send(event, data) {
|
|
773
|
+
console.log(`[MCP:Local] ${event} on ${path}`);
|
|
774
|
+
return {
|
|
775
|
+
status: 'ok',
|
|
776
|
+
message: `Simulated response from mcp://${this.server}/${path}`,
|
|
777
|
+
event,
|
|
778
|
+
...data,
|
|
779
|
+
__simulated: true
|
|
780
|
+
};
|
|
781
|
+
},
|
|
782
|
+
|
|
783
|
+
async handle(event, data) {
|
|
784
|
+
return this.send(event, data);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
this.resources.set(path, resource);
|
|
789
|
+
return resource;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async listTools() {
|
|
793
|
+
if (this.mode === 'local') {
|
|
794
|
+
return []; // No tools in local mode
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return await this.sendRequest('tools.list', {});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async invokeTool(toolName, args) {
|
|
801
|
+
if (this.mode === 'local') {
|
|
802
|
+
throw new Error('Tools not available in local mode');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return await this.sendRequest('tools.invoke', { name: toolName, args });
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
startHeartbeat() {
|
|
809
|
+
if (this.mode === 'local') return;
|
|
810
|
+
|
|
811
|
+
this.heartbeatInterval = setInterval(() => {
|
|
812
|
+
if (this.mode === 'websocket' && this.ws) {
|
|
813
|
+
this.ws.ping();
|
|
814
|
+
} else if (this.mode === 'http') {
|
|
815
|
+
// HTTP heartbeat via ping endpoint
|
|
816
|
+
fetch(`${this.baseUrl}/mcp/v1/ping`, {
|
|
817
|
+
method: 'GET',
|
|
818
|
+
headers: this.getHTTPHeaders()
|
|
819
|
+
}).catch(() => {
|
|
820
|
+
// Heartbeat failed, emit error
|
|
821
|
+
this.emit('error', new Error('Heartbeat failed'));
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}, HEARTBEAT_INTERVAL);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
stopHeartbeat() {
|
|
828
|
+
if (this.heartbeatInterval) {
|
|
829
|
+
clearInterval(this.heartbeatInterval);
|
|
830
|
+
this.heartbeatInterval = null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async disconnect() {
|
|
835
|
+
this.stopHeartbeat();
|
|
836
|
+
this.connected = false;
|
|
837
|
+
this.resources.clear();
|
|
838
|
+
|
|
839
|
+
if (this.ws) {
|
|
840
|
+
this.ws.close();
|
|
841
|
+
this.ws = null;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Reject all pending requests
|
|
845
|
+
this.pendingRequests.forEach(({ reject }) => {
|
|
846
|
+
reject(new Error('Connection closed'));
|
|
847
|
+
});
|
|
848
|
+
this.pendingRequests.clear();
|
|
849
|
+
|
|
850
|
+
this.emit('disconnected');
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* MCP Resource - represents a remote resource/agent
|
|
856
|
+
*/
|
|
857
|
+
class MCPResource {
|
|
858
|
+
constructor(connection, path, metadata) {
|
|
859
|
+
this.connection = connection;
|
|
860
|
+
this.path = path;
|
|
861
|
+
this.metadata = metadata;
|
|
862
|
+
this.type = metadata.type || 'agent';
|
|
863
|
+
this.capabilities = metadata.capabilities || [];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async send(event, data, options = {}) {
|
|
867
|
+
return await this.connection.sendRequest('resource.invoke', {
|
|
868
|
+
path: this.path,
|
|
869
|
+
event,
|
|
870
|
+
data,
|
|
871
|
+
options
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async sendStream(event, data, options = {}) {
|
|
876
|
+
if (!this.capabilities.includes('streaming')) {
|
|
877
|
+
throw new Error('Resource does not support streaming');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// For WebSocket, use streaming protocol
|
|
881
|
+
if (this.connection.mode === 'websocket') {
|
|
882
|
+
return this.streamWebSocket(event, data, options);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// For HTTP, use chunked transfer
|
|
886
|
+
return this.streamHTTP(event, data, options);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async *streamWebSocket(event, data, options) {
|
|
890
|
+
const id = ++this.connection.messageId;
|
|
891
|
+
const message = JSON.stringify({
|
|
892
|
+
id,
|
|
893
|
+
method: 'resource.stream',
|
|
894
|
+
params: { path: this.path, event, data, options }
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// Set up stream handler
|
|
898
|
+
const chunks = [];
|
|
899
|
+
let streamEnded = false;
|
|
900
|
+
let streamError = null;
|
|
901
|
+
|
|
902
|
+
const handler = (msg) => {
|
|
903
|
+
if (msg.id === id) {
|
|
904
|
+
if (msg.chunk) {
|
|
905
|
+
chunks.push(msg.chunk);
|
|
906
|
+
}
|
|
907
|
+
if (msg.done) {
|
|
908
|
+
streamEnded = true;
|
|
909
|
+
}
|
|
910
|
+
if (msg.error) {
|
|
911
|
+
streamError = new Error(msg.error);
|
|
912
|
+
streamEnded = true;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
this.connection.on('message', handler);
|
|
918
|
+
this.connection.ws.send(message);
|
|
919
|
+
|
|
920
|
+
try {
|
|
921
|
+
// Yield chunks as they arrive
|
|
922
|
+
while (!streamEnded) {
|
|
923
|
+
if (chunks.length > 0) {
|
|
924
|
+
yield chunks.shift();
|
|
925
|
+
} else {
|
|
926
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Yield any remaining chunks
|
|
931
|
+
while (chunks.length > 0) {
|
|
932
|
+
yield chunks.shift();
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (streamError) {
|
|
936
|
+
throw streamError;
|
|
937
|
+
}
|
|
938
|
+
} finally {
|
|
939
|
+
this.connection.off('message', handler);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async *streamHTTP(event, data, options) {
|
|
944
|
+
const response = await fetch(`${this.connection.baseUrl}/mcp/v1/stream`, {
|
|
945
|
+
method: 'POST',
|
|
946
|
+
headers: this.connection.getHTTPHeaders(),
|
|
947
|
+
body: JSON.stringify({
|
|
948
|
+
path: this.path,
|
|
949
|
+
event,
|
|
950
|
+
data,
|
|
951
|
+
options
|
|
952
|
+
})
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
if (!response.ok) {
|
|
956
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Read streaming response
|
|
960
|
+
const reader = response.body.getReader();
|
|
961
|
+
const decoder = new TextDecoder();
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
while (true) {
|
|
965
|
+
const { done, value } = await reader.read();
|
|
966
|
+
|
|
967
|
+
if (done) break;
|
|
968
|
+
|
|
969
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
970
|
+
const lines = chunk.split('\n').filter(line => line.trim());
|
|
971
|
+
|
|
972
|
+
for (const line of lines) {
|
|
973
|
+
if (line.startsWith('data: ')) {
|
|
974
|
+
const data = JSON.parse(line.slice(6));
|
|
975
|
+
yield data;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
} finally {
|
|
980
|
+
reader.releaseLock();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async handle(event, data) {
|
|
985
|
+
return this.send(event, data);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Authentication Manager
|
|
991
|
+
*/
|
|
992
|
+
class AuthenticationManager {
|
|
993
|
+
constructor(config = {}) {
|
|
994
|
+
this.credentials = new Map();
|
|
995
|
+
this.tokenCache = new Map();
|
|
996
|
+
|
|
997
|
+
// Load initial credentials
|
|
998
|
+
if (config.credentials) {
|
|
999
|
+
Object.entries(config.credentials).forEach(([server, creds]) => {
|
|
1000
|
+
this.credentials.set(server, creds);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async getCredentials(server) {
|
|
1006
|
+
// Check if we have cached credentials
|
|
1007
|
+
if (this.credentials.has(server)) {
|
|
1008
|
+
return this.credentials.get(server);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Try to get from environment
|
|
1012
|
+
const envKey = `MCP_AUTH_${server.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
|
1013
|
+
if (process.env[envKey]) {
|
|
1014
|
+
const token = process.env[envKey];
|
|
1015
|
+
const creds = { token };
|
|
1016
|
+
this.credentials.set(server, creds);
|
|
1017
|
+
return creds;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
setCredentials(server, credentials) {
|
|
1024
|
+
this.credentials.set(server, credentials);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
clearCredentials(server) {
|
|
1028
|
+
this.credentials.delete(server);
|
|
1029
|
+
this.tokenCache.delete(server);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Server Registry - for service discovery
|
|
1035
|
+
*/
|
|
1036
|
+
class ServerRegistry extends EventEmitter {
|
|
1037
|
+
constructor(registryUrl) {
|
|
1038
|
+
super();
|
|
1039
|
+
this.registryUrl = registryUrl;
|
|
1040
|
+
this.cache = new Map();
|
|
1041
|
+
this.cacheExpiry = 60000; // 1 minute
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async connect() {
|
|
1045
|
+
// Verify registry is accessible
|
|
1046
|
+
try {
|
|
1047
|
+
const response = await fetch(`${this.registryUrl}/health`, { timeout: 5000 });
|
|
1048
|
+
if (!response.ok) {
|
|
1049
|
+
throw new Error(`Registry unhealthy: ${response.status}`);
|
|
1050
|
+
}
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
throw new Error(`Failed to connect to registry: ${error.message}`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async discover(resourcePath, options = {}) {
|
|
1057
|
+
const cacheKey = `discover:${resourcePath}`;
|
|
1058
|
+
|
|
1059
|
+
// Check cache
|
|
1060
|
+
const cached = this.cache.get(cacheKey);
|
|
1061
|
+
if (cached && Date.now() - cached.timestamp < this.cacheExpiry) {
|
|
1062
|
+
return cached.servers;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Query registry
|
|
1066
|
+
try {
|
|
1067
|
+
const response = await fetch(`${this.registryUrl}/discover`, {
|
|
1068
|
+
method: 'POST',
|
|
1069
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1070
|
+
body: JSON.stringify({ resource: resourcePath, ...options })
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
if (!response.ok) {
|
|
1074
|
+
throw new Error(`Registry query failed: ${response.status}`);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const data = await response.json();
|
|
1078
|
+
const servers = data.servers || [];
|
|
1079
|
+
|
|
1080
|
+
// Cache result
|
|
1081
|
+
this.cache.set(cacheKey, {
|
|
1082
|
+
servers,
|
|
1083
|
+
timestamp: Date.now()
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
return servers;
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
console.error('[Registry] Discovery failed:', error.message);
|
|
1089
|
+
|
|
1090
|
+
// Return cached result if available (even if expired)
|
|
1091
|
+
if (cached) {
|
|
1092
|
+
console.warn('[Registry] Using stale cache');
|
|
1093
|
+
return cached.servers;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return [];
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Load Balancer
|
|
1103
|
+
*/
|
|
1104
|
+
class LoadBalancer {
|
|
1105
|
+
constructor(client) {
|
|
1106
|
+
this.client = client;
|
|
1107
|
+
this.strategy = 'round-robin'; // or 'least-connections', 'random', 'weighted'
|
|
1108
|
+
this.counters = new Map();
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
selectServer(servers) {
|
|
1112
|
+
if (servers.length === 0) {
|
|
1113
|
+
throw new Error('No servers available');
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (servers.length === 1) {
|
|
1117
|
+
return servers[0];
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
switch (this.strategy) {
|
|
1121
|
+
case 'round-robin':
|
|
1122
|
+
return this.roundRobin(servers);
|
|
1123
|
+
|
|
1124
|
+
case 'random':
|
|
1125
|
+
return servers[Math.floor(Math.random() * servers.length)];
|
|
1126
|
+
|
|
1127
|
+
case 'least-latency':
|
|
1128
|
+
return this.leastLatency(servers);
|
|
1129
|
+
|
|
1130
|
+
default:
|
|
1131
|
+
return servers[0];
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
roundRobin(servers) {
|
|
1136
|
+
const key = servers.join(',');
|
|
1137
|
+
const counter = this.counters.get(key) || 0;
|
|
1138
|
+
const index = counter % servers.length;
|
|
1139
|
+
this.counters.set(key, counter + 1);
|
|
1140
|
+
return servers[index];
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
leastLatency(servers) {
|
|
1144
|
+
let bestServer = servers[0];
|
|
1145
|
+
let bestLatency = Infinity;
|
|
1146
|
+
|
|
1147
|
+
for (const server of servers) {
|
|
1148
|
+
const health = this.client.getServerHealth(server);
|
|
1149
|
+
if (health.latency !== null && health.latency < bestLatency) {
|
|
1150
|
+
bestLatency = health.latency;
|
|
1151
|
+
bestServer = server;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return bestServer;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Global MCP client instance
|
|
1160
|
+
export const mcpClient = new MCPClient();
|
|
1161
|
+
|
|
1162
|
+
// Load configuration from environment
|
|
1163
|
+
if (process.env.KOI_MCP_DEBUG) {
|
|
1164
|
+
mcpClient.on('connected', (server) => console.log(`[MCP:Debug] Connected: ${server}`));
|
|
1165
|
+
mcpClient.on('disconnected', (server) => console.log(`[MCP:Debug] Disconnected: ${server}`));
|
|
1166
|
+
mcpClient.on('error', (error) => console.error(`[MCP:Debug] Error:`, error));
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (process.env.KOI_MCP_REGISTRY) {
|
|
1170
|
+
mcpClient.initRegistry(process.env.KOI_MCP_REGISTRY);
|
|
1171
|
+
}
|