Skip to content

Commit 9d084c2

Browse files
rudycelekliDumbrisclaude
authored
fix(isolation): detect the runtime of Windows launchers with an .exe/.cmd suffix (#1778)
## Problem `IsolationManager.DetectRuntimeType` switches on `filepath.Base(command)` exactly. On Windows the launchers carry an executable suffix: `uvx.exe`, `npx.cmd`, `npm.cmd`, `yarn.cmd`, `pip.exe`, `go.exe`. Configs often name them that way, either by full path or because `npx.cmd` is what resolves. None of those match a case, so they fall to the `binary` default. `python.exe` and `node.exe` only work through the substring fallback. With Docker isolation enabled, `binary` selects the generic image, and `TransformCommandForContainer` passes the original command through. The container is asked to run `npx.cmd` (or `uvx.exe`) inside a Linux image where it doesn't exist, so the server never starts. ## Fix Lowercase the base name and strip a `.exe`, `.cmd` or `.bat` suffix before the switch, so `npx.cmd` maps to `npx`, `uvx.exe` to `uvx`, and so on. Then the right image and in-container command are used. Unsuffixed names are unchanged. ## Test `TestDetectRuntimeType_WindowsExecutableSuffix` covers `uvx.exe`, `npx.cmd`, `NPX.CMD`, `npm.cmd`, `yarn.cmd`, `pip.exe` and `go.exe`, plus `uvx` and an unknown `my-tool`. On main all seven suffixed names return `binary`; with this change they map to their runtime. `go test -count=1 ./internal/upstream/core/` passes. Made with [Cursor](https://cursor.com) --------- Co-authored-by: Algis Dumbris <a.dumbris@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e138559 commit 9d084c2

4 files changed

Lines changed: 72 additions & 7 deletions

File tree

‎internal/diagnostics/remediation.go‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@ func RuntimeAwareRemediation(code Code, hints ClassifierHints) string {
6868
// fall back to the base command name so the message still names something
6969
// concrete rather than a generic "interpreter".
7070
func detectDockerRuntimeType(command string) string {
71-
cmdName := filepath.Base(command)
71+
base := filepath.Base(command)
72+
// Mirror core.DetectRuntimeType: case-insensitive, with a Windows
73+
// launcher suffix (.exe/.cmd/.bat) stripped.
74+
cmdName := strings.ToLower(base)
75+
switch filepath.Ext(cmdName) {
76+
case ".exe", ".cmd", ".bat":
77+
cmdName = strings.TrimSuffix(cmdName, filepath.Ext(cmdName))
78+
}
7279
switch cmdName {
7380
case "python", "python3", "python3.11", "python3.12", "python3.13":
7481
return "python"
@@ -101,14 +108,13 @@ func detectDockerRuntimeType(command string) string {
101108
case "composer":
102109
return "composer"
103110
default:
104-
lower := strings.ToLower(cmdName)
105-
if strings.Contains(lower, "python") {
111+
if strings.Contains(cmdName, "python") {
106112
return "python"
107113
}
108-
if strings.Contains(lower, "node") {
114+
if strings.Contains(cmdName, "node") {
109115
return "node"
110116
}
111-
return cmdName
117+
return base
112118
}
113119
}
114120

‎internal/diagnostics/remediation_runtime_gate_test.go‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,27 @@ func TestMirroredGitRuntimeGateMatchesConfig(t *testing.T) {
7272
}
7373
}
7474
}
75+
76+
// The mirror has to normalise a Windows launcher name the way
77+
// core.DetectRuntimeType does, or a uvx.exe/npx.cmd server gets remediation
78+
// for a "uvx.exe" runtime and no git-dependency hint while the spawn path uses
79+
// the uvx image and the git-capable substitute.
80+
func TestDetectDockerRuntimeTypeWindowsLauncherSuffix(t *testing.T) {
81+
cases := map[string]string{
82+
"uvx.exe": "uvx",
83+
"npx.cmd": "npx",
84+
"Uvx.EXE": "uvx",
85+
"npm.bat": "npm",
86+
"my-tool.exe": "my-tool.exe",
87+
}
88+
for command, want := range cases {
89+
if got := detectDockerRuntimeType(command); got != want {
90+
t.Errorf("detectDockerRuntimeType(%q) = %q, want %q", command, got, want)
91+
}
92+
}
93+
94+
hints := ClassifierHints{DockerCommand: "uvx.exe", DockerArgs: []string{"git+https://x"}}
95+
if !hintsHaveGitDependency(hints) {
96+
t.Error("hintsHaveGitDependency(uvx.exe, git+) = false, want true")
97+
}
98+
}

‎internal/upstream/core/isolation.go‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,13 @@ func (im *IsolationManager) warnPerServerIgnoredOnce(serverName string) {
220220

221221
// DetectRuntimeType detects the runtime type based on the command
222222
func (im *IsolationManager) DetectRuntimeType(command string) string {
223-
// Extract just the command name without path
224-
cmdName := filepath.Base(command)
223+
// Extract just the command name without path or a Windows executable
224+
// suffix (uvx.exe, npx.cmd), compared case-insensitively as Windows does.
225+
cmdName := strings.ToLower(filepath.Base(command))
226+
switch filepath.Ext(cmdName) {
227+
case ".exe", ".cmd", ".bat":
228+
cmdName = strings.TrimSuffix(cmdName, filepath.Ext(cmdName))
229+
}
225230

226231
// Handle common runtime commands
227232
switch cmdName {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package core
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
8+
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
9+
)
10+
11+
// Windows configs name launchers with their executable suffix (uvx.exe,
12+
// npx.cmd). Treating those as an unknown "binary" picks the generic image and
13+
// runs "npx.cmd" inside a Linux container, where it does not exist.
14+
func TestDetectRuntimeType_WindowsExecutableSuffix(t *testing.T) {
15+
im := NewIsolationManager(config.DefaultDockerIsolationConfig())
16+
cases := map[string]string{
17+
"uvx.exe": cmdUvx,
18+
"npx.cmd": cmdNpx,
19+
"NPX.CMD": cmdNpx,
20+
"npm.cmd": cmdNpm,
21+
"yarn.cmd": cmdYarn,
22+
"pip.exe": cmdPip,
23+
"go.exe": cmdGo,
24+
"uvx": cmdUvx,
25+
"my-tool": "binary",
26+
}
27+
for cmd, want := range cases {
28+
assert.Equal(t, want, im.DetectRuntimeType(cmd), cmd)
29+
}
30+
}

0 commit comments

Comments
 (0)