coding-tool-x 3.6.6 → 3.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/web/assets/{Analytics-CcGKjDkP.js → Analytics-C57ZRDq5.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DPOa1w5D.js → ConfigTemplates-B3qqqL05.js} +1 -1
- package/dist/web/assets/Home-CG5XrTNc.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-BVbHUdey.js → PluginManager-BhX0LUzr.js} +1 -1
- package/dist/web/assets/{ProjectList-BQrz2FKo.js → ProjectList-BQ01OZn9.js} +1 -1
- package/dist/web/assets/{SessionList-CRGer64T.js → SessionList-DPv2IOXG.js} +1 -1
- package/dist/web/assets/{SkillManager-BhJKtWYM.js → SkillManager-D5CfQm2O.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-pc6tpF3m.js → WorkspaceManager-Ce8ggteK.js} +1 -1
- package/dist/web/assets/index-CpnFNWqC.js +2 -0
- package/dist/web/assets/index-DJ9VFidd.css +1 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +40 -65
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/proxy-utils.js +6 -3
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +771 -63
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +187 -63
- package/dist/web/assets/Home-BP1LxGtp.js +0 -1
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-BsdIJwLf.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
|
|
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' | '
|
|
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
|
|
182
|
-
* @param {object} [serverSpec.headers] - Additional headers for
|
|
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 === '
|
|
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('
|
|
622
|
+
throw new McpClientError('streamable_http/sse transport requires a "url" field');
|
|
576
623
|
}
|
|
577
624
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
694
|
+
this._sseResponse = res;
|
|
604
695
|
|
|
605
696
|
if (res.statusCode >= 200 && res.statusCode < 400) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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.
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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 (!
|
|
679
|
-
throw new Error(`无效的服务器类型: ${type},必须是 stdio、
|
|
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
|
|
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 (
|
|
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:
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
* 测试
|
|
1484
|
+
* 测试 streamable_http/sse 类型服务器
|
|
1472
1485
|
*/
|
|
1473
1486
|
async function testHttpServer(spec) {
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
/**
|