coding-tool-x 3.2.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 (185) hide show
  1. package/CHANGELOG.md +599 -0
  2. package/LICENSE +21 -0
  3. package/README.md +439 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
  6. package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
  7. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  8. package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
  9. package/dist/web/assets/Home-38JTUlYt.js +1 -0
  10. package/dist/web/assets/Home-CjupSEWE.css +1 -0
  11. package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
  12. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  13. package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
  14. package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
  15. package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
  16. package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
  17. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  18. package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
  19. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  20. package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
  21. package/dist/web/assets/icons-DRrXwWZi.js +1 -0
  22. package/dist/web/assets/index-CetESrXw.css +1 -0
  23. package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
  24. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  25. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  26. package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
  27. package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
  28. package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
  29. package/dist/web/favicon.ico +0 -0
  30. package/dist/web/index.html +20 -0
  31. package/dist/web/logo.png +0 -0
  32. package/docs/bannel.png +0 -0
  33. package/docs/home.png +0 -0
  34. package/docs/logo.png +0 -0
  35. package/docs/model-redirection.md +251 -0
  36. package/docs/multi-channel-load-balancing.md +249 -0
  37. package/package.json +80 -0
  38. package/src/commands/channels.js +551 -0
  39. package/src/commands/cli-type.js +101 -0
  40. package/src/commands/daemon.js +365 -0
  41. package/src/commands/doctor.js +333 -0
  42. package/src/commands/export-config.js +205 -0
  43. package/src/commands/list.js +222 -0
  44. package/src/commands/logs.js +261 -0
  45. package/src/commands/plugin.js +585 -0
  46. package/src/commands/port-config.js +135 -0
  47. package/src/commands/proxy-control.js +264 -0
  48. package/src/commands/proxy.js +152 -0
  49. package/src/commands/resume.js +137 -0
  50. package/src/commands/search.js +190 -0
  51. package/src/commands/security.js +37 -0
  52. package/src/commands/stats.js +398 -0
  53. package/src/commands/switch.js +48 -0
  54. package/src/commands/toggle-proxy.js +247 -0
  55. package/src/commands/ui.js +99 -0
  56. package/src/commands/update.js +97 -0
  57. package/src/commands/workspace.js +454 -0
  58. package/src/config/default.js +69 -0
  59. package/src/config/loader.js +149 -0
  60. package/src/config/model-metadata.js +167 -0
  61. package/src/config/model-metadata.json +125 -0
  62. package/src/config/model-pricing.js +35 -0
  63. package/src/config/paths.js +190 -0
  64. package/src/index.js +680 -0
  65. package/src/plugins/constants.js +15 -0
  66. package/src/plugins/event-bus.js +54 -0
  67. package/src/plugins/manifest-validator.js +129 -0
  68. package/src/plugins/plugin-api.js +128 -0
  69. package/src/plugins/plugin-installer.js +601 -0
  70. package/src/plugins/plugin-loader.js +229 -0
  71. package/src/plugins/plugin-manager.js +170 -0
  72. package/src/plugins/registry.js +152 -0
  73. package/src/plugins/schema/plugin-manifest.json +115 -0
  74. package/src/reset-config.js +94 -0
  75. package/src/server/api/agents.js +826 -0
  76. package/src/server/api/aliases.js +36 -0
  77. package/src/server/api/channels.js +368 -0
  78. package/src/server/api/claude-hooks.js +480 -0
  79. package/src/server/api/codex-channels.js +417 -0
  80. package/src/server/api/codex-projects.js +104 -0
  81. package/src/server/api/codex-proxy.js +195 -0
  82. package/src/server/api/codex-sessions.js +483 -0
  83. package/src/server/api/codex-statistics.js +57 -0
  84. package/src/server/api/commands.js +482 -0
  85. package/src/server/api/config-export.js +212 -0
  86. package/src/server/api/config-registry.js +357 -0
  87. package/src/server/api/config-sync.js +155 -0
  88. package/src/server/api/config-templates.js +248 -0
  89. package/src/server/api/config.js +521 -0
  90. package/src/server/api/convert.js +260 -0
  91. package/src/server/api/dashboard.js +142 -0
  92. package/src/server/api/env.js +144 -0
  93. package/src/server/api/favorites.js +77 -0
  94. package/src/server/api/gemini-channels.js +366 -0
  95. package/src/server/api/gemini-projects.js +91 -0
  96. package/src/server/api/gemini-proxy.js +173 -0
  97. package/src/server/api/gemini-sessions.js +376 -0
  98. package/src/server/api/gemini-statistics.js +57 -0
  99. package/src/server/api/health-check.js +31 -0
  100. package/src/server/api/mcp.js +399 -0
  101. package/src/server/api/opencode-channels.js +419 -0
  102. package/src/server/api/opencode-projects.js +99 -0
  103. package/src/server/api/opencode-proxy.js +207 -0
  104. package/src/server/api/opencode-sessions.js +327 -0
  105. package/src/server/api/opencode-statistics.js +57 -0
  106. package/src/server/api/plugins.js +463 -0
  107. package/src/server/api/pm2-autostart.js +269 -0
  108. package/src/server/api/projects.js +124 -0
  109. package/src/server/api/prompts.js +279 -0
  110. package/src/server/api/proxy.js +306 -0
  111. package/src/server/api/security.js +53 -0
  112. package/src/server/api/sessions.js +514 -0
  113. package/src/server/api/settings.js +142 -0
  114. package/src/server/api/skills.js +570 -0
  115. package/src/server/api/statistics.js +238 -0
  116. package/src/server/api/ui-config.js +64 -0
  117. package/src/server/api/workspaces.js +456 -0
  118. package/src/server/codex-proxy-server.js +681 -0
  119. package/src/server/dev-server.js +26 -0
  120. package/src/server/gemini-proxy-server.js +610 -0
  121. package/src/server/index.js +422 -0
  122. package/src/server/opencode-proxy-server.js +4771 -0
  123. package/src/server/proxy-server.js +669 -0
  124. package/src/server/services/agents-service.js +1137 -0
  125. package/src/server/services/alias.js +71 -0
  126. package/src/server/services/channel-health.js +234 -0
  127. package/src/server/services/channel-scheduler.js +240 -0
  128. package/src/server/services/channels.js +447 -0
  129. package/src/server/services/codex-channels.js +705 -0
  130. package/src/server/services/codex-config.js +90 -0
  131. package/src/server/services/codex-parser.js +322 -0
  132. package/src/server/services/codex-sessions.js +936 -0
  133. package/src/server/services/codex-settings-manager.js +619 -0
  134. package/src/server/services/codex-speed-test-template.json +24 -0
  135. package/src/server/services/codex-statistics-service.js +161 -0
  136. package/src/server/services/commands-service.js +574 -0
  137. package/src/server/services/config-export-service.js +1165 -0
  138. package/src/server/services/config-registry-service.js +828 -0
  139. package/src/server/services/config-sync-manager.js +941 -0
  140. package/src/server/services/config-sync-service.js +504 -0
  141. package/src/server/services/config-templates-service.js +913 -0
  142. package/src/server/services/enhanced-cache.js +196 -0
  143. package/src/server/services/env-checker.js +409 -0
  144. package/src/server/services/env-manager.js +436 -0
  145. package/src/server/services/favorites.js +165 -0
  146. package/src/server/services/format-converter.js +620 -0
  147. package/src/server/services/gemini-channels.js +459 -0
  148. package/src/server/services/gemini-config.js +73 -0
  149. package/src/server/services/gemini-sessions.js +689 -0
  150. package/src/server/services/gemini-settings-manager.js +263 -0
  151. package/src/server/services/gemini-statistics-service.js +157 -0
  152. package/src/server/services/health-check.js +85 -0
  153. package/src/server/services/mcp-client.js +790 -0
  154. package/src/server/services/mcp-service.js +1732 -0
  155. package/src/server/services/model-detector.js +1245 -0
  156. package/src/server/services/network-access.js +80 -0
  157. package/src/server/services/opencode-channels.js +366 -0
  158. package/src/server/services/opencode-gateway-adapters.js +1168 -0
  159. package/src/server/services/opencode-gateway-converter.js +639 -0
  160. package/src/server/services/opencode-sessions.js +931 -0
  161. package/src/server/services/opencode-settings-manager.js +478 -0
  162. package/src/server/services/opencode-statistics-service.js +161 -0
  163. package/src/server/services/plugins-service.js +1268 -0
  164. package/src/server/services/prompts-service.js +534 -0
  165. package/src/server/services/proxy-runtime.js +79 -0
  166. package/src/server/services/repo-scanner-base.js +708 -0
  167. package/src/server/services/request-logger.js +130 -0
  168. package/src/server/services/response-decoder.js +21 -0
  169. package/src/server/services/security-config.js +131 -0
  170. package/src/server/services/session-cache.js +127 -0
  171. package/src/server/services/session-converter.js +577 -0
  172. package/src/server/services/sessions.js +900 -0
  173. package/src/server/services/settings-manager.js +163 -0
  174. package/src/server/services/skill-service.js +1482 -0
  175. package/src/server/services/speed-test.js +1146 -0
  176. package/src/server/services/statistics-service.js +1043 -0
  177. package/src/server/services/ui-config.js +132 -0
  178. package/src/server/services/workspace-service.js +830 -0
  179. package/src/server/utils/pricing.js +73 -0
  180. package/src/server/websocket-server.js +513 -0
  181. package/src/ui/menu.js +139 -0
  182. package/src/ui/prompts.js +100 -0
  183. package/src/utils/format.js +43 -0
  184. package/src/utils/port-helper.js +108 -0
  185. package/src/utils/session.js +240 -0
@@ -0,0 +1,790 @@
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
+ // 确保 PATH 不被覆盖,优先使用用户提供的 env,但保留 PATH
242
+ const mergedEnv = { ...process.env, ...env };
243
+ // 如果用户提供的 env 中有 PATH,将其追加到系统 PATH 前面
244
+ if (env && env.PATH && process.env.PATH) {
245
+ mergedEnv.PATH = `${env.PATH}:${process.env.PATH}`;
246
+ }
247
+
248
+ this._child = spawn(command, args, {
249
+ env: mergedEnv,
250
+ stdio: ['pipe', 'pipe', 'pipe'],
251
+ cwd: cwd || process.cwd()
252
+ });
253
+ } catch (err) {
254
+ clearTimeout(timer);
255
+ throw new McpClientError(`Failed to spawn "${command}": ${err.message}`);
256
+ }
257
+
258
+ // Once we get the spawn event (or first stdout), consider connected
259
+ let settled = false;
260
+
261
+ const settle = (err) => {
262
+ if (settled) return;
263
+ settled = true;
264
+ clearTimeout(timer);
265
+ if (err) reject(err);
266
+ else resolve();
267
+ };
268
+
269
+ this._child.on('spawn', () => {
270
+ // Process spawned successfully - set up data handlers then resolve
271
+ this._setupStdioHandlers();
272
+ settle(null);
273
+ });
274
+
275
+ this._child.on('error', (err) => {
276
+ if (err.code === 'ENOENT') {
277
+ const pathHint = mergedEnv.PATH
278
+ ? `\n Current PATH: ${mergedEnv.PATH.split(':').slice(0, 5).join(':')}\n (showing first 5 entries)`
279
+ : '\n PATH is not set!';
280
+ settle(new McpClientError(
281
+ `Command "${command}" not found. Please check:\n` +
282
+ ` 1. Is "${command}" installed?\n` +
283
+ ` 2. Try using absolute path (e.g., /usr/bin/node or $(which ${command}))\n` +
284
+ ` 3. Check your PATH environment variable${pathHint}`
285
+ ));
286
+ } else {
287
+ settle(new McpClientError(`Failed to start process: ${err.message}`));
288
+ }
289
+ });
290
+
291
+ // If the process exits before we consider it connected
292
+ this._child.on('close', (code, signal) => {
293
+ settle(new McpClientError(
294
+ `Process exited before connection established (code=${code}, signal=${signal})`
295
+ ));
296
+ });
297
+ });
298
+ }
299
+
300
+ /** @private */
301
+ _setupStdioHandlers() {
302
+ const child = this._child;
303
+
304
+ child.stdout.on('data', (chunk) => {
305
+ this._stdoutBuffer += chunk.toString();
306
+ this._processStdoutBuffer();
307
+ });
308
+
309
+ child.stderr.on('data', (chunk) => {
310
+ const text = chunk.toString().trim();
311
+ if (text) {
312
+ this.emit('stderr', text);
313
+ }
314
+ });
315
+
316
+ // Remove the initial 'close' listener that was for connection detection
317
+ child.removeAllListeners('close');
318
+ child.removeAllListeners('error');
319
+
320
+ child.on('close', (code, signal) => {
321
+ if (this._connected) {
322
+ this._connected = false;
323
+ this._initialized = false;
324
+
325
+ // Reject all pending with crash error
326
+ for (const [id, pending] of this._pending) {
327
+ clearTimeout(pending.timer);
328
+ pending.reject(new McpClientError(
329
+ `Server process exited unexpectedly (code=${code}, signal=${signal})`
330
+ ));
331
+ }
332
+ this._pending.clear();
333
+
334
+ this.emit('crash', { code, signal });
335
+ this.emit('disconnected');
336
+ }
337
+ });
338
+
339
+ child.on('error', (err) => {
340
+ this.emit('error', err);
341
+ });
342
+ }
343
+
344
+ /** @private */
345
+ _processStdoutBuffer() {
346
+ // JSON-RPC over stdio uses newline-delimited JSON
347
+ let newlineIdx;
348
+ while ((newlineIdx = this._stdoutBuffer.indexOf('\n')) !== -1) {
349
+ const line = this._stdoutBuffer.slice(0, newlineIdx).trim();
350
+ this._stdoutBuffer = this._stdoutBuffer.slice(newlineIdx + 1);
351
+
352
+ if (!line) continue;
353
+
354
+ try {
355
+ const msg = JSON.parse(line);
356
+ this._handleMessage(msg);
357
+ } catch (err) {
358
+ // Not valid JSON - could be a log line from the server, ignore
359
+ this.emit('stderr', `[non-json stdout]: ${line}`);
360
+ }
361
+ }
362
+ }
363
+
364
+ /** @private */
365
+ _sendStdio(msg) {
366
+ if (!this._child || this._child.killed) {
367
+ throw new McpClientError('Process is not running');
368
+ }
369
+ const data = JSON.stringify(msg) + '\n';
370
+ this._child.stdin.write(data);
371
+ }
372
+
373
+ /** @private */
374
+ async _disconnectStdio() {
375
+ return new Promise((resolve) => {
376
+ if (!this._child || this._child.killed) {
377
+ resolve();
378
+ return;
379
+ }
380
+
381
+ const child = this._child;
382
+ this._child = null;
383
+
384
+ // Give the process a chance to exit gracefully
385
+ const forceTimer = setTimeout(() => {
386
+ if (!child.killed) {
387
+ child.kill('SIGKILL');
388
+ }
389
+ resolve();
390
+ }, 2000);
391
+
392
+ child.on('close', () => {
393
+ clearTimeout(forceTimer);
394
+ resolve();
395
+ });
396
+
397
+ // Close stdin to signal EOF, then SIGTERM
398
+ try {
399
+ child.stdin.end();
400
+ } catch (e) {
401
+ // stdin might already be closed
402
+ }
403
+
404
+ setTimeout(() => {
405
+ if (!child.killed) {
406
+ child.kill('SIGTERM');
407
+ }
408
+ }, 500);
409
+ });
410
+ }
411
+
412
+ /** @private */
413
+ _killChild() {
414
+ if (this._child && !this._child.killed) {
415
+ try {
416
+ this._child.kill('SIGKILL');
417
+ } catch (e) {
418
+ // ignore
419
+ }
420
+ this._child = null;
421
+ }
422
+ }
423
+
424
+ // --------------------------------------------------------------------------
425
+ // HTTP/SSE transport
426
+ // --------------------------------------------------------------------------
427
+
428
+ /** @private */
429
+ async _connectHttp() {
430
+ const { url } = this._spec;
431
+
432
+ if (!url) {
433
+ throw new McpClientError('http/sse transport requires a "url" field');
434
+ }
435
+
436
+ // For HTTP transport, we verify the server is reachable with a GET request.
437
+ // The MCP Streamable HTTP transport uses a single endpoint for both
438
+ // POST (JSON-RPC requests) and GET (SSE event stream).
439
+ return new Promise((resolve, reject) => {
440
+ const timer = setTimeout(() => {
441
+ reject(new McpClientError(`HTTP connection timeout after ${this._timeout}ms`));
442
+ }, this._timeout);
443
+
444
+ try {
445
+ const parsedUrl = new URL(url);
446
+ const client = parsedUrl.protocol === 'https:' ? https : http;
447
+
448
+ const options = {
449
+ hostname: parsedUrl.hostname,
450
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
451
+ path: parsedUrl.pathname + parsedUrl.search,
452
+ method: 'GET',
453
+ timeout: this._timeout,
454
+ headers: {
455
+ 'Accept': 'text/event-stream',
456
+ ...this._spec.headers
457
+ }
458
+ };
459
+
460
+ const req = client.request(options, (res) => {
461
+ clearTimeout(timer);
462
+
463
+ if (res.statusCode >= 200 && res.statusCode < 400) {
464
+ // Store the base URL for sending requests
465
+ this._httpSessionUrl = url;
466
+ // We don't keep this SSE connection open during connect;
467
+ // we will open a new one per request or use POST.
468
+ res.destroy();
469
+ resolve();
470
+ } else {
471
+ res.destroy();
472
+ reject(new McpClientError(`HTTP server returned status ${res.statusCode}`));
473
+ }
474
+ });
475
+
476
+ req.on('error', (err) => {
477
+ clearTimeout(timer);
478
+ reject(new McpClientError(`HTTP connection failed: ${err.message}`));
479
+ });
480
+
481
+ req.on('timeout', () => {
482
+ req.destroy();
483
+ clearTimeout(timer);
484
+ reject(new McpClientError(`HTTP connection timeout after ${this._timeout}ms`));
485
+ });
486
+
487
+ req.end();
488
+ } catch (err) {
489
+ clearTimeout(timer);
490
+ reject(new McpClientError(`Invalid URL: ${err.message}`));
491
+ }
492
+ });
493
+ }
494
+
495
+ /** @private */
496
+ _sendHttp(msg) {
497
+ return new Promise((resolve, reject) => {
498
+ const timer = setTimeout(() => {
499
+ reject(new McpClientError(`HTTP send timeout after ${this._timeout}ms`));
500
+ }, this._timeout);
501
+
502
+ try {
503
+ const parsedUrl = new URL(this._httpSessionUrl);
504
+ const client = parsedUrl.protocol === 'https:' ? https : http;
505
+
506
+ const body = JSON.stringify(msg);
507
+
508
+ const options = {
509
+ hostname: parsedUrl.hostname,
510
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
511
+ path: parsedUrl.pathname + parsedUrl.search,
512
+ method: 'POST',
513
+ timeout: this._timeout,
514
+ headers: {
515
+ 'Content-Type': 'application/json',
516
+ 'Content-Length': Buffer.byteLength(body),
517
+ 'Accept': 'application/json, text/event-stream',
518
+ ...this._spec.headers
519
+ }
520
+ };
521
+
522
+ const req = client.request(options, (res) => {
523
+ let data = '';
524
+ res.on('data', (chunk) => { data += chunk.toString(); });
525
+ res.on('end', () => {
526
+ clearTimeout(timer);
527
+
528
+ if (res.statusCode < 200 || res.statusCode >= 300) {
529
+ reject(new McpClientError(`HTTP ${res.statusCode}: ${data}`));
530
+ return;
531
+ }
532
+
533
+ const contentType = res.headers['content-type'] || '';
534
+
535
+ // JSON response (direct response to JSON-RPC)
536
+ if (contentType.includes('application/json')) {
537
+ try {
538
+ const parsed = JSON.parse(data);
539
+ this._handleMessage(parsed);
540
+ resolve();
541
+ } catch (err) {
542
+ reject(new McpClientError(`Invalid JSON response: ${err.message}`));
543
+ }
544
+ return;
545
+ }
546
+
547
+ // SSE response (streamed events)
548
+ if (contentType.includes('text/event-stream')) {
549
+ this._parseSsePayload(data);
550
+ resolve();
551
+ return;
552
+ }
553
+
554
+ // Accepted with no body (202, notifications)
555
+ if (res.statusCode === 202 || !data.trim()) {
556
+ resolve();
557
+ return;
558
+ }
559
+
560
+ // Try parsing as JSON anyway
561
+ try {
562
+ const parsed = JSON.parse(data);
563
+ this._handleMessage(parsed);
564
+ resolve();
565
+ } catch (err) {
566
+ resolve(); // Non-JSON, non-SSE - just accept it
567
+ }
568
+ });
569
+ });
570
+
571
+ req.on('error', (err) => {
572
+ clearTimeout(timer);
573
+ reject(new McpClientError(`HTTP request failed: ${err.message}`));
574
+ });
575
+
576
+ req.on('timeout', () => {
577
+ req.destroy();
578
+ clearTimeout(timer);
579
+ reject(new McpClientError(`HTTP request timeout`));
580
+ });
581
+
582
+ req.write(body);
583
+ req.end();
584
+ } catch (err) {
585
+ clearTimeout(timer);
586
+ reject(new McpClientError(`HTTP send error: ${err.message}`));
587
+ }
588
+ });
589
+ }
590
+
591
+ /** @private */
592
+ _parseSsePayload(data) {
593
+ // Parse Server-Sent Events format
594
+ const lines = data.split('\n');
595
+ let eventData = '';
596
+
597
+ for (const line of lines) {
598
+ if (line.startsWith('data: ')) {
599
+ eventData += line.slice(6);
600
+ } else if (line === '' && eventData) {
601
+ try {
602
+ const msg = JSON.parse(eventData);
603
+ this._handleMessage(msg);
604
+ } catch (err) {
605
+ this.emit('stderr', `[invalid SSE data]: ${eventData}`);
606
+ }
607
+ eventData = '';
608
+ }
609
+ }
610
+
611
+ // Handle trailing data without a final empty line
612
+ if (eventData) {
613
+ try {
614
+ const msg = JSON.parse(eventData);
615
+ this._handleMessage(msg);
616
+ } catch (err) {
617
+ this.emit('stderr', `[invalid SSE data]: ${eventData}`);
618
+ }
619
+ }
620
+ }
621
+
622
+ /** @private */
623
+ _disconnectHttp() {
624
+ this._httpSessionUrl = null;
625
+ }
626
+
627
+ // --------------------------------------------------------------------------
628
+ // JSON-RPC 2.0 framing
629
+ // --------------------------------------------------------------------------
630
+
631
+ /** @private */
632
+ _request(method, params) {
633
+ return new Promise((resolve, reject) => {
634
+ const id = this._nextId++;
635
+
636
+ const timer = setTimeout(() => {
637
+ this._pending.delete(id);
638
+ reject(new McpClientError(`Request "${method}" timed out after ${this._timeout}ms`));
639
+ }, this._timeout);
640
+
641
+ this._pending.set(id, { resolve, reject, timer, method });
642
+
643
+ const msg = {
644
+ jsonrpc: JSONRPC_VERSION,
645
+ id,
646
+ method,
647
+ params: params || {}
648
+ };
649
+
650
+ try {
651
+ if (this._type === 'stdio') {
652
+ this._sendStdio(msg);
653
+ } else {
654
+ this._sendHttp(msg).catch((err) => {
655
+ if (this._pending.has(id)) {
656
+ clearTimeout(timer);
657
+ this._pending.delete(id);
658
+ reject(err);
659
+ }
660
+ });
661
+ }
662
+ } catch (err) {
663
+ clearTimeout(timer);
664
+ this._pending.delete(id);
665
+ reject(new McpClientError(`Failed to send request: ${err.message}`));
666
+ }
667
+ });
668
+ }
669
+
670
+ /** @private */
671
+ _notify(method, params) {
672
+ const msg = {
673
+ jsonrpc: JSONRPC_VERSION,
674
+ method,
675
+ params: params || {}
676
+ };
677
+
678
+ try {
679
+ if (this._type === 'stdio') {
680
+ this._sendStdio(msg);
681
+ } else {
682
+ // Fire-and-forget for HTTP notifications
683
+ this._sendHttp(msg).catch((err) => {
684
+ this.emit('error', new McpClientError(`Notification send failed: ${err.message}`));
685
+ });
686
+ }
687
+ } catch (err) {
688
+ this.emit('error', new McpClientError(`Notification send failed: ${err.message}`));
689
+ }
690
+ }
691
+
692
+ /** @private */
693
+ _handleMessage(msg) {
694
+ // JSON-RPC response (has id, has result or error)
695
+ if (msg.id !== undefined && msg.id !== null) {
696
+ const pending = this._pending.get(msg.id);
697
+ if (!pending) {
698
+ // Could be a server-initiated request; emit for external handling
699
+ this.emit('server-request', msg);
700
+ return;
701
+ }
702
+
703
+ clearTimeout(pending.timer);
704
+ this._pending.delete(msg.id);
705
+
706
+ if (msg.error) {
707
+ const err = new McpClientError(
708
+ msg.error.message || 'Unknown server error',
709
+ msg.error.code,
710
+ msg.error.data
711
+ );
712
+ pending.reject(err);
713
+ } else {
714
+ pending.resolve(msg.result);
715
+ }
716
+ return;
717
+ }
718
+
719
+ // JSON-RPC notification (has method, no id)
720
+ if (msg.method) {
721
+ this.emit('notification', { method: msg.method, params: msg.params });
722
+ return;
723
+ }
724
+
725
+ // Unknown message shape
726
+ this.emit('unknown-message', msg);
727
+ }
728
+
729
+ // --------------------------------------------------------------------------
730
+ // Assertions
731
+ // --------------------------------------------------------------------------
732
+
733
+ /** @private */
734
+ _assertConnected() {
735
+ if (!this._connected) {
736
+ throw new McpClientError('Not connected. Call connect() first.');
737
+ }
738
+ }
739
+
740
+ /** @private */
741
+ _assertInitialized() {
742
+ this._assertConnected();
743
+ if (!this._initialized) {
744
+ throw new McpClientError('Not initialized. Call initialize() first.');
745
+ }
746
+ }
747
+ }
748
+
749
+ // ============================================================================
750
+ // McpClientError
751
+ // ============================================================================
752
+
753
+ class McpClientError extends Error {
754
+ /**
755
+ * @param {string} message - Error message
756
+ * @param {number} [code] - JSON-RPC error code
757
+ * @param {*} [data] - Additional error data
758
+ */
759
+ constructor(message, code, data) {
760
+ super(message);
761
+ this.name = 'McpClientError';
762
+ this.code = code || undefined;
763
+ this.data = data || undefined;
764
+ }
765
+ }
766
+
767
+ // ============================================================================
768
+ // Convenience factory
769
+ // ============================================================================
770
+
771
+ /**
772
+ * Create and connect an McpClient in one call.
773
+ * Returns a fully initialized client ready for tool calls.
774
+ *
775
+ * @param {object} serverSpec - Server specification (same as McpClient constructor)
776
+ * @param {object} [options] - Client options
777
+ * @returns {Promise<McpClient>} Connected and initialized client
778
+ */
779
+ async function createClient(serverSpec, options = {}) {
780
+ const client = new McpClient(serverSpec, options);
781
+ await client.connect();
782
+ await client.initialize();
783
+ return client;
784
+ }
785
+
786
+ module.exports = {
787
+ McpClient,
788
+ McpClientError,
789
+ createClient
790
+ };