cprime-supergateway 3.4.7 → 3.4.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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(grep -r \"spawn\\\\|fork\\\\|exec\\\\|child_process\" --include=*.ts --include=*.js src/)",
5
+ "Bash(npx tsc:*)"
6
+ ]
7
+ }
8
+ }
@@ -41,16 +41,96 @@ export async function stdioToSse(args) {
41
41
  logger.info(` - messagePath: ${messagePath}`);
42
42
  logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`);
43
43
  logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`);
44
- onSignals({ logger });
44
+ let isShuttingDown = false;
45
+ onSignals({
46
+ logger,
47
+ cleanup: () => {
48
+ isShuttingDown = true;
49
+ child.kill();
50
+ },
51
+ });
52
+ const sessions = {};
45
53
  const child = spawn(stdioCmd, {
46
54
  shell: true,
47
55
  env: { ...process.env, ...decryptedEnvs },
48
56
  });
57
+ /** Coalesce rapid stderr bursts into one DB log after this idle gap (ms). */
58
+ const STDERR_LOG_DEBOUNCE_MS = 400;
59
+ let stderrLogBuffer = '';
60
+ let stderrLogFlushTimer = null;
61
+ const flushStderrToLogs = () => {
62
+ stderrLogFlushTimer = null;
63
+ if (!stderrLogBuffer)
64
+ return;
65
+ const text = stderrLogBuffer;
66
+ stderrLogBuffer = '';
67
+ logger.error(`Child stderr: ${text}`);
68
+ for (const [sid, session] of Object.entries(sessions)) {
69
+ logService.log(text, 'system', logger, {
70
+ ...session,
71
+ sessionId: sid,
72
+ });
73
+ }
74
+ };
75
+ const scheduleStderrLogFlush = () => {
76
+ if (stderrLogFlushTimer)
77
+ clearTimeout(stderrLogFlushTimer);
78
+ stderrLogFlushTimer = setTimeout(flushStderrToLogs, STDERR_LOG_DEBOUNCE_MS);
79
+ };
80
+ const broadcastChildError = (errorParams) => {
81
+ const notification = {
82
+ jsonrpc: '2.0',
83
+ method: 'error',
84
+ params: errorParams,
85
+ };
86
+ for (const [sid, session] of Object.entries(sessions)) {
87
+ try {
88
+ session.transport.send(notification);
89
+ }
90
+ catch (err) {
91
+ logger.error(`Failed to send child error to session ${sid}:`, err);
92
+ }
93
+ }
94
+ };
95
+ const logChildError = async (errorData) => {
96
+ for (const [sid, session] of Object.entries(sessions)) {
97
+ await logService.log(errorData, 'system', logger, {
98
+ ip: session.ip,
99
+ userId: session.userId,
100
+ sessionId: sid,
101
+ });
102
+ }
103
+ };
104
+ child.on('error', (err) => {
105
+ logger.error(`Child process error: ${err.message}`);
106
+ const errorData = {
107
+ type: 'spawn-error',
108
+ message: err.message,
109
+ code: err.code ?? null,
110
+ timestamp: new Date().toISOString(),
111
+ };
112
+ broadcastChildError(errorData);
113
+ logChildError(errorData);
114
+ });
49
115
  child.on('exit', (code, signal) => {
116
+ if (stderrLogFlushTimer) {
117
+ clearTimeout(stderrLogFlushTimer);
118
+ stderrLogFlushTimer = null;
119
+ }
120
+ flushStderrToLogs();
50
121
  logger.error(`Child exited: code=${code}, signal=${signal}`);
51
- process.exit(code ?? 1);
122
+ if (isShuttingDown || code === 0)
123
+ return;
124
+ const errorData = {
125
+ type: 'exit',
126
+ exitCode: code,
127
+ signal: signal ?? null,
128
+ message: `Child process exited unexpectedly (code=${code}, signal=${signal})`,
129
+ timestamp: new Date().toISOString(),
130
+ };
131
+ broadcastChildError(errorData);
132
+ logChildError(errorData);
52
133
  });
53
- const sessions = {};
54
134
  const app = express();
55
135
  if (corsOrigin) {
56
136
  app.use(cors({ origin: corsOrigin }));
@@ -93,6 +173,10 @@ export async function stdioToSse(args) {
93
173
  ...sessions[sessionId],
94
174
  sessionId,
95
175
  });
176
+ if (child.killed || child.exitCode !== null) {
177
+ logger.error(`Cannot forward message to child — process is not running (session ${sessionId})`);
178
+ return;
179
+ }
96
180
  child.stdin.write(JSON.stringify(msg) + '\n');
97
181
  };
98
182
  sseTransport.onclose = () => {
@@ -169,12 +253,7 @@ export async function stdioToSse(args) {
169
253
  });
170
254
  });
171
255
  child.stderr.on('data', (chunk) => {
172
- logger.error(`Child stderr: ${chunk.toString('utf8')}`);
173
- for (const [sid, session] of Object.entries(sessions)) {
174
- logService.log(chunk.toString('utf8'), 'system', logger, {
175
- ...session,
176
- sessionId: sid,
177
- });
178
- }
256
+ stderrLogBuffer += chunk.toString('utf8');
257
+ scheduleStderrLogFlush();
179
258
  });
180
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cprime-supergateway",
3
- "version": "3.4.7",
3
+ "version": "3.4.8",
4
4
  "description": "Run MCP stdio servers over SSE, Streamable HTTP or visa versa",
5
5
  "repository": {
6
6
  "type": "git",
@@ -90,15 +90,13 @@ export async function stdioToSse(args: StdioToSseArgs) {
90
90
  ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`,
91
91
  )
92
92
 
93
- onSignals({ logger })
94
-
95
- const child: ChildProcessWithoutNullStreams = spawn(stdioCmd, {
96
- shell: true,
97
- env: { ...process.env, ...decryptedEnvs },
98
- })
99
- child.on('exit', (code, signal) => {
100
- logger.error(`Child exited: code=${code}, signal=${signal}`)
101
- process.exit(code ?? 1)
93
+ let isShuttingDown = false
94
+ onSignals({
95
+ logger,
96
+ cleanup: () => {
97
+ isShuttingDown = true
98
+ child.kill()
99
+ },
102
100
  })
103
101
 
104
102
  const sessions: Record<
@@ -111,6 +109,92 @@ export async function stdioToSse(args: StdioToSseArgs) {
111
109
  }
112
110
  > = {}
113
111
 
112
+ const child: ChildProcessWithoutNullStreams = spawn(stdioCmd, {
113
+ shell: true,
114
+ env: { ...process.env, ...decryptedEnvs },
115
+ })
116
+
117
+ /** Coalesce rapid stderr bursts into one DB log after this idle gap (ms). */
118
+ const STDERR_LOG_DEBOUNCE_MS = 400
119
+ let stderrLogBuffer = ''
120
+ let stderrLogFlushTimer: ReturnType<typeof setTimeout> | null = null
121
+
122
+ const flushStderrToLogs = () => {
123
+ stderrLogFlushTimer = null
124
+ if (!stderrLogBuffer) return
125
+ const text = stderrLogBuffer
126
+ stderrLogBuffer = ''
127
+ logger.error(`Child stderr: ${text}`)
128
+ for (const [sid, session] of Object.entries(sessions)) {
129
+ logService.log(text, 'system', logger, {
130
+ ...session,
131
+ sessionId: sid,
132
+ })
133
+ }
134
+ }
135
+
136
+ const scheduleStderrLogFlush = () => {
137
+ if (stderrLogFlushTimer) clearTimeout(stderrLogFlushTimer)
138
+ stderrLogFlushTimer = setTimeout(flushStderrToLogs, STDERR_LOG_DEBOUNCE_MS)
139
+ }
140
+
141
+ const broadcastChildError = (errorParams: Record<string, unknown>) => {
142
+ const notification: JSONRPCMessage = {
143
+ jsonrpc: '2.0',
144
+ method: 'error',
145
+ params: errorParams,
146
+ }
147
+ for (const [sid, session] of Object.entries(sessions)) {
148
+ try {
149
+ session.transport.send(notification)
150
+ } catch (err) {
151
+ logger.error(`Failed to send child error to session ${sid}:`, err)
152
+ }
153
+ }
154
+ }
155
+
156
+ const logChildError = async (errorData: Record<string, unknown>) => {
157
+ for (const [sid, session] of Object.entries(sessions)) {
158
+ await logService.log(errorData, 'system', logger, {
159
+ ip: session.ip,
160
+ userId: session.userId,
161
+ sessionId: sid,
162
+ })
163
+ }
164
+ }
165
+
166
+ child.on('error', (err) => {
167
+ logger.error(`Child process error: ${err.message}`)
168
+ const errorData = {
169
+ type: 'spawn-error',
170
+ message: err.message,
171
+ code: (err as NodeJS.ErrnoException).code ?? null,
172
+ timestamp: new Date().toISOString(),
173
+ }
174
+ broadcastChildError(errorData)
175
+ logChildError(errorData)
176
+ })
177
+
178
+ child.on('exit', (code, signal) => {
179
+ if (stderrLogFlushTimer) {
180
+ clearTimeout(stderrLogFlushTimer)
181
+ stderrLogFlushTimer = null
182
+ }
183
+ flushStderrToLogs()
184
+
185
+ logger.error(`Child exited: code=${code}, signal=${signal}`)
186
+ if (isShuttingDown || code === 0) return
187
+ const errorData = {
188
+ type: 'exit',
189
+ exitCode: code,
190
+ signal: signal ?? null,
191
+ message: `Child process exited unexpectedly (code=${code}, signal=${signal})`,
192
+ timestamp: new Date().toISOString(),
193
+ }
194
+ broadcastChildError(errorData)
195
+ logChildError(errorData)
196
+ })
197
+
114
198
  const app = express()
115
199
 
116
200
  if (corsOrigin) {
@@ -163,6 +247,12 @@ export async function stdioToSse(args: StdioToSseArgs) {
163
247
  ...sessions[sessionId],
164
248
  sessionId,
165
249
  })
250
+ if (child.killed || child.exitCode !== null) {
251
+ logger.error(
252
+ `Cannot forward message to child — process is not running (session ${sessionId})`,
253
+ )
254
+ return
255
+ }
166
256
  child.stdin.write(JSON.stringify(msg) + '\n')
167
257
  }
168
258
 
@@ -245,12 +335,7 @@ export async function stdioToSse(args: StdioToSseArgs) {
245
335
  })
246
336
 
247
337
  child.stderr.on('data', (chunk: Buffer) => {
248
- logger.error(`Child stderr: ${chunk.toString('utf8')}`)
249
- for (const [sid, session] of Object.entries(sessions)) {
250
- logService.log(chunk.toString('utf8'), 'system', logger, {
251
- ...session,
252
- sessionId: sid,
253
- })
254
- }
338
+ stderrLogBuffer += chunk.toString('utf8')
339
+ scheduleStderrLogFlush()
255
340
  })
256
341
  }