@adversity/coding-tool-x 2.5.0 → 2.6.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.
@@ -0,0 +1,775 @@
1
+ /**
2
+ * MCP JSON-RPC Client Wrapper
3
+ *
4
+ * Reusable client for communicating with MCP servers over stdio or HTTP/SSE
5
+ * transports using the JSON-RPC 2.0 protocol.
6
+ *
7
+ * Usage:
8
+ * const client = new McpClient({ type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-time'] });
9
+ * await client.connect();
10
+ * await client.initialize();
11
+ * const tools = await client.listTools();
12
+ * const result = await client.callTool('get_current_time', { timezone: 'UTC' });
13
+ * await client.disconnect();
14
+ */
15
+
16
+ const { spawn } = require('child_process');
17
+ const http = require('http');
18
+ const https = require('https');
19
+ const { EventEmitter } = require('events');
20
+
21
+ const DEFAULT_TIMEOUT = 10000; // 10 seconds
22
+ const JSONRPC_VERSION = '2.0';
23
+ const MCP_PROTOCOL_VERSION = '2024-11-05';
24
+
25
+ // ============================================================================
26
+ // McpClient
27
+ // ============================================================================
28
+
29
+ class McpClient extends EventEmitter {
30
+ /**
31
+ * @param {object} serverSpec - Server specification
32
+ * @param {string} [serverSpec.type='stdio'] - Transport type: 'stdio' | 'http' | 'sse'
33
+ * @param {string} [serverSpec.command] - Command for stdio transport
34
+ * @param {string[]} [serverSpec.args] - Args for stdio transport
35
+ * @param {object} [serverSpec.env] - Additional env vars for stdio transport
36
+ * @param {string} [serverSpec.cwd] - Working directory for stdio transport
37
+ * @param {string} [serverSpec.url] - URL for http/sse transport
38
+ * @param {object} [serverSpec.headers] - Additional headers for http/sse transport
39
+ * @param {object} [options] - Client options
40
+ * @param {number} [options.timeout=10000] - Operation timeout in ms
41
+ */
42
+ constructor(serverSpec, options = {}) {
43
+ super();
44
+ this._spec = serverSpec;
45
+ this._type = serverSpec.type || 'stdio';
46
+ this._timeout = options.timeout || DEFAULT_TIMEOUT;
47
+
48
+ // Internal state
49
+ this._nextId = 1;
50
+ this._pending = new Map(); // id -> { resolve, reject, timer }
51
+ this._connected = false;
52
+ this._initialized = false;
53
+ this._serverCapabilities = null;
54
+ this._serverInfo = null;
55
+
56
+ // Stdio transport state
57
+ this._child = null;
58
+ this._stdoutBuffer = '';
59
+
60
+ // HTTP/SSE transport state
61
+ this._sseAbortController = null;
62
+ this._httpSessionUrl = null;
63
+ }
64
+
65
+ // --------------------------------------------------------------------------
66
+ // Public API
67
+ // --------------------------------------------------------------------------
68
+
69
+ /**
70
+ * Connect to the MCP server (spawn process or open HTTP/SSE connection).
71
+ * @returns {Promise<void>}
72
+ */
73
+ async connect() {
74
+ if (this._connected) {
75
+ throw new McpClientError('Already connected');
76
+ }
77
+
78
+ if (this._type === 'stdio') {
79
+ await this._connectStdio();
80
+ } else if (this._type === 'http' || this._type === 'sse') {
81
+ await this._connectHttp();
82
+ } else {
83
+ throw new McpClientError(`Unsupported transport type: ${this._type}`);
84
+ }
85
+
86
+ this._connected = true;
87
+ this.emit('connected');
88
+ }
89
+
90
+ /**
91
+ * Perform the MCP initialize handshake.
92
+ * Sends initialize request and waits for the server response,
93
+ * then sends the initialized notification.
94
+ * @returns {Promise<object>} Server capabilities
95
+ */
96
+ async initialize() {
97
+ this._assertConnected();
98
+
99
+ const result = await this._request('initialize', {
100
+ protocolVersion: MCP_PROTOCOL_VERSION,
101
+ capabilities: {},
102
+ clientInfo: {
103
+ name: 'coding-tool-x',
104
+ version: '1.0.0'
105
+ }
106
+ });
107
+
108
+ this._serverCapabilities = result.capabilities || {};
109
+ this._serverInfo = result.serverInfo || {};
110
+
111
+ // Send initialized notification (no response expected)
112
+ this._notify('notifications/initialized', {});
113
+
114
+ this._initialized = true;
115
+ this.emit('initialized', result);
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * List available tools from the server.
122
+ * @returns {Promise<object[]>} Array of tool definitions
123
+ */
124
+ async listTools() {
125
+ this._assertInitialized();
126
+ const result = await this._request('tools/list', {});
127
+ return result.tools || [];
128
+ }
129
+
130
+ /**
131
+ * Call a tool on the server.
132
+ * @param {string} name - Tool name
133
+ * @param {object} [args={}] - Tool arguments
134
+ * @returns {Promise<object>} Tool result
135
+ */
136
+ async callTool(name, args = {}) {
137
+ this._assertInitialized();
138
+ const result = await this._request('tools/call', {
139
+ name,
140
+ arguments: args
141
+ });
142
+ return result;
143
+ }
144
+
145
+ /**
146
+ * List available resources from the server.
147
+ * @returns {Promise<object[]>} Array of resource definitions
148
+ */
149
+ async listResources() {
150
+ this._assertInitialized();
151
+ const result = await this._request('resources/list', {});
152
+ return result.resources || [];
153
+ }
154
+
155
+ /**
156
+ * List available prompts from the server.
157
+ * @returns {Promise<object[]>} Array of prompt definitions
158
+ */
159
+ async listPrompts() {
160
+ this._assertInitialized();
161
+ const result = await this._request('prompts/list', {});
162
+ return result.prompts || [];
163
+ }
164
+
165
+ /**
166
+ * Disconnect and clean up all resources.
167
+ * @returns {Promise<void>}
168
+ */
169
+ async disconnect() {
170
+ if (!this._connected) return;
171
+
172
+ // Reject all pending requests
173
+ for (const [id, pending] of this._pending) {
174
+ clearTimeout(pending.timer);
175
+ pending.reject(new McpClientError('Client disconnected'));
176
+ }
177
+ this._pending.clear();
178
+
179
+ if (this._type === 'stdio') {
180
+ await this._disconnectStdio();
181
+ } else {
182
+ this._disconnectHttp();
183
+ }
184
+
185
+ this._connected = false;
186
+ this._initialized = false;
187
+ this.emit('disconnected');
188
+ }
189
+
190
+ /**
191
+ * Whether the client is currently connected.
192
+ * @returns {boolean}
193
+ */
194
+ get connected() {
195
+ return this._connected;
196
+ }
197
+
198
+ /**
199
+ * Whether the client has completed initialization.
200
+ * @returns {boolean}
201
+ */
202
+ get initialized() {
203
+ return this._initialized;
204
+ }
205
+
206
+ /**
207
+ * Server capabilities returned during initialization.
208
+ * @returns {object|null}
209
+ */
210
+ get serverCapabilities() {
211
+ return this._serverCapabilities;
212
+ }
213
+
214
+ /**
215
+ * Server info returned during initialization.
216
+ * @returns {object|null}
217
+ */
218
+ get serverInfo() {
219
+ return this._serverInfo;
220
+ }
221
+
222
+ // --------------------------------------------------------------------------
223
+ // Stdio transport
224
+ // --------------------------------------------------------------------------
225
+
226
+ /** @private */
227
+ async _connectStdio() {
228
+ const { command, args = [], env, cwd } = this._spec;
229
+
230
+ if (!command) {
231
+ throw new McpClientError('stdio transport requires a "command" field');
232
+ }
233
+
234
+ return new Promise((resolve, reject) => {
235
+ const timer = setTimeout(() => {
236
+ this._killChild();
237
+ reject(new McpClientError(`Connection timeout after ${this._timeout}ms`));
238
+ }, this._timeout);
239
+
240
+ try {
241
+ this._child = spawn(command, args, {
242
+ env: { ...process.env, ...env },
243
+ stdio: ['pipe', 'pipe', 'pipe'],
244
+ cwd: cwd || process.cwd()
245
+ });
246
+ } catch (err) {
247
+ clearTimeout(timer);
248
+ throw new McpClientError(`Failed to spawn "${command}": ${err.message}`);
249
+ }
250
+
251
+ // Once we get the spawn event (or first stdout), consider connected
252
+ let settled = false;
253
+
254
+ const settle = (err) => {
255
+ if (settled) return;
256
+ settled = true;
257
+ clearTimeout(timer);
258
+ if (err) reject(err);
259
+ else resolve();
260
+ };
261
+
262
+ this._child.on('spawn', () => {
263
+ // Process spawned successfully - set up data handlers then resolve
264
+ this._setupStdioHandlers();
265
+ settle(null);
266
+ });
267
+
268
+ this._child.on('error', (err) => {
269
+ if (err.code === 'ENOENT') {
270
+ settle(new McpClientError(`Command "${command}" not found. Ensure it is installed and in PATH.`));
271
+ } else {
272
+ settle(new McpClientError(`Failed to start process: ${err.message}`));
273
+ }
274
+ });
275
+
276
+ // If the process exits before we consider it connected
277
+ this._child.on('close', (code, signal) => {
278
+ settle(new McpClientError(
279
+ `Process exited before connection established (code=${code}, signal=${signal})`
280
+ ));
281
+ });
282
+ });
283
+ }
284
+
285
+ /** @private */
286
+ _setupStdioHandlers() {
287
+ const child = this._child;
288
+
289
+ child.stdout.on('data', (chunk) => {
290
+ this._stdoutBuffer += chunk.toString();
291
+ this._processStdoutBuffer();
292
+ });
293
+
294
+ child.stderr.on('data', (chunk) => {
295
+ const text = chunk.toString().trim();
296
+ if (text) {
297
+ this.emit('stderr', text);
298
+ }
299
+ });
300
+
301
+ // Remove the initial 'close' listener that was for connection detection
302
+ child.removeAllListeners('close');
303
+ child.removeAllListeners('error');
304
+
305
+ child.on('close', (code, signal) => {
306
+ if (this._connected) {
307
+ this._connected = false;
308
+ this._initialized = false;
309
+
310
+ // Reject all pending with crash error
311
+ for (const [id, pending] of this._pending) {
312
+ clearTimeout(pending.timer);
313
+ pending.reject(new McpClientError(
314
+ `Server process exited unexpectedly (code=${code}, signal=${signal})`
315
+ ));
316
+ }
317
+ this._pending.clear();
318
+
319
+ this.emit('crash', { code, signal });
320
+ this.emit('disconnected');
321
+ }
322
+ });
323
+
324
+ child.on('error', (err) => {
325
+ this.emit('error', err);
326
+ });
327
+ }
328
+
329
+ /** @private */
330
+ _processStdoutBuffer() {
331
+ // JSON-RPC over stdio uses newline-delimited JSON
332
+ let newlineIdx;
333
+ while ((newlineIdx = this._stdoutBuffer.indexOf('\n')) !== -1) {
334
+ const line = this._stdoutBuffer.slice(0, newlineIdx).trim();
335
+ this._stdoutBuffer = this._stdoutBuffer.slice(newlineIdx + 1);
336
+
337
+ if (!line) continue;
338
+
339
+ try {
340
+ const msg = JSON.parse(line);
341
+ this._handleMessage(msg);
342
+ } catch (err) {
343
+ // Not valid JSON - could be a log line from the server, ignore
344
+ this.emit('stderr', `[non-json stdout]: ${line}`);
345
+ }
346
+ }
347
+ }
348
+
349
+ /** @private */
350
+ _sendStdio(msg) {
351
+ if (!this._child || this._child.killed) {
352
+ throw new McpClientError('Process is not running');
353
+ }
354
+ const data = JSON.stringify(msg) + '\n';
355
+ this._child.stdin.write(data);
356
+ }
357
+
358
+ /** @private */
359
+ async _disconnectStdio() {
360
+ return new Promise((resolve) => {
361
+ if (!this._child || this._child.killed) {
362
+ resolve();
363
+ return;
364
+ }
365
+
366
+ const child = this._child;
367
+ this._child = null;
368
+
369
+ // Give the process a chance to exit gracefully
370
+ const forceTimer = setTimeout(() => {
371
+ if (!child.killed) {
372
+ child.kill('SIGKILL');
373
+ }
374
+ resolve();
375
+ }, 2000);
376
+
377
+ child.on('close', () => {
378
+ clearTimeout(forceTimer);
379
+ resolve();
380
+ });
381
+
382
+ // Close stdin to signal EOF, then SIGTERM
383
+ try {
384
+ child.stdin.end();
385
+ } catch (e) {
386
+ // stdin might already be closed
387
+ }
388
+
389
+ setTimeout(() => {
390
+ if (!child.killed) {
391
+ child.kill('SIGTERM');
392
+ }
393
+ }, 500);
394
+ });
395
+ }
396
+
397
+ /** @private */
398
+ _killChild() {
399
+ if (this._child && !this._child.killed) {
400
+ try {
401
+ this._child.kill('SIGKILL');
402
+ } catch (e) {
403
+ // ignore
404
+ }
405
+ this._child = null;
406
+ }
407
+ }
408
+
409
+ // --------------------------------------------------------------------------
410
+ // HTTP/SSE transport
411
+ // --------------------------------------------------------------------------
412
+
413
+ /** @private */
414
+ async _connectHttp() {
415
+ const { url } = this._spec;
416
+
417
+ if (!url) {
418
+ throw new McpClientError('http/sse transport requires a "url" field');
419
+ }
420
+
421
+ // For HTTP transport, we verify the server is reachable with a GET request.
422
+ // The MCP Streamable HTTP transport uses a single endpoint for both
423
+ // POST (JSON-RPC requests) and GET (SSE event stream).
424
+ return new Promise((resolve, reject) => {
425
+ const timer = setTimeout(() => {
426
+ reject(new McpClientError(`HTTP connection timeout after ${this._timeout}ms`));
427
+ }, this._timeout);
428
+
429
+ try {
430
+ const parsedUrl = new URL(url);
431
+ const client = parsedUrl.protocol === 'https:' ? https : http;
432
+
433
+ const options = {
434
+ hostname: parsedUrl.hostname,
435
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
436
+ path: parsedUrl.pathname + parsedUrl.search,
437
+ method: 'GET',
438
+ timeout: this._timeout,
439
+ headers: {
440
+ 'Accept': 'text/event-stream',
441
+ ...this._spec.headers
442
+ }
443
+ };
444
+
445
+ const req = client.request(options, (res) => {
446
+ clearTimeout(timer);
447
+
448
+ if (res.statusCode >= 200 && res.statusCode < 400) {
449
+ // Store the base URL for sending requests
450
+ this._httpSessionUrl = url;
451
+ // We don't keep this SSE connection open during connect;
452
+ // we will open a new one per request or use POST.
453
+ res.destroy();
454
+ resolve();
455
+ } else {
456
+ res.destroy();
457
+ reject(new McpClientError(`HTTP server returned status ${res.statusCode}`));
458
+ }
459
+ });
460
+
461
+ req.on('error', (err) => {
462
+ clearTimeout(timer);
463
+ reject(new McpClientError(`HTTP connection failed: ${err.message}`));
464
+ });
465
+
466
+ req.on('timeout', () => {
467
+ req.destroy();
468
+ clearTimeout(timer);
469
+ reject(new McpClientError(`HTTP connection timeout after ${this._timeout}ms`));
470
+ });
471
+
472
+ req.end();
473
+ } catch (err) {
474
+ clearTimeout(timer);
475
+ reject(new McpClientError(`Invalid URL: ${err.message}`));
476
+ }
477
+ });
478
+ }
479
+
480
+ /** @private */
481
+ _sendHttp(msg) {
482
+ return new Promise((resolve, reject) => {
483
+ const timer = setTimeout(() => {
484
+ reject(new McpClientError(`HTTP send timeout after ${this._timeout}ms`));
485
+ }, this._timeout);
486
+
487
+ try {
488
+ const parsedUrl = new URL(this._httpSessionUrl);
489
+ const client = parsedUrl.protocol === 'https:' ? https : http;
490
+
491
+ const body = JSON.stringify(msg);
492
+
493
+ const options = {
494
+ hostname: parsedUrl.hostname,
495
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
496
+ path: parsedUrl.pathname + parsedUrl.search,
497
+ method: 'POST',
498
+ timeout: this._timeout,
499
+ headers: {
500
+ 'Content-Type': 'application/json',
501
+ 'Content-Length': Buffer.byteLength(body),
502
+ 'Accept': 'application/json, text/event-stream',
503
+ ...this._spec.headers
504
+ }
505
+ };
506
+
507
+ const req = client.request(options, (res) => {
508
+ let data = '';
509
+ res.on('data', (chunk) => { data += chunk.toString(); });
510
+ res.on('end', () => {
511
+ clearTimeout(timer);
512
+
513
+ if (res.statusCode < 200 || res.statusCode >= 300) {
514
+ reject(new McpClientError(`HTTP ${res.statusCode}: ${data}`));
515
+ return;
516
+ }
517
+
518
+ const contentType = res.headers['content-type'] || '';
519
+
520
+ // JSON response (direct response to JSON-RPC)
521
+ if (contentType.includes('application/json')) {
522
+ try {
523
+ const parsed = JSON.parse(data);
524
+ this._handleMessage(parsed);
525
+ resolve();
526
+ } catch (err) {
527
+ reject(new McpClientError(`Invalid JSON response: ${err.message}`));
528
+ }
529
+ return;
530
+ }
531
+
532
+ // SSE response (streamed events)
533
+ if (contentType.includes('text/event-stream')) {
534
+ this._parseSsePayload(data);
535
+ resolve();
536
+ return;
537
+ }
538
+
539
+ // Accepted with no body (202, notifications)
540
+ if (res.statusCode === 202 || !data.trim()) {
541
+ resolve();
542
+ return;
543
+ }
544
+
545
+ // Try parsing as JSON anyway
546
+ try {
547
+ const parsed = JSON.parse(data);
548
+ this._handleMessage(parsed);
549
+ resolve();
550
+ } catch (err) {
551
+ resolve(); // Non-JSON, non-SSE - just accept it
552
+ }
553
+ });
554
+ });
555
+
556
+ req.on('error', (err) => {
557
+ clearTimeout(timer);
558
+ reject(new McpClientError(`HTTP request failed: ${err.message}`));
559
+ });
560
+
561
+ req.on('timeout', () => {
562
+ req.destroy();
563
+ clearTimeout(timer);
564
+ reject(new McpClientError(`HTTP request timeout`));
565
+ });
566
+
567
+ req.write(body);
568
+ req.end();
569
+ } catch (err) {
570
+ clearTimeout(timer);
571
+ reject(new McpClientError(`HTTP send error: ${err.message}`));
572
+ }
573
+ });
574
+ }
575
+
576
+ /** @private */
577
+ _parseSsePayload(data) {
578
+ // Parse Server-Sent Events format
579
+ const lines = data.split('\n');
580
+ let eventData = '';
581
+
582
+ for (const line of lines) {
583
+ if (line.startsWith('data: ')) {
584
+ eventData += line.slice(6);
585
+ } else if (line === '' && eventData) {
586
+ try {
587
+ const msg = JSON.parse(eventData);
588
+ this._handleMessage(msg);
589
+ } catch (err) {
590
+ this.emit('stderr', `[invalid SSE data]: ${eventData}`);
591
+ }
592
+ eventData = '';
593
+ }
594
+ }
595
+
596
+ // Handle trailing data without a final empty line
597
+ if (eventData) {
598
+ try {
599
+ const msg = JSON.parse(eventData);
600
+ this._handleMessage(msg);
601
+ } catch (err) {
602
+ this.emit('stderr', `[invalid SSE data]: ${eventData}`);
603
+ }
604
+ }
605
+ }
606
+
607
+ /** @private */
608
+ _disconnectHttp() {
609
+ this._httpSessionUrl = null;
610
+ }
611
+
612
+ // --------------------------------------------------------------------------
613
+ // JSON-RPC 2.0 framing
614
+ // --------------------------------------------------------------------------
615
+
616
+ /** @private */
617
+ _request(method, params) {
618
+ return new Promise((resolve, reject) => {
619
+ const id = this._nextId++;
620
+
621
+ const timer = setTimeout(() => {
622
+ this._pending.delete(id);
623
+ reject(new McpClientError(`Request "${method}" timed out after ${this._timeout}ms`));
624
+ }, this._timeout);
625
+
626
+ this._pending.set(id, { resolve, reject, timer, method });
627
+
628
+ const msg = {
629
+ jsonrpc: JSONRPC_VERSION,
630
+ id,
631
+ method,
632
+ params: params || {}
633
+ };
634
+
635
+ try {
636
+ if (this._type === 'stdio') {
637
+ this._sendStdio(msg);
638
+ } else {
639
+ this._sendHttp(msg).catch((err) => {
640
+ if (this._pending.has(id)) {
641
+ clearTimeout(timer);
642
+ this._pending.delete(id);
643
+ reject(err);
644
+ }
645
+ });
646
+ }
647
+ } catch (err) {
648
+ clearTimeout(timer);
649
+ this._pending.delete(id);
650
+ reject(new McpClientError(`Failed to send request: ${err.message}`));
651
+ }
652
+ });
653
+ }
654
+
655
+ /** @private */
656
+ _notify(method, params) {
657
+ const msg = {
658
+ jsonrpc: JSONRPC_VERSION,
659
+ method,
660
+ params: params || {}
661
+ };
662
+
663
+ try {
664
+ if (this._type === 'stdio') {
665
+ this._sendStdio(msg);
666
+ } else {
667
+ // Fire-and-forget for HTTP notifications
668
+ this._sendHttp(msg).catch((err) => {
669
+ this.emit('error', new McpClientError(`Notification send failed: ${err.message}`));
670
+ });
671
+ }
672
+ } catch (err) {
673
+ this.emit('error', new McpClientError(`Notification send failed: ${err.message}`));
674
+ }
675
+ }
676
+
677
+ /** @private */
678
+ _handleMessage(msg) {
679
+ // JSON-RPC response (has id, has result or error)
680
+ if (msg.id !== undefined && msg.id !== null) {
681
+ const pending = this._pending.get(msg.id);
682
+ if (!pending) {
683
+ // Could be a server-initiated request; emit for external handling
684
+ this.emit('server-request', msg);
685
+ return;
686
+ }
687
+
688
+ clearTimeout(pending.timer);
689
+ this._pending.delete(msg.id);
690
+
691
+ if (msg.error) {
692
+ const err = new McpClientError(
693
+ msg.error.message || 'Unknown server error',
694
+ msg.error.code,
695
+ msg.error.data
696
+ );
697
+ pending.reject(err);
698
+ } else {
699
+ pending.resolve(msg.result);
700
+ }
701
+ return;
702
+ }
703
+
704
+ // JSON-RPC notification (has method, no id)
705
+ if (msg.method) {
706
+ this.emit('notification', { method: msg.method, params: msg.params });
707
+ return;
708
+ }
709
+
710
+ // Unknown message shape
711
+ this.emit('unknown-message', msg);
712
+ }
713
+
714
+ // --------------------------------------------------------------------------
715
+ // Assertions
716
+ // --------------------------------------------------------------------------
717
+
718
+ /** @private */
719
+ _assertConnected() {
720
+ if (!this._connected) {
721
+ throw new McpClientError('Not connected. Call connect() first.');
722
+ }
723
+ }
724
+
725
+ /** @private */
726
+ _assertInitialized() {
727
+ this._assertConnected();
728
+ if (!this._initialized) {
729
+ throw new McpClientError('Not initialized. Call initialize() first.');
730
+ }
731
+ }
732
+ }
733
+
734
+ // ============================================================================
735
+ // McpClientError
736
+ // ============================================================================
737
+
738
+ class McpClientError extends Error {
739
+ /**
740
+ * @param {string} message - Error message
741
+ * @param {number} [code] - JSON-RPC error code
742
+ * @param {*} [data] - Additional error data
743
+ */
744
+ constructor(message, code, data) {
745
+ super(message);
746
+ this.name = 'McpClientError';
747
+ this.code = code || undefined;
748
+ this.data = data || undefined;
749
+ }
750
+ }
751
+
752
+ // ============================================================================
753
+ // Convenience factory
754
+ // ============================================================================
755
+
756
+ /**
757
+ * Create and connect an McpClient in one call.
758
+ * Returns a fully initialized client ready for tool calls.
759
+ *
760
+ * @param {object} serverSpec - Server specification (same as McpClient constructor)
761
+ * @param {object} [options] - Client options
762
+ * @returns {Promise<McpClient>} Connected and initialized client
763
+ */
764
+ async function createClient(serverSpec, options = {}) {
765
+ const client = new McpClient(serverSpec, options);
766
+ await client.connect();
767
+ await client.initialize();
768
+ return client;
769
+ }
770
+
771
+ module.exports = {
772
+ McpClient,
773
+ McpClientError,
774
+ createClient
775
+ };