@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.
Files changed (85) hide show
  1. package/QUICKSTART.md +89 -0
  2. package/README.md +545 -0
  3. package/examples/actions-demo.koi +177 -0
  4. package/examples/cache-test.koi +29 -0
  5. package/examples/calculator.koi +61 -0
  6. package/examples/clear-registry.js +33 -0
  7. package/examples/clear-registry.koi +30 -0
  8. package/examples/code-introspection-test.koi +149 -0
  9. package/examples/counter.koi +132 -0
  10. package/examples/delegation-test.koi +52 -0
  11. package/examples/directory-import-test.koi +84 -0
  12. package/examples/hello-world-claude.koi +52 -0
  13. package/examples/hello-world.koi +52 -0
  14. package/examples/hello.koi +24 -0
  15. package/examples/mcp-example.koi +70 -0
  16. package/examples/multi-event-handler-test.koi +144 -0
  17. package/examples/new-import-test.koi +89 -0
  18. package/examples/pipeline.koi +162 -0
  19. package/examples/registry-demo.koi +184 -0
  20. package/examples/registry-playbook-demo.koi +162 -0
  21. package/examples/registry-playbook-email-compositor-2.koi +140 -0
  22. package/examples/registry-playbook-email-compositor.koi +140 -0
  23. package/examples/sentiment.koi +90 -0
  24. package/examples/simple.koi +48 -0
  25. package/examples/skill-import-test.koi +76 -0
  26. package/examples/skills/advanced/index.koi +95 -0
  27. package/examples/skills/math-operations.koi +69 -0
  28. package/examples/skills/string-operations.koi +56 -0
  29. package/examples/task-chaining-demo.koi +244 -0
  30. package/examples/test-await.koi +22 -0
  31. package/examples/test-crypto-sha256.koi +196 -0
  32. package/examples/test-delegation.koi +41 -0
  33. package/examples/test-multi-team-routing.koi +258 -0
  34. package/examples/test-no-handler.koi +35 -0
  35. package/examples/test-npm-import.koi +67 -0
  36. package/examples/test-parse.koi +10 -0
  37. package/examples/test-peers-with-team.koi +59 -0
  38. package/examples/test-permissions-fail.koi +20 -0
  39. package/examples/test-permissions.koi +36 -0
  40. package/examples/test-simple-registry.koi +31 -0
  41. package/examples/test-typescript-import.koi +64 -0
  42. package/examples/test-uses-team-syntax.koi +25 -0
  43. package/examples/test-uses-team.koi +31 -0
  44. package/examples/utils/calculator.test.ts +144 -0
  45. package/examples/utils/calculator.ts +56 -0
  46. package/examples/utils/math-helpers.js +50 -0
  47. package/examples/utils/math-helpers.ts +55 -0
  48. package/examples/web-delegation-demo.koi +165 -0
  49. package/package.json +78 -0
  50. package/src/cli/koi.js +793 -0
  51. package/src/compiler/build-optimizer.js +447 -0
  52. package/src/compiler/cache-manager.js +274 -0
  53. package/src/compiler/import-resolver.js +369 -0
  54. package/src/compiler/parser.js +7542 -0
  55. package/src/compiler/transpiler.js +1105 -0
  56. package/src/compiler/typescript-transpiler.js +148 -0
  57. package/src/grammar/koi.pegjs +767 -0
  58. package/src/runtime/action-registry.js +172 -0
  59. package/src/runtime/actions/call-skill.js +45 -0
  60. package/src/runtime/actions/format.js +115 -0
  61. package/src/runtime/actions/print.js +42 -0
  62. package/src/runtime/actions/registry-delete.js +37 -0
  63. package/src/runtime/actions/registry-get.js +37 -0
  64. package/src/runtime/actions/registry-keys.js +33 -0
  65. package/src/runtime/actions/registry-search.js +34 -0
  66. package/src/runtime/actions/registry-set.js +50 -0
  67. package/src/runtime/actions/return.js +31 -0
  68. package/src/runtime/actions/send-message.js +58 -0
  69. package/src/runtime/actions/update-state.js +36 -0
  70. package/src/runtime/agent.js +1368 -0
  71. package/src/runtime/cli-logger.js +205 -0
  72. package/src/runtime/incremental-json-parser.js +201 -0
  73. package/src/runtime/index.js +33 -0
  74. package/src/runtime/llm-provider.js +1372 -0
  75. package/src/runtime/mcp-client.js +1171 -0
  76. package/src/runtime/planner.js +273 -0
  77. package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
  78. package/src/runtime/registry-backends/local.js +260 -0
  79. package/src/runtime/registry.js +162 -0
  80. package/src/runtime/role.js +14 -0
  81. package/src/runtime/router.js +395 -0
  82. package/src/runtime/runtime.js +113 -0
  83. package/src/runtime/skill-selector.js +173 -0
  84. package/src/runtime/skill.js +25 -0
  85. 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
+ }