coding-tool-x 3.6.7 → 3.6.9

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 (54) hide show
  1. package/dist/web/assets/{Analytics-CKycBI0G.js → Analytics-BuBlsbcI.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-DQczxuOX.js} +1 -1
  3. package/dist/web/assets/Home-BvAzzWnB.js +1 -0
  4. package/dist/web/assets/Home-CIua-b-o.css +1 -0
  5. package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-C8gAlaps.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-nqgi6_cR.js} +1 -1
  7. package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-D8AxMYy6.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-BDbNbNkK.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-DGGaGW2c.js} +1 -1
  10. package/dist/web/assets/index-Dsh4Q5Cg.css +1 -0
  11. package/dist/web/assets/index-y-eEayxR.js +2 -0
  12. package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
  13. package/dist/web/index.html +3 -3
  14. package/package.json +1 -1
  15. package/src/commands/stats.js +1 -1
  16. package/src/config/channel-preset-websites.js +2 -1
  17. package/src/config/paths.js +8 -0
  18. package/src/server/api/agents.js +16 -54
  19. package/src/server/api/claude-hooks.js +1 -1
  20. package/src/server/api/claude-statistics.js +57 -0
  21. package/src/server/api/commands.js +16 -54
  22. package/src/server/api/dashboard.js +2 -2
  23. package/src/server/api/gemini-channels.js +2 -0
  24. package/src/server/api/hooks.js +4 -1
  25. package/src/server/api/plugins.js +46 -69
  26. package/src/server/api/skills.js +21 -77
  27. package/src/server/api/validation-errors.js +34 -0
  28. package/src/server/gemini-proxy-server.js +205 -21
  29. package/src/server/index.js +1 -0
  30. package/src/server/services/agents-service.js +166 -75
  31. package/src/server/services/base/base-channel-service.js +15 -0
  32. package/src/server/services/base/response-usage-parser.js +44 -0
  33. package/src/server/services/channel-balance.js +86 -1
  34. package/src/server/services/claude-statistics-service.js +139 -0
  35. package/src/server/services/commands-service.js +203 -121
  36. package/src/server/services/config-artifact-paths.js +116 -0
  37. package/src/server/services/config-export-service.js +7 -6
  38. package/src/server/services/config-registry-service.js +2 -2
  39. package/src/server/services/config-sync-manager.js +45 -11
  40. package/src/server/services/config-templates-service.js +21 -19
  41. package/src/server/services/format-converter.js +42 -4
  42. package/src/server/services/gemini-channels.js +30 -1
  43. package/src/server/services/gemini-sessions.js +347 -61
  44. package/src/server/services/mcp-client.js +236 -49
  45. package/src/server/services/mcp-service.js +51 -64
  46. package/src/server/services/notification-hooks.js +659 -88
  47. package/src/server/services/opencode-channels.js +15 -0
  48. package/src/server/services/plugins-service.js +1137 -86
  49. package/src/server/services/repo-scanner-base.js +6 -29
  50. package/src/server/services/skill-service.js +198 -65
  51. package/dist/web/assets/Home-BeGkC4bb.css +0 -1
  52. package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
  53. package/dist/web/assets/index-0isNzXNY.css +0 -1
  54. package/dist/web/assets/index-fWgvlZQx.js +0 -2
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * MCP JSON-RPC Client Wrapper
3
3
  *
4
- * Reusable client for communicating with MCP servers over stdio or HTTP/SSE
5
- * transports using the JSON-RPC 2.0 protocol.
4
+ * Reusable client for communicating with MCP servers over stdio, Streamable
5
+ * HTTP, or legacy SSE transports using the JSON-RPC 2.0 protocol.
6
6
  *
7
7
  * Usage:
8
8
  * const client = new McpClient({ type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-time'] });
@@ -166,6 +166,49 @@ function buildMissingCommandMessage(command, resolvedCommand, env = {}) {
166
166
  return [hint.title, ...hint.details].join('\n')
167
167
  }
168
168
 
169
+ function parseSseEvent(block = '') {
170
+ const lines = String(block).split('\n');
171
+ const dataLines = [];
172
+ let eventType = 'message';
173
+
174
+ for (const rawLine of lines) {
175
+ const line = rawLine.trimEnd();
176
+ if (!line || line.startsWith(':')) {
177
+ continue;
178
+ }
179
+
180
+ const separatorIdx = line.indexOf(':');
181
+ const field = separatorIdx === -1 ? line : line.slice(0, separatorIdx);
182
+ let value = separatorIdx === -1 ? '' : line.slice(separatorIdx + 1);
183
+ if (value.startsWith(' ')) {
184
+ value = value.slice(1);
185
+ }
186
+
187
+ if (field === 'event') {
188
+ eventType = value || 'message';
189
+ } else if (field === 'data') {
190
+ dataLines.push(value);
191
+ }
192
+ }
193
+
194
+ if (dataLines.length === 0) {
195
+ return null;
196
+ }
197
+
198
+ return {
199
+ eventType,
200
+ data: dataLines.join('\n')
201
+ };
202
+ }
203
+
204
+ function resolveLegacySseEndpoint(baseUrl, endpoint) {
205
+ const endpointText = String(endpoint || '').trim();
206
+ if (!endpointText) {
207
+ throw new McpClientError('SSE endpoint event did not include a request URL');
208
+ }
209
+ return new URL(endpointText, baseUrl).toString();
210
+ }
211
+
169
212
  // ============================================================================
170
213
  // McpClient
171
214
  // ============================================================================
@@ -173,13 +216,13 @@ function buildMissingCommandMessage(command, resolvedCommand, env = {}) {
173
216
  class McpClient extends EventEmitter {
174
217
  /**
175
218
  * @param {object} serverSpec - Server specification
176
- * @param {string} [serverSpec.type='stdio'] - Transport type: 'stdio' | 'http' | 'sse'
219
+ * @param {string} [serverSpec.type='stdio'] - Transport type: 'stdio' | 'streamable_http' | 'sse'
177
220
  * @param {string} [serverSpec.command] - Command for stdio transport
178
221
  * @param {string[]} [serverSpec.args] - Args for stdio transport
179
222
  * @param {object} [serverSpec.env] - Additional env vars for stdio transport
180
223
  * @param {string} [serverSpec.cwd] - Working directory for stdio transport
181
- * @param {string} [serverSpec.url] - URL for http/sse transport
182
- * @param {object} [serverSpec.headers] - Additional headers for http/sse transport
224
+ * @param {string} [serverSpec.url] - URL for streamable_http/sse transport
225
+ * @param {object} [serverSpec.headers] - Additional headers for streamable_http/sse transport
183
226
  * @param {object} [options] - Client options
184
227
  * @param {number} [options.timeout=10000] - Operation timeout in ms
185
228
  */
@@ -205,6 +248,10 @@ class McpClient extends EventEmitter {
205
248
  this._sseAbortController = null;
206
249
  this._httpSessionUrl = null;
207
250
  this._httpSessionId = null;
251
+ this._legacySseMode = false;
252
+ this._sseRequest = null;
253
+ this._sseResponse = null;
254
+ this._sseBuffer = '';
208
255
  this._negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
209
256
  }
210
257
 
@@ -223,7 +270,7 @@ class McpClient extends EventEmitter {
223
270
 
224
271
  if (this._type === 'stdio') {
225
272
  await this._connectStdio();
226
- } else if (this._type === 'http' || this._type === 'sse') {
273
+ } else if (this._type === 'streamable_http' || this._type === 'sse') {
227
274
  await this._connectHttp();
228
275
  } else {
229
276
  throw new McpClientError(`Unsupported transport type: ${this._type}`);
@@ -572,17 +619,61 @@ class McpClient extends EventEmitter {
572
619
  const { url } = this._spec;
573
620
 
574
621
  if (!url) {
575
- throw new McpClientError('http/sse transport requires a "url" field');
622
+ throw new McpClientError('streamable_http/sse transport requires a "url" field');
576
623
  }
577
624
 
578
- // For HTTP transport, we verify the server is reachable with a GET request.
579
- // The MCP Streamable HTTP transport uses a single endpoint for both
580
- // POST (JSON-RPC requests) and GET (SSE event stream).
625
+ try {
626
+ new URL(url);
627
+ } catch (err) {
628
+ throw new McpClientError(`Invalid URL: ${err.message}`);
629
+ }
630
+
631
+ if (this._type === 'sse') {
632
+ await this._connectLegacySse();
633
+ return;
634
+ }
635
+
636
+ // Streamable HTTP sends each JSON-RPC message as a POST to the MCP endpoint.
637
+ // Do not probe with GET here: valid servers may return 405 when they do not
638
+ // expose an unsolicited SSE stream on the same endpoint.
639
+ this._httpSessionUrl = url;
640
+ }
641
+
642
+ /** @private */
643
+ async _connectLegacySse() {
644
+ const { url } = this._spec;
645
+
581
646
  return new Promise((resolve, reject) => {
647
+ let settled = false;
582
648
  const timer = setTimeout(() => {
583
- reject(new McpClientError(`HTTP connection timeout after ${this._timeout}ms`));
649
+ cleanup();
650
+ reject(new McpClientError(`SSE connection timeout after ${this._timeout}ms`));
584
651
  }, this._timeout);
585
652
 
653
+ const cleanup = () => {
654
+ clearTimeout(timer);
655
+ if (this._sseRequest && !this._sseRequest.destroyed) {
656
+ this._sseRequest.destroy();
657
+ }
658
+ if (this._sseResponse && !this._sseResponse.destroyed) {
659
+ this._sseResponse.destroy();
660
+ }
661
+ this._sseRequest = null;
662
+ this._sseResponse = null;
663
+ };
664
+
665
+ const settle = (err) => {
666
+ if (settled) return;
667
+ settled = true;
668
+ clearTimeout(timer);
669
+ if (err) {
670
+ cleanup();
671
+ reject(err);
672
+ } else {
673
+ resolve();
674
+ }
675
+ };
676
+
586
677
  try {
587
678
  const parsedUrl = new URL(url);
588
679
  const client = parsedUrl.protocol === 'https:' ? https : http;
@@ -600,36 +691,70 @@ class McpClient extends EventEmitter {
600
691
  };
601
692
 
602
693
  const req = client.request(options, (res) => {
603
- clearTimeout(timer);
694
+ this._sseResponse = res;
604
695
 
605
696
  if (res.statusCode >= 200 && res.statusCode < 400) {
606
- // Store the base URL for sending requests
607
- this._httpSessionUrl = url;
608
- // We don't keep this SSE connection open during connect;
609
- // we will open a new one per request or use POST.
610
- res.destroy();
611
- resolve();
697
+ const contentType = res.headers['content-type'] || '';
698
+ if (!contentType.includes('text/event-stream')) {
699
+ settle(new McpClientError(`SSE endpoint returned unsupported content type: ${contentType || 'unknown'}`));
700
+ return;
701
+ }
702
+
703
+ res.on('data', (chunk) => {
704
+ this._sseBuffer += chunk.toString();
705
+ const events = this._drainSseEvents();
706
+
707
+ for (const event of events) {
708
+ if (!settled && event.eventType === 'endpoint') {
709
+ try {
710
+ this._httpSessionUrl = resolveLegacySseEndpoint(url, event.data);
711
+ this._legacySseMode = true;
712
+ settle(null);
713
+ } catch (err) {
714
+ settle(err);
715
+ }
716
+ continue;
717
+ }
718
+
719
+ this._handleSseEvent(event);
720
+ }
721
+ });
722
+
723
+ res.on('end', () => {
724
+ if (!settled) {
725
+ settle(new McpClientError('SSE endpoint closed before endpoint event'));
726
+ return;
727
+ }
728
+ this._handleHttpDisconnect('SSE stream closed');
729
+ });
730
+
731
+ res.on('error', (err) => {
732
+ if (!settled) {
733
+ settle(new McpClientError(`SSE stream error: ${err.message}`));
734
+ return;
735
+ }
736
+ this._handleHttpDisconnect(`SSE stream error: ${err.message}`);
737
+ });
612
738
  } else {
613
- res.destroy();
614
- reject(new McpClientError(`HTTP server returned status ${res.statusCode}`));
739
+ res.resume();
740
+ settle(new McpClientError(`SSE server returned status ${res.statusCode}`));
615
741
  }
616
742
  });
617
743
 
744
+ this._sseRequest = req;
745
+
618
746
  req.on('error', (err) => {
619
- clearTimeout(timer);
620
- reject(new McpClientError(`HTTP connection failed: ${err.message}`));
747
+ settle(new McpClientError(`SSE connection failed: ${err.message}`));
621
748
  });
622
749
 
623
750
  req.on('timeout', () => {
624
751
  req.destroy();
625
- clearTimeout(timer);
626
- reject(new McpClientError(`HTTP connection timeout after ${this._timeout}ms`));
752
+ settle(new McpClientError(`SSE connection timeout after ${this._timeout}ms`));
627
753
  });
628
754
 
629
755
  req.end();
630
756
  } catch (err) {
631
- clearTimeout(timer);
632
- reject(new McpClientError(`Invalid URL: ${err.message}`));
757
+ settle(new McpClientError(`Invalid URL: ${err.message}`));
633
758
  }
634
759
  });
635
760
  }
@@ -684,7 +809,7 @@ class McpClient extends EventEmitter {
684
809
  const trimmedData = data.trim();
685
810
 
686
811
  if (!trimmedData) {
687
- if (isNotification || res.statusCode === 202 || res.statusCode === 204) {
812
+ if (isNotification || res.statusCode === 204 || (res.statusCode === 202 && this._legacySseMode)) {
688
813
  resolve();
689
814
  return;
690
815
  }
@@ -745,39 +870,101 @@ class McpClient extends EventEmitter {
745
870
 
746
871
  /** @private */
747
872
  _parseSsePayload(data) {
748
- // Parse Server-Sent Events format
749
- const lines = data.split('\n');
750
- let eventData = '';
751
-
752
- for (const line of lines) {
753
- if (line.startsWith('data: ')) {
754
- eventData += line.slice(6);
755
- } else if (line === '' && eventData) {
756
- try {
757
- const msg = JSON.parse(eventData);
758
- this._handleMessage(msg);
759
- } catch (err) {
760
- this.emit('stderr', `[invalid SSE data]: ${eventData}`);
761
- }
762
- eventData = '';
873
+ const previousBuffer = this._sseBuffer;
874
+ this._sseBuffer = data;
875
+ const events = this._drainSseEvents({ flush: true });
876
+ this._sseBuffer = previousBuffer;
877
+
878
+ for (const event of events) {
879
+ this._handleSseEvent(event);
880
+ }
881
+ }
882
+
883
+ /** @private */
884
+ _drainSseEvents({ flush = false } = {}) {
885
+ const events = [];
886
+ let normalized = this._sseBuffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
887
+ let delimiterIdx;
888
+
889
+ while ((delimiterIdx = normalized.indexOf('\n\n')) !== -1) {
890
+ const block = normalized.slice(0, delimiterIdx);
891
+ normalized = normalized.slice(delimiterIdx + 2);
892
+ const event = parseSseEvent(block);
893
+ if (event && event.data) {
894
+ events.push(event);
763
895
  }
764
896
  }
765
897
 
766
- // Handle trailing data without a final empty line
767
- if (eventData) {
768
- try {
769
- const msg = JSON.parse(eventData);
770
- this._handleMessage(msg);
771
- } catch (err) {
772
- this.emit('stderr', `[invalid SSE data]: ${eventData}`);
898
+ if (flush && normalized.trim()) {
899
+ const event = parseSseEvent(normalized);
900
+ if (event && event.data) {
901
+ events.push(event);
773
902
  }
903
+ normalized = '';
904
+ }
905
+
906
+ this._sseBuffer = normalized;
907
+ return events;
908
+ }
909
+
910
+ /** @private */
911
+ _handleSseEvent(event) {
912
+ if (!event || !event.data || event.data === '[DONE]') {
913
+ return;
914
+ }
915
+
916
+ if (event.eventType === 'endpoint') {
917
+ return;
918
+ }
919
+
920
+ try {
921
+ const msg = JSON.parse(event.data);
922
+ this._handleMessage(msg);
923
+ } catch (err) {
924
+ this.emit('stderr', `[invalid SSE data]: ${event.data}`);
774
925
  }
775
926
  }
776
927
 
928
+ /** @private */
929
+ _handleHttpDisconnect(message) {
930
+ if (!this._connected) {
931
+ return;
932
+ }
933
+
934
+ this._connected = false;
935
+ this._initialized = false;
936
+
937
+ for (const [id, pending] of this._pending) {
938
+ clearTimeout(pending.timer);
939
+ pending.reject(new McpClientError(message));
940
+ }
941
+ this._pending.clear();
942
+
943
+ this.emit('disconnected');
944
+ }
945
+
777
946
  /** @private */
778
947
  _disconnectHttp() {
948
+ if (this._sseRequest && !this._sseRequest.destroyed) {
949
+ try {
950
+ this._sseRequest.destroy();
951
+ } catch (e) {
952
+ // ignore
953
+ }
954
+ }
955
+ if (this._sseResponse && !this._sseResponse.destroyed) {
956
+ try {
957
+ this._sseResponse.destroy();
958
+ } catch (e) {
959
+ // ignore
960
+ }
961
+ }
962
+ this._sseRequest = null;
963
+ this._sseResponse = null;
964
+ this._sseBuffer = '';
779
965
  this._httpSessionUrl = null;
780
966
  this._httpSessionId = null;
967
+ this._legacySseMode = false;
781
968
  this._negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
782
969
  }
783
970
 
@@ -9,8 +9,6 @@ const path = require('path');
9
9
  const os = require('os');
10
10
  const toml = require('@iarna/toml');
11
11
  const { spawn } = require('child_process');
12
- const http = require('http');
13
- const https = require('https');
14
12
  const { McpClient, buildMissingCommandMessage, createMissingCommandHint } = require('./mcp-client');
15
13
  const { NATIVE_PATHS, PATHS } = require('../../config/paths');
16
14
  const { resolvePreferredHomeDir } = require('../../utils/home-dir');
@@ -35,6 +33,18 @@ const OPENCODE_CONFIG_PATHS = {
35
33
  // serverId -> { client, timestamp }
36
34
  const mcpClientPool = new Map();
37
35
  const POOL_TTL = 5 * 60 * 1000; // 5 minutes
36
+ const STREAMABLE_HTTP_TYPE = 'streamable_http';
37
+ const MCP_SERVER_TYPES = ['stdio', STREAMABLE_HTTP_TYPE, 'sse'];
38
+ const REMOTE_MCP_SERVER_TYPES = [STREAMABLE_HTTP_TYPE, 'sse'];
39
+
40
+ function normalizeServerSpec(spec = {}) {
41
+ if (!spec || typeof spec !== 'object') {
42
+ return spec;
43
+ }
44
+ const normalized = { ...spec };
45
+ normalized.type = normalized.type || 'stdio';
46
+ return normalized;
47
+ }
38
48
 
39
49
  // MCP 预设模板
40
50
  const MCP_PRESETS = [
@@ -545,6 +555,7 @@ function getAllServers() {
545
555
  continue;
546
556
  }
547
557
  server.apps = normalizeServerApps(server.apps);
558
+ server.server = normalizeServerSpec(server.server);
548
559
  }
549
560
 
550
561
  return servers;
@@ -568,6 +579,8 @@ async function saveServer(server, options = {}) {
568
579
  throw new Error('MCP 服务器 ID 不能为空');
569
580
  }
570
581
 
582
+ server.server = normalizeServerSpec(server.server);
583
+
571
584
  // 验证服务器配置
572
585
  validateServerSpec(server.server);
573
586
 
@@ -675,15 +688,15 @@ function validateServerSpec(spec) {
675
688
 
676
689
  const type = spec.type || 'stdio';
677
690
 
678
- if (!['stdio', 'http', 'sse'].includes(type)) {
679
- throw new Error(`无效的服务器类型: ${type},必须是 stdio、http 或 sse`);
691
+ if (!MCP_SERVER_TYPES.includes(type)) {
692
+ throw new Error(`无效的服务器类型: ${spec.type || type},必须是 stdio、streamable_http 或 sse`);
680
693
  }
681
694
 
682
695
  if (type === 'stdio') {
683
696
  if (!spec.command || !spec.command.trim()) {
684
697
  throw new Error('stdio 类型必须指定 command');
685
698
  }
686
- } else if (type === 'http' || type === 'sse') {
699
+ } else if (REMOTE_MCP_SERVER_TYPES.includes(type)) {
687
700
  if (!spec.url || !spec.url.trim()) {
688
701
  throw new Error(`${type} 类型必须指定 url`);
689
702
  }
@@ -892,7 +905,7 @@ function convertToCodexFormat(spec) {
892
905
  if (spec.cwd) {
893
906
  result.cwd = spec.cwd;
894
907
  }
895
- } else if (result.type === 'http' || result.type === 'sse') {
908
+ } else if (REMOTE_MCP_SERVER_TYPES.includes(result.type)) {
896
909
  result.url = spec.url || '';
897
910
  if (spec.headers && Object.keys(spec.headers).length > 0) {
898
911
  result.http_headers = spec.headers;
@@ -1023,7 +1036,7 @@ function convertFromOpenCodeFormat(spec) {
1023
1036
 
1024
1037
  if (sourceType === 'remote') {
1025
1038
  const result = {
1026
- type: 'http',
1039
+ type: STREAMABLE_HTTP_TYPE,
1027
1040
  url: spec.url || ''
1028
1041
  };
1029
1042
  if (spec.headers && typeof spec.headers === 'object') {
@@ -1033,7 +1046,7 @@ function convertFromOpenCodeFormat(spec) {
1033
1046
  }
1034
1047
 
1035
1048
  // 已经是通用格式时直接兼容处理
1036
- if (sourceType === 'stdio' || sourceType === 'http' || sourceType === 'sse') {
1049
+ if (sourceType === 'stdio' || REMOTE_MCP_SERVER_TYPES.includes(sourceType)) {
1037
1050
  return convertFromCodexFormat(spec);
1038
1051
  }
1039
1052
 
@@ -1256,7 +1269,7 @@ function convertFromCodexFormat(spec) {
1256
1269
  if (spec.cwd) {
1257
1270
  result.cwd = spec.cwd;
1258
1271
  }
1259
- } else if (result.type === 'http' || result.type === 'sse') {
1272
+ } else if (REMOTE_MCP_SERVER_TYPES.includes(result.type)) {
1260
1273
  result.url = spec.url || '';
1261
1274
  if (spec.http_headers) {
1262
1275
  result.headers = spec.http_headers;
@@ -1324,7 +1337,7 @@ async function testServer(serverId) {
1324
1337
  try {
1325
1338
  if (type === 'stdio') {
1326
1339
  return await testStdioServer(spec);
1327
- } else if (type === 'http' || type === 'sse') {
1340
+ } else if (REMOTE_MCP_SERVER_TYPES.includes(type)) {
1328
1341
  return await testHttpServer(spec);
1329
1342
  } else {
1330
1343
  return { success: false, message: `不支持的服务器类型: ${type}` };
@@ -1468,65 +1481,39 @@ async function testStdioServer(spec) {
1468
1481
  }
1469
1482
 
1470
1483
  /**
1471
- * 测试 http/sse 类型服务器
1484
+ * 测试 streamable_http/sse 类型服务器
1472
1485
  */
1473
1486
  async function testHttpServer(spec) {
1474
- return new Promise((resolve) => {
1475
- const startTime = Date.now();
1476
- const timeout = 10000;
1477
-
1478
- try {
1479
- const url = new URL(spec.url);
1480
- const isHttps = url.protocol === 'https:';
1481
- const client = isHttps ? https : http;
1482
-
1483
- const options = {
1484
- hostname: url.hostname,
1485
- port: url.port || (isHttps ? 443 : 80),
1486
- path: url.pathname + url.search,
1487
- method: 'GET',
1488
- timeout: timeout,
1489
- headers: {
1490
- ...spec.headers
1491
- }
1492
- };
1493
-
1494
- const req = client.request(options, (res) => {
1495
- resolve({
1496
- success: res.statusCode >= 200 && res.statusCode < 500,
1497
- message: res.statusCode >= 200 && res.statusCode < 400
1498
- ? `服务器响应正常 (HTTP ${res.statusCode})`
1499
- : `服务器响应异常 (HTTP ${res.statusCode})`,
1500
- duration: Date.now() - startTime
1501
- });
1502
- });
1503
-
1504
- req.on('error', (err) => {
1505
- resolve({
1506
- success: false,
1507
- message: `连接失败: ${err.message}`,
1508
- duration: Date.now() - startTime
1509
- });
1510
- });
1487
+ const startTime = Date.now();
1488
+ let client = null;
1511
1489
 
1512
- req.on('timeout', () => {
1513
- req.destroy();
1514
- resolve({
1515
- success: false,
1516
- message: '连接超时',
1517
- duration: timeout
1518
- });
1519
- });
1490
+ try {
1491
+ client = new McpClient(spec, { timeout: 10000 });
1492
+ await client.connect();
1493
+ await client.initialize();
1520
1494
 
1521
- req.end();
1522
- } catch (err) {
1523
- resolve({
1524
- success: false,
1525
- message: `URL 无效: ${err.message}`,
1526
- duration: Date.now() - startTime
1527
- });
1495
+ return {
1496
+ success: true,
1497
+ message: '服务器 MCP 握手成功',
1498
+ duration: Date.now() - startTime
1499
+ };
1500
+ } catch (err) {
1501
+ const failure = buildMcpFailureResult(err, err.message, Date.now() - startTime);
1502
+ return {
1503
+ success: false,
1504
+ message: failure.message,
1505
+ hint: failure.hint,
1506
+ duration: failure.duration
1507
+ };
1508
+ } finally {
1509
+ if (client) {
1510
+ try {
1511
+ await client.disconnect();
1512
+ } catch (err) {
1513
+ // ignore cleanup failures
1514
+ }
1528
1515
  }
1529
- });
1516
+ }
1530
1517
  }
1531
1518
 
1532
1519
  /**