@codescene/codehealth-mcp 1.1.5 → 1.1.7

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.
Files changed (2) hide show
  1. package/lib/run.js +102 -25
  2. package/package.json +1 -1
package/lib/run.js CHANGED
@@ -10,7 +10,53 @@
10
10
  * works as expected.
11
11
  */
12
12
 
13
- import { spawnSync } from "node:child_process";
13
+ import { spawn } from "node:child_process";
14
+
15
+ /** Signals that indicate a normal client-initiated shutdown. */
16
+ const CLEAN_SHUTDOWN_SIGNALS = new Set(["SIGTERM", "SIGINT"]);
17
+
18
+ /** Maps signal names to their numeric values for exit code encoding. */
19
+ const SIGNAL_NUMBERS = { SIGKILL: 9, SIGHUP: 1 };
20
+
21
+ /**
22
+ * Handle spawn errors (binary not found, permission denied, etc.).
23
+ * Throws for unexpected errors.
24
+ *
25
+ * @param {Error} error - The spawn error
26
+ * @param {string} binaryPath - Path to the binary that failed to spawn
27
+ * @returns {never}
28
+ */
29
+ function handleSpawnError(error, binaryPath) {
30
+ if (error.code === "ENOENT") {
31
+ process.stderr.write(`Error: Binary not found at ${binaryPath}\n`);
32
+ process.exit(127);
33
+ }
34
+ if (error.code === "EACCES") {
35
+ process.stderr.write(
36
+ `Error: Permission denied executing ${binaryPath}\n`
37
+ );
38
+ process.exit(126);
39
+ }
40
+ throw error;
41
+ }
42
+
43
+ /**
44
+ * Determine the exit code when the child was killed by a signal.
45
+ *
46
+ * SIGTERM and SIGINT are normal shutdown signals sent by MCP clients
47
+ * (e.g. VS Code, Zed) when the user closes the agent. Exiting with
48
+ * a non-zero code causes the client to surface a "fatal error" dialog,
49
+ * so treat these as clean exits.
50
+ *
51
+ * @param {string} signal - The signal name (e.g. "SIGTERM")
52
+ * @returns {number} The exit code to use
53
+ */
54
+ function exitCodeForSignal(signal) {
55
+ if (CLEAN_SHUTDOWN_SIGNALS.has(signal)) {
56
+ return 0;
57
+ }
58
+ return 128 + (SIGNAL_NUMBERS[signal] || 1);
59
+ }
14
60
 
15
61
  /**
16
62
  * Runs the cs-mcp binary, forwarding all stdio and signals.
@@ -23,37 +69,68 @@ import { spawnSync } from "node:child_process";
23
69
  * @returns {never}
24
70
  */
25
71
  export function runBinary(binaryPath, args) {
26
- const result = spawnSync(binaryPath, args, {
72
+ const child = spawn(binaryPath, args, {
27
73
  stdio: "inherit",
28
74
  env: process.env,
29
75
  windowsHide: true,
30
76
  });
31
77
 
32
- if (result.error) {
33
- // Handle spawn errors (e.g. binary not found, permission denied)
34
- if (result.error.code === "ENOENT") {
35
- process.stderr.write(`Error: Binary not found at ${binaryPath}\n`);
36
- process.exit(127);
37
- }
38
- if (result.error.code === "EACCES") {
39
- process.stderr.write(
40
- `Error: Permission denied executing ${binaryPath}\n`
41
- );
42
- process.exit(126);
78
+ let exited = false;
79
+
80
+ /**
81
+ * Exit exactly once and unregister signal handlers.
82
+ *
83
+ * @param {number} code
84
+ */
85
+ function exitOnce(code) {
86
+ if (exited) {
87
+ return;
43
88
  }
44
- throw result.error;
45
- }
89
+ exited = true;
46
90
 
47
- // Exit with the same code as the child process.
48
- // If the child was killed by a signal, use 128 + signal number convention.
49
- if (result.status !== null) {
50
- process.exit(result.status);
91
+ process.off("SIGTERM", forwardSigterm);
92
+ process.off("SIGINT", forwardSigint);
93
+ process.exit(code);
51
94
  }
52
- if (result.signal) {
53
- // Convert signal name to number (e.g. SIGTERM -> 15)
54
- const signalNumbers = { SIGTERM: 15, SIGINT: 2, SIGKILL: 9, SIGHUP: 1 };
55
- const sigNum = signalNumbers[result.signal] || 1;
56
- process.exit(128 + sigNum);
95
+
96
+ /**
97
+ * Forward a signal to the child process.
98
+ *
99
+ * @param {NodeJS.Signals} signal
100
+ */
101
+ function forwardSignal(signal) {
102
+ if (child.exitCode !== null || child.signalCode !== null) {
103
+ return;
104
+ }
105
+ try {
106
+ child.kill(signal);
107
+ } catch {
108
+ // Ignore race conditions where the child exits before kill().
109
+ }
57
110
  }
58
- process.exit(1);
111
+
112
+ const forwardSigterm = () => forwardSignal("SIGTERM");
113
+ const forwardSigint = () => forwardSignal("SIGINT");
114
+
115
+ process.on("SIGTERM", forwardSigterm);
116
+ process.on("SIGINT", forwardSigint);
117
+
118
+ child.once("error", (error) => {
119
+ handleSpawnError(error, binaryPath);
120
+ });
121
+
122
+ child.once("exit", (status, signal) => {
123
+ if (status !== null) {
124
+ exitOnce(status);
125
+ return;
126
+ }
127
+ if (signal) {
128
+ exitOnce(exitCodeForSignal(signal));
129
+ return;
130
+ }
131
+ exitOnce(1);
132
+ });
133
+
134
+ // Keep the wrapper process alive while the child is running.
135
+ // Actual exit happens in the child "exit" event handler above.
59
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codescene/codehealth-mcp",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "mcpName": "com.codescene/codescene-mcp-server",
5
5
  "description": "CodeScene MCP Server — Code Health analysis for AI coding agents",
6
6
  "type": "module",