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.
@@ -22,7 +22,8 @@ const {
22
22
  resolveTargetUrl,
23
23
  ensureOpenAiStreamUsage
24
24
  } = require('../../../shared/proxy-utils');
25
- const { attachServerShutdownHandling, expediteServerShutdown } = require('../../../server/services/server-shutdown');
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
- const requestMetadata = new Map();
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 = Buffer.from(buf);
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 = 120000) {
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 = 120000) {
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
- releaseChannel(channel.id, 'opencode');
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: 120000, // 代理连接超时 2 分钟
3239
- timeout: 120000 // 请求超时 2 分钟
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
- releaseChannel(req.selectedChannel.id, 'opencode');
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.clear();
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(() => {
@@ -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
- ? `成功从 ${resolved.key} 导入 ${count} 个 MCP 服务器`
330
- : `${resolved.key} 没有可导入的 MCP 服务器`
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));
@@ -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[id]) {
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
- : 1000;
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[id]) {
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 };