fix(mcp): make transport cross-compile on Windows

`internal/mcp/transport.go` used syscall.Setpgid and syscall.Kill
unconditionally, both Unix-only. Split the platform bits into
`transport_unix.go` (build tag `!windows`) keeping the existing
process-group semantics, and `transport_windows.go` (build tag
`windows`) falling back to `os.Process.Kill` (kills only the
immediate process — full process-tree kill on Windows would need
golang.org/x/sys/windows + job objects, deferred).

Caught by `goreleaser release --snapshot` cross-compiling for
windows/amd64 and windows/arm64.
This commit is contained in:
2026-05-20 03:34:00 +02:00
parent 59d7e2fa43
commit 67948df8cb
3 changed files with 43 additions and 7 deletions
+7 -7
View File
@@ -12,7 +12,6 @@ import (
"os/exec"
"sync"
"sync/atomic"
"syscall"
"time"
)
@@ -49,8 +48,9 @@ func NewTransport(command string, args []string, env map[string]string, logger *
func (t *Transport) Start(ctx context.Context) error {
t.cmd = exec.CommandContext(ctx, t.command, t.args...)
t.cmd.Env = t.buildEnv()
// Create a new process group so Close can kill the entire tree.
t.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
// Platform-specific: on Unix, isolate the child in a new process group
// so Close can kill the entire tree.
setProcessGroup(t.cmd)
t.stderr = limitedWriter{max: maxStderrCapture}
t.cmd.Stderr = &t.stderr
@@ -146,10 +146,10 @@ func (t *Transport) Close() error {
case <-time.After(2 * time.Second):
}
// Graceful didn't work — kill the entire process group.
t.logger.Warn("mcp: server did not exit, killing process group", "command", t.command)
if err := syscall.Kill(-t.cmd.Process.Pid, syscall.SIGKILL); err != nil {
t.logger.Warn("mcp: SIGKILL to process group failed",
// Graceful didn't work — kill the process (Unix: whole group; Windows: just the process).
t.logger.Warn("mcp: server did not exit, killing", "command", t.command)
if err := killProcessTree(t.cmd.Process); err != nil {
t.logger.Warn("mcp: kill failed",
"command", t.command, "pid", t.cmd.Process.Pid, "error", err)
}
return <-done
+18
View File
@@ -0,0 +1,18 @@
//go:build !windows
package mcp
import (
"os"
"os/exec"
"syscall"
)
func setProcessGroup(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
func killProcessTree(p *os.Process) error {
// Negative PID targets the process group created via Setpgid.
return syscall.Kill(-p.Pid, syscall.SIGKILL)
}
+18
View File
@@ -0,0 +1,18 @@
//go:build windows
package mcp
import (
"os"
"os/exec"
)
// setProcessGroup is a no-op on Windows; we rely on os.Process.Kill in Close.
// Pulling in golang.org/x/sys/windows + job objects to kill the full process
// tree would be the upgrade path if MCP servers ever spawn children we need
// to reap.
func setProcessGroup(cmd *exec.Cmd) {}
func killProcessTree(p *os.Process) error {
return p.Kill()
}