cprime-supergateway 3.4.6 → 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,17 +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 server = new Server({ name: 'supergateway', version: getVersion() }, { capabilities: {} });
54
- const sessions = {};
55
134
  const app = express();
56
135
  if (corsOrigin) {
57
136
  app.use(cors({ origin: corsOrigin }));
@@ -76,6 +155,7 @@ export async function stdioToSse(args) {
76
155
  res,
77
156
  headers,
78
157
  });
158
+ const server = new Server({ name: 'supergateway', version: getVersion() }, { capabilities: {} });
79
159
  const sseTransport = new SSEServerTransport(`${baseUrl}${messagePath}`, res);
80
160
  await server.connect(sseTransport);
81
161
  const sessionId = sseTransport.sessionId;
@@ -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.6",
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,22 +90,15 @@ 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
- const server = new Server(
105
- { name: 'supergateway', version: getVersion() },
106
- { capabilities: {} },
107
- )
108
-
109
102
  const sessions: Record<
110
103
  string,
111
104
  {
@@ -116,6 +109,92 @@ export async function stdioToSse(args: StdioToSseArgs) {
116
109
  }
117
110
  > = {}
118
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
+
119
198
  const app = express()
120
199
 
121
200
  if (corsOrigin) {
@@ -145,6 +224,10 @@ export async function stdioToSse(args: StdioToSseArgs) {
145
224
  headers,
146
225
  })
147
226
 
227
+ const server = new Server(
228
+ { name: 'supergateway', version: getVersion() },
229
+ { capabilities: {} },
230
+ )
148
231
  const sseTransport = new SSEServerTransport(`${baseUrl}${messagePath}`, res)
149
232
  await server.connect(sseTransport)
150
233
 
@@ -164,6 +247,12 @@ export async function stdioToSse(args: StdioToSseArgs) {
164
247
  ...sessions[sessionId],
165
248
  sessionId,
166
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
+ }
167
256
  child.stdin.write(JSON.stringify(msg) + '\n')
168
257
  }
169
258
 
@@ -246,12 +335,7 @@ export async function stdioToSse(args: StdioToSseArgs) {
246
335
  })
247
336
 
248
337
  child.stderr.on('data', (chunk: Buffer) => {
249
- logger.error(`Child stderr: ${chunk.toString('utf8')}`)
250
- for (const [sid, session] of Object.entries(sessions)) {
251
- logService.log(chunk.toString('utf8'), 'system', logger, {
252
- ...session,
253
- sessionId: sid,
254
- })
255
- }
338
+ stderrLogBuffer += chunk.toString('utf8')
339
+ scheduleStderrLogFlush()
256
340
  })
257
341
  }