coding-tool-x 3.9.4 → 3.9.5-261012
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 +2 -0
- package/package.json +1 -1
- package/src/commands/daemon.js +12 -4
- package/src/config/model-metadata.json +153 -10
- package/src/index.js +98 -14
- package/src/platforms/drivers/claude/openai-gateway.js +1 -1
- package/src/platforms/drivers/claude/proxy-implementation.js +13 -8
- package/src/platforms/drivers/codex/proxy-implementation.js +16 -10
- package/src/platforms/drivers/dsh/api-operations.js +46 -43
- package/src/platforms/drivers/dsh/proxy-implementation.js +32 -17
- package/src/platforms/drivers/gemini/proxy-implementation.js +16 -10
- package/src/platforms/drivers/omp/gateway.js +35 -11
- package/src/platforms/drivers/opencode/proxy-implementation.js +36 -29
- package/src/server/api/mcp.js +2 -2
- package/src/server/index.js +3 -0
- package/src/server/services/mcp-service.js +2 -19
- package/src/server/services/memory-reclaimer.js +88 -0
- package/src/server/services/server-shutdown.js +28 -3
- package/src/shared/driver-factories/mcp.js +2 -8
- package/src/shared/mcp-import.js +107 -0
- package/src/shared/proxy-transport.js +33 -0
|
@@ -22,7 +22,8 @@ const {
|
|
|
22
22
|
resolveTargetUrl,
|
|
23
23
|
ensureOpenAiStreamUsage
|
|
24
24
|
} = require('../../../shared/proxy-utils');
|
|
25
|
-
const {
|
|
25
|
+
const { bindProxyRequest, prepareProxyResponse } = require('../../../shared/proxy-transport');
|
|
26
|
+
const { attachServerShutdownHandling, expediteServerShutdown, PROXY_IDLE_TIMEOUT_MS } = require('../../../server/services/server-shutdown');
|
|
26
27
|
const { buildCodexTargetUrl, createCodexRequest } = require('../codex/wire');
|
|
27
28
|
const { createClaudeRequest, buildClaudeTargetUrl, buildClaudeCountTokensTargetUrl, buildClaudeCountTokensHeaders } = require('../claude/wire');
|
|
28
29
|
const { createGeminiRequest, buildGeminiTargetUrl, shouldUseGeminiCliFormat } = require('../gemini/wire');
|
|
@@ -32,7 +33,7 @@ let proxyApp = null;
|
|
|
32
33
|
let currentPort = null;
|
|
33
34
|
|
|
34
35
|
// 用于存储每个请求的元数据
|
|
35
|
-
|
|
36
|
+
let requestMetadata = new WeakMap();
|
|
36
37
|
|
|
37
38
|
// 用于缓存已打印过的模型重定向规则,避免重复打印
|
|
38
39
|
// 格式: { channelId: { "originalModel": "redirectedModel", ... } }
|
|
@@ -52,7 +53,7 @@ const resolveOpenCodeTarget = resolveTargetUrl;
|
|
|
52
53
|
const jsonBodyParser = express.json({
|
|
53
54
|
limit: '100mb',
|
|
54
55
|
verify: (req, res, buf) => {
|
|
55
|
-
req.rawBody =
|
|
56
|
+
req.rawBody = buf;
|
|
56
57
|
}
|
|
57
58
|
});
|
|
58
59
|
|
|
@@ -350,7 +351,7 @@ function collectHttpResponseBody(res) {
|
|
|
350
351
|
});
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
function postJson(url, headers, payload, timeoutMs =
|
|
354
|
+
function postJson(url, headers, payload, timeoutMs = PROXY_IDLE_TIMEOUT_MS) {
|
|
354
355
|
return new Promise((resolve, reject) => {
|
|
355
356
|
const target = new URL(url);
|
|
356
357
|
const isHttps = target.protocol === 'https:';
|
|
@@ -388,7 +389,7 @@ function postJson(url, headers, payload, timeoutMs = 120000) {
|
|
|
388
389
|
});
|
|
389
390
|
}
|
|
390
391
|
|
|
391
|
-
function postJsonStream(url, headers, payload, timeoutMs =
|
|
392
|
+
function postJsonStream(url, headers, payload, timeoutMs = PROXY_IDLE_TIMEOUT_MS) {
|
|
392
393
|
return new Promise((resolve, reject) => {
|
|
393
394
|
const target = new URL(url);
|
|
394
395
|
const isHttps = target.protocol === 'https:';
|
|
@@ -3069,7 +3070,8 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
3069
3070
|
|
|
3070
3071
|
const proxy = httpProxy.createProxyServer({});
|
|
3071
3072
|
|
|
3072
|
-
proxy.on('proxyReq', (proxyReq, req) => {
|
|
3073
|
+
proxy.on('proxyReq', (proxyReq, req, res) => {
|
|
3074
|
+
bindProxyRequest(proxyReq, res);
|
|
3073
3075
|
const activeChannel = req.selectedChannel;
|
|
3074
3076
|
if (!activeChannel) return;
|
|
3075
3077
|
|
|
@@ -3128,11 +3130,30 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
3128
3130
|
const channel = await allocateChannel({ source: 'opencode', enableSessionBinding: false });
|
|
3129
3131
|
req.selectedChannel = channel;
|
|
3130
3132
|
|
|
3133
|
+
const release = (() => {
|
|
3134
|
+
let released = false;
|
|
3135
|
+
return () => {
|
|
3136
|
+
if (released) return;
|
|
3137
|
+
released = true;
|
|
3138
|
+
releaseChannel(channel.id, 'opencode');
|
|
3139
|
+
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
3140
|
+
};
|
|
3141
|
+
})();
|
|
3142
|
+
|
|
3143
|
+
req.__releaseChannel = release;
|
|
3144
|
+
res.on('finish', release);
|
|
3145
|
+
res.on('close', release);
|
|
3146
|
+
res.on('error', release);
|
|
3147
|
+
|
|
3148
|
+
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
3149
|
+
|
|
3150
|
+
if (res.destroyed || res.writableEnded) { release(); return; }
|
|
3151
|
+
|
|
3131
3152
|
// 检查 API key 是否有效
|
|
3132
3153
|
const effectiveKey = await getEffectiveApiKey(channel);
|
|
3154
|
+
if (res.destroyed || res.writableEnded) { release(); return; }
|
|
3133
3155
|
if (!effectiveKey) {
|
|
3134
|
-
|
|
3135
|
-
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
3156
|
+
release();
|
|
3136
3157
|
return res.status(401).json({
|
|
3137
3158
|
error: {
|
|
3138
3159
|
message: 'API key not configured or expired. Please update your channel key.',
|
|
@@ -3195,21 +3216,6 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
3195
3216
|
req.rawBody = Buffer.from(JSON.stringify(req.body));
|
|
3196
3217
|
}
|
|
3197
3218
|
|
|
3198
|
-
const release = (() => {
|
|
3199
|
-
let released = false;
|
|
3200
|
-
return () => {
|
|
3201
|
-
if (released) return;
|
|
3202
|
-
released = true;
|
|
3203
|
-
releaseChannel(channel.id, 'opencode');
|
|
3204
|
-
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
3205
|
-
};
|
|
3206
|
-
})();
|
|
3207
|
-
|
|
3208
|
-
res.on('close', release);
|
|
3209
|
-
res.on('error', release);
|
|
3210
|
-
|
|
3211
|
-
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
3212
|
-
|
|
3213
3219
|
const gatewaySourceType = normalizeGatewaySourceType(channel);
|
|
3214
3220
|
if (gatewaySourceType === 'codex') {
|
|
3215
3221
|
const handled = await handleCodexGatewayRequest(req, res, channel, effectiveKey);
|
|
@@ -3235,8 +3241,8 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
3235
3241
|
proxy.web(req, res, {
|
|
3236
3242
|
target,
|
|
3237
3243
|
changeOrigin: true,
|
|
3238
|
-
proxyTimeout:
|
|
3239
|
-
timeout:
|
|
3244
|
+
proxyTimeout: PROXY_IDLE_TIMEOUT_MS, // Allow long reasoning gaps without an unbounded upstream wait
|
|
3245
|
+
timeout: 0 // The upstream timeout bounds idle waits; streaming clients may pause
|
|
3240
3246
|
}, (err) => {
|
|
3241
3247
|
release();
|
|
3242
3248
|
if (err) {
|
|
@@ -3253,6 +3259,7 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
3253
3259
|
}
|
|
3254
3260
|
});
|
|
3255
3261
|
} catch (error) {
|
|
3262
|
+
req.__releaseChannel?.();
|
|
3256
3263
|
console.error('OpenCode channel allocation error:', error);
|
|
3257
3264
|
if (!res.headersSent) {
|
|
3258
3265
|
res.status(503).json({
|
|
@@ -3267,6 +3274,7 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
3267
3274
|
|
|
3268
3275
|
// 代理响应只负责维护传输健康状态;实时日志和 usage 来自 CLI 原生日志。
|
|
3269
3276
|
proxy.on('proxyRes', (proxyRes, req, res) => {
|
|
3277
|
+
prepareProxyResponse(proxyRes, res);
|
|
3270
3278
|
const metadata = requestMetadata.get(req);
|
|
3271
3279
|
if (!metadata) return;
|
|
3272
3280
|
|
|
@@ -3303,8 +3311,7 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
3303
3311
|
console.error('OpenCode proxy error:', err);
|
|
3304
3312
|
if (req && req.selectedChannel) {
|
|
3305
3313
|
recordFailure(req.selectedChannel.id, 'opencode', err);
|
|
3306
|
-
|
|
3307
|
-
broadcastSchedulerState('opencode', getSchedulerState('opencode'));
|
|
3314
|
+
req.__releaseChannel?.();
|
|
3308
3315
|
}
|
|
3309
3316
|
if (res && !res.headersSent) {
|
|
3310
3317
|
res.status(502).json({
|
|
@@ -3359,9 +3366,9 @@ async function stopOpenCodeProxyServer(options = {}) {
|
|
|
3359
3366
|
return { success: true, message: 'OpenCode proxy server not running' };
|
|
3360
3367
|
}
|
|
3361
3368
|
|
|
3362
|
-
requestMetadata
|
|
3369
|
+
requestMetadata = new WeakMap();
|
|
3363
3370
|
|
|
3364
|
-
const shutdownTimer = expediteServerShutdown(proxyServer);
|
|
3371
|
+
const shutdownTimer = expediteServerShutdown(proxyServer, { forceAfterMs: options.forceAfterMs });
|
|
3365
3372
|
|
|
3366
3373
|
return new Promise((resolve) => {
|
|
3367
3374
|
proxyServer.close(() => {
|
package/src/server/api/mcp.js
CHANGED
|
@@ -326,8 +326,8 @@ router.post('/import/:platform', async (req, res) => {
|
|
|
326
326
|
success: true,
|
|
327
327
|
imported: count,
|
|
328
328
|
message: count > 0
|
|
329
|
-
?
|
|
330
|
-
: `${resolved.key}
|
|
329
|
+
? `已从 ${resolved.key} 导入或更新 ${count} 个 MCP 服务器`
|
|
330
|
+
: `${resolved.key} 的 MCP 配置没有新增或变更`
|
|
331
331
|
});
|
|
332
332
|
} catch (error) {
|
|
333
333
|
console.error('[MCP API] Import failed:', sanitizeErrorMessage(error.message || error));
|
package/src/server/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
createSameOriginGuard
|
|
25
25
|
} = require('./services/network-access');
|
|
26
26
|
const { createApiRequestLogger } = require('./services/request-logger');
|
|
27
|
+
const { startMemoryReclaimer, trackMemoryActivity } = require('./services/memory-reclaimer');
|
|
27
28
|
const { inspectWebBuildState, ensureWebDistReady } = require('./services/web-build');
|
|
28
29
|
const { ensureHttpsCredentials } = require('./services/https-cert');
|
|
29
30
|
const { scheduleSessionHistoryMaintenance } = require('./services/session-history-maintenance');
|
|
@@ -197,6 +198,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
197
198
|
const allowRemoteMutation = isRemoteMutationAllowedByEnv(process.env);
|
|
198
199
|
|
|
199
200
|
// Middleware
|
|
201
|
+
app.use(trackMemoryActivity);
|
|
200
202
|
app.use(express.json({ limit: '100mb' }));
|
|
201
203
|
app.use(express.urlencoded({ limit: '100mb', extended: true }));
|
|
202
204
|
|
|
@@ -368,6 +370,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
368
370
|
traceStartupMemory('proxy-restore-complete');
|
|
369
371
|
await performStartupHealthCheck({ config });
|
|
370
372
|
traceStartupMemory('health-check-complete');
|
|
373
|
+
startMemoryReclaimer();
|
|
371
374
|
return server;
|
|
372
375
|
} finally {
|
|
373
376
|
try {
|
|
@@ -9,6 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const { spawn } = require('child_process');
|
|
10
10
|
const { McpClient, buildMissingCommandMessage, createMissingCommandHint } = require('./mcp-client');
|
|
11
11
|
const mcpFormat = require('../../shared/mcp-format');
|
|
12
|
+
const { importMcpServer } = require('../../shared/mcp-import');
|
|
12
13
|
const { convertToOmpMcpFormat, convertFromOmpMcpFormat } = mcpFormat;
|
|
13
14
|
const { PATHS } = require('../../config/paths');
|
|
14
15
|
const { ControlManifestStore } = require('./control-manifest-store');
|
|
@@ -1072,25 +1073,7 @@ async function importGenericMcpServers(driver, platform, servers) {
|
|
|
1072
1073
|
|
|
1073
1074
|
for (const [id, spec] of Object.entries(mcpServers)) {
|
|
1074
1075
|
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) continue;
|
|
1075
|
-
if (servers
|
|
1076
|
-
servers[id].apps = normalizeServerApps(servers[id].apps, {});
|
|
1077
|
-
if (!servers[id].apps[platform]) {
|
|
1078
|
-
servers[id].apps[platform] = true;
|
|
1079
|
-
count++;
|
|
1080
|
-
}
|
|
1081
|
-
continue;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
const now = Date.now();
|
|
1085
|
-
servers[id] = {
|
|
1086
|
-
id,
|
|
1087
|
-
name: id,
|
|
1088
|
-
server: normalizeServerSpec(spec),
|
|
1089
|
-
apps: { [platform]: true },
|
|
1090
|
-
createdAt: now,
|
|
1091
|
-
updatedAt: now
|
|
1092
|
-
};
|
|
1093
|
-
count++;
|
|
1076
|
+
if (importMcpServer(servers, { id, platform, spec: normalizeServerSpec(spec) })) count++;
|
|
1094
1077
|
}
|
|
1095
1078
|
|
|
1096
1079
|
return count;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MIB = 1024 * 1024;
|
|
4
|
+
const CHECK_INTERVAL_MS = 5_000;
|
|
5
|
+
const MIN_IDLE_MS = 30_000;
|
|
6
|
+
const GC_COOLDOWN_MS = 5 * 60_000;
|
|
7
|
+
const RSS_PRESSURE_BYTES = 380 * MIB;
|
|
8
|
+
const HEAP_PRESSURE_BYTES = 256 * MIB;
|
|
9
|
+
const URGENT_RSS_BYTES = 450 * MIB;
|
|
10
|
+
const URGENT_GC_COOLDOWN_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
let timer = null;
|
|
13
|
+
let activeRequests = 0;
|
|
14
|
+
let lastActivityAt = Date.now();
|
|
15
|
+
let lastCollectionAt = 0;
|
|
16
|
+
|
|
17
|
+
function trackMemoryActivity(_req, res, next) {
|
|
18
|
+
if (typeof global.gc !== 'function') {
|
|
19
|
+
if (typeof next === 'function') next();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
activeRequests += 1;
|
|
24
|
+
lastActivityAt = Date.now();
|
|
25
|
+
let completed = false;
|
|
26
|
+
const complete = () => {
|
|
27
|
+
if (completed) return;
|
|
28
|
+
completed = true;
|
|
29
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
30
|
+
lastActivityAt = Date.now();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// HTTP completion ends a request; upgraded sockets stay active until close.
|
|
34
|
+
if (typeof res.writeHead === 'function') res.once('finish', complete);
|
|
35
|
+
res.once('close', complete);
|
|
36
|
+
if (typeof next === 'function') next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function collectIfIdleAndUnderPressure() {
|
|
40
|
+
if (activeRequests > 0) return;
|
|
41
|
+
|
|
42
|
+
const before = process.memoryUsage();
|
|
43
|
+
const urgent = before.rss >= URGENT_RSS_BYTES;
|
|
44
|
+
if (!urgent && Date.now() - lastActivityAt < MIN_IDLE_MS) return;
|
|
45
|
+
if (before.rss < RSS_PRESSURE_BYTES && before.heapUsed < HEAP_PRESSURE_BYTES) return;
|
|
46
|
+
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const cooldown = urgent ? URGENT_GC_COOLDOWN_MS : GC_COOLDOWN_MS;
|
|
49
|
+
if (now - lastCollectionAt < cooldown) return;
|
|
50
|
+
lastCollectionAt = now;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
global.gc();
|
|
54
|
+
if (process.env.CC_TOOL_MEMORY_TRACE === '1') {
|
|
55
|
+
const after = process.memoryUsage();
|
|
56
|
+
console.log(`[MEM] idle-gc ${JSON.stringify({
|
|
57
|
+
pid: process.pid,
|
|
58
|
+
urgent,
|
|
59
|
+
rssBefore: before.rss,
|
|
60
|
+
rssAfter: after.rss,
|
|
61
|
+
heapUsedBefore: before.heapUsed,
|
|
62
|
+
heapUsedAfter: after.heapUsed
|
|
63
|
+
})}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn(`[MEM] Idle memory collection failed: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function startMemoryReclaimer() {
|
|
71
|
+
if (timer || typeof global.gc !== 'function') return false;
|
|
72
|
+
timer = setInterval(collectIfIdleAndUnderPressure, CHECK_INTERVAL_MS);
|
|
73
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stopMemoryReclaimer() {
|
|
78
|
+
if (timer) clearInterval(timer);
|
|
79
|
+
timer = null;
|
|
80
|
+
activeRequests = 0;
|
|
81
|
+
lastActivityAt = Date.now();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
startMemoryReclaimer,
|
|
86
|
+
stopMemoryReclaimer,
|
|
87
|
+
trackMemoryActivity
|
|
88
|
+
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const SOCKET_TRACKER = Symbol('ccTool.socketTracker');
|
|
2
|
+
const { trackMemoryActivity } = require('./memory-reclaimer');
|
|
3
|
+
const proxyServers = new Set();
|
|
4
|
+
const PROXY_IDLE_TIMEOUT_MS = 10 * 60_000;
|
|
2
5
|
|
|
3
6
|
function attachServerShutdownHandling(server, options = {}) {
|
|
4
7
|
if (!server || server[SOCKET_TRACKER]) {
|
|
@@ -6,6 +9,10 @@ function attachServerShutdownHandling(server, options = {}) {
|
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
const sockets = new Set();
|
|
12
|
+
// Proxy servers share the process with the UI; streaming requests must also
|
|
13
|
+
// prevent idle memory collection until their response finishes or closes.
|
|
14
|
+
server.prependListener('request', trackMemoryActivity);
|
|
15
|
+
server.prependListener('upgrade', (req, socket) => trackMemoryActivity(req, socket));
|
|
9
16
|
server.on('connection', (socket) => {
|
|
10
17
|
sockets.add(socket);
|
|
11
18
|
socket.on('close', () => {
|
|
@@ -15,17 +22,33 @@ function attachServerShutdownHandling(server, options = {}) {
|
|
|
15
22
|
|
|
16
23
|
const keepAliveTimeout = Number.isFinite(options.keepAliveTimeout)
|
|
17
24
|
? Math.max(0, options.keepAliveTimeout)
|
|
18
|
-
:
|
|
25
|
+
: 5000;
|
|
19
26
|
const headersTimeout = Number.isFinite(options.headersTimeout)
|
|
20
27
|
? Math.max(keepAliveTimeout + 1000, options.headersTimeout)
|
|
21
|
-
: keepAliveTimeout + 1000;
|
|
28
|
+
: Math.max(60_000, keepAliveTimeout + 1000);
|
|
22
29
|
|
|
23
30
|
server.keepAliveTimeout = keepAliveTimeout;
|
|
24
31
|
server.headersTimeout = headersTimeout;
|
|
25
32
|
server[SOCKET_TRACKER] = { sockets };
|
|
33
|
+
if (options.drainOnExit !== false) {
|
|
34
|
+
if (server.listening) proxyServers.add(server);
|
|
35
|
+
else server.once('listening', () => proxyServers.add(server));
|
|
36
|
+
server.once('close', () => proxyServers.delete(server));
|
|
37
|
+
}
|
|
26
38
|
return server;
|
|
27
39
|
}
|
|
28
40
|
|
|
41
|
+
async function drainProxyServers(options = {}) {
|
|
42
|
+
await Promise.all([...proxyServers].map(server => new Promise((resolve, reject) => {
|
|
43
|
+
const timer = expediteServerShutdown(server, { forceAfterMs: options.forceAfterMs ?? 120_000 });
|
|
44
|
+
server.close(error => {
|
|
45
|
+
if (timer) clearTimeout(timer);
|
|
46
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error);
|
|
47
|
+
else resolve();
|
|
48
|
+
});
|
|
49
|
+
})));
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
function expediteServerShutdown(server, options = {}) {
|
|
30
53
|
if (!server) {
|
|
31
54
|
return null;
|
|
@@ -75,5 +98,7 @@ function expediteServerShutdown(server, options = {}) {
|
|
|
75
98
|
|
|
76
99
|
module.exports = {
|
|
77
100
|
attachServerShutdownHandling,
|
|
78
|
-
expediteServerShutdown
|
|
101
|
+
expediteServerShutdown,
|
|
102
|
+
drainProxyServers,
|
|
103
|
+
PROXY_IDLE_TIMEOUT_MS
|
|
79
104
|
};
|
|
@@ -8,6 +8,7 @@ const tomlStringify = require('@iarna/toml').stringify;
|
|
|
8
8
|
const { ok, failed } = require('../driver-result');
|
|
9
9
|
const { extractServerSpec, convertToCodexFormat, convertFromCodexFormat, convertToOmpMcpFormat, convertFromOmpMcpFormat, convertToOpenCodeFormat, convertFromOpenCodeFormat } = require('../mcp-format');
|
|
10
10
|
const { redactSecrets, validateMcpId } = require('../project-config');
|
|
11
|
+
const { importMcpServer } = require('../mcp-import');
|
|
11
12
|
|
|
12
13
|
const SCHEMA_URL = 'https://raw.githubusercontent.com/can1357/oh-my-omp/main/packages/coding-agent/src/config/mcp-schema.json';
|
|
13
14
|
|
|
@@ -192,14 +193,7 @@ function createMcpDriver({ platform, ...context } = {}) {
|
|
|
192
193
|
let count = 0;
|
|
193
194
|
for (const [id, spec] of Object.entries(map)) {
|
|
194
195
|
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) continue;
|
|
195
|
-
if (servers
|
|
196
|
-
servers[id].apps = { ...(servers[id].apps || {}), [platform]: true };
|
|
197
|
-
count += 1;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const now = Date.now();
|
|
201
|
-
servers[id] = { id, name: id, server: internalSpec(spec), apps: { [platform]: true }, createdAt: now, updatedAt: now };
|
|
202
|
-
count += 1;
|
|
196
|
+
if (importMcpServer(servers, { id, platform, spec: internalSpec(spec) })) count += 1;
|
|
203
197
|
}
|
|
204
198
|
return count;
|
|
205
199
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isDeepStrictEqual } = require('util');
|
|
4
|
+
const { validateMcpId } = require('./project-config');
|
|
5
|
+
|
|
6
|
+
const REDACTED_VALUES = new Set(['[REDACTED]', '[redacted]', '[EXPRESSION]']);
|
|
7
|
+
|
|
8
|
+
function normalizeImportedSpec(spec) {
|
|
9
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) return spec;
|
|
10
|
+
return { ...spec, type: spec.type || 'stdio' };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function restoreRedactedUrl(incoming, existing) {
|
|
14
|
+
if (typeof incoming !== 'string' || typeof existing !== 'string') return incoming;
|
|
15
|
+
try {
|
|
16
|
+
const nextUrl = new URL(incoming);
|
|
17
|
+
const previousUrl = new URL(existing);
|
|
18
|
+
const isRedactedPart = value => {
|
|
19
|
+
try {
|
|
20
|
+
return REDACTED_VALUES.has(decodeURIComponent(value));
|
|
21
|
+
} catch (_) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const decodePart = value => {
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(value);
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
if (isRedactedPart(nextUrl.username) && previousUrl.username) {
|
|
33
|
+
nextUrl.username = decodePart(previousUrl.username);
|
|
34
|
+
}
|
|
35
|
+
if (isRedactedPart(nextUrl.password) && previousUrl.password) {
|
|
36
|
+
nextUrl.password = decodePart(previousUrl.password);
|
|
37
|
+
}
|
|
38
|
+
for (const [key, value] of nextUrl.searchParams.entries()) {
|
|
39
|
+
if (REDACTED_VALUES.has(value) && previousUrl.searchParams.has(key)) {
|
|
40
|
+
nextUrl.searchParams.set(key, previousUrl.searchParams.get(key));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return nextUrl.toString();
|
|
44
|
+
} catch (_) {
|
|
45
|
+
return incoming;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function restoreRedactedValues(incoming, existing, key = '') {
|
|
50
|
+
if (REDACTED_VALUES.has(incoming)) return existing === undefined ? incoming : existing;
|
|
51
|
+
if (key === 'url') return restoreRedactedUrl(incoming, existing);
|
|
52
|
+
if (Array.isArray(incoming)) {
|
|
53
|
+
const previous = Array.isArray(existing) ? existing : [];
|
|
54
|
+
return incoming.map((value, index) => restoreRedactedValues(value, previous[index]));
|
|
55
|
+
}
|
|
56
|
+
if (!incoming || typeof incoming !== 'object') return incoming;
|
|
57
|
+
const previous = existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {};
|
|
58
|
+
return Object.fromEntries(Object.entries(incoming).map(([childKey, value]) => [
|
|
59
|
+
childKey,
|
|
60
|
+
restoreRedactedValues(value, previous[childKey], childKey)
|
|
61
|
+
]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function importMcpServer(servers, { id, platform, spec, name } = {}) {
|
|
65
|
+
let normalizedId;
|
|
66
|
+
try {
|
|
67
|
+
normalizedId = validateMcpId(id);
|
|
68
|
+
} catch (_) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const importedSpec = normalizeImportedSpec(spec);
|
|
72
|
+
const existing = servers[normalizedId];
|
|
73
|
+
|
|
74
|
+
if (!existing) {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
servers[normalizedId] = {
|
|
77
|
+
id: normalizedId,
|
|
78
|
+
name: typeof name === 'string' && name.trim() ? name : normalizedId,
|
|
79
|
+
server: importedSpec,
|
|
80
|
+
apps: { [platform]: true },
|
|
81
|
+
createdAt: now,
|
|
82
|
+
updatedAt: now
|
|
83
|
+
};
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Treat imports as full snapshots, restoring only values hidden by the platform.
|
|
88
|
+
const nextSpec = restoreRedactedValues(importedSpec, existing.server);
|
|
89
|
+
const nextApps = {
|
|
90
|
+
...(existing.apps && typeof existing.apps === 'object' && !Array.isArray(existing.apps)
|
|
91
|
+
? existing.apps
|
|
92
|
+
: {}),
|
|
93
|
+
[platform]: true
|
|
94
|
+
};
|
|
95
|
+
const nextName = typeof name === 'string' && name.trim() ? name : existing.name || normalizedId;
|
|
96
|
+
const changed = !isDeepStrictEqual(existing.server, nextSpec)
|
|
97
|
+
|| !isDeepStrictEqual(existing.apps || {}, nextApps)
|
|
98
|
+
|| existing.name !== nextName;
|
|
99
|
+
|
|
100
|
+
existing.server = nextSpec;
|
|
101
|
+
existing.apps = nextApps;
|
|
102
|
+
existing.name = nextName;
|
|
103
|
+
if (changed) existing.updatedAt = Date.now();
|
|
104
|
+
return changed;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { importMcpServer };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Connection headers describe one transport hop, not the downstream socket.
|
|
4
|
+
function downstreamHeaders(headers = {}) {
|
|
5
|
+
const result = { ...headers };
|
|
6
|
+
const connectionHeaders = String(headers.connection || '').split(',').map(name => name.trim().toLowerCase());
|
|
7
|
+
for (const name of ['connection', 'keep-alive', 'proxy-connection', 'proxy-authenticate',
|
|
8
|
+
'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ...connectionHeaders]) {
|
|
9
|
+
delete result[name];
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function bindProxyRequest(proxyReq, res) {
|
|
15
|
+
if (!res) return;
|
|
16
|
+
const abort = () => {
|
|
17
|
+
if (!res.writableEnded && typeof proxyReq.destroy === 'function') proxyReq.destroy();
|
|
18
|
+
};
|
|
19
|
+
res.once('close', abort);
|
|
20
|
+
proxyReq.once('close', () => res.removeListener('close', abort));
|
|
21
|
+
if (res.destroyed) abort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function prepareProxyResponse(proxyRes, res) {
|
|
25
|
+
proxyRes.headers = downstreamHeaders(proxyRes.headers);
|
|
26
|
+
const fail = error => {
|
|
27
|
+
if (res && !res.destroyed) res.destroy(error);
|
|
28
|
+
};
|
|
29
|
+
proxyRes.once('error', fail);
|
|
30
|
+
proxyRes.once('aborted', () => fail(new Error('Upstream response aborted')));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { downstreamHeaders, bindProxyRequest, prepareProxyResponse };
|