fix(studio): derive Node MCP names from script entry points

This commit is contained in:
xiexian committed 2026-10-03 00:38:57 +08:00
1 parent 77990ed9eb
commit 9097d2a036
3 files changed
+126 -1

No files matched your search

+22 -1
View File
@@ -166,7 +166,7 @@ func NameFromArgv(command string, args []string) string {
}
}
case "node":
if operand := firstCommandOperand(args); operand != "" {
if operand := nodeCommandOperand(args); operand != "" {
candidate = operand
}
case "uv":
@@ -221,6 +221,27 @@ func firstCommandOperand(args []string) string {
return ""
}
func nodeCommandOperand(args []string) string {
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--":
if i+1 < len(args) {
return args[i+1]
}
return ""
case arg == "-", arg == "-e", arg == "--eval", arg == "-p", arg == "--print",
strings.HasPrefix(arg, "--eval="), strings.HasPrefix(arg, "--print="):
return ""
case arg == "-r", arg == "--require", arg == "--import":
i++
case arg != "" && !strings.HasPrefix(arg, "-"):
return arg
}
}
return ""
}
// SanitizeName reduces a derived name to the lowercase-and-dashes form config
// and tool prefixes accept.
func SanitizeName(raw string) string {
@@ -0,0 +1,53 @@
package mcpsetup
import (
"reflect"
"strings"
"testing"
)
func TestNodeServerNameUsesScriptEntryPoint(t *testing.T) {
for _, tc := range []struct {
args []string
name string
}{
{[]string{"reasonix_server.js", "client-mode"}, "reasonix-server"},
{[]string{"--require", "./preload.cjs", "reasonix_server.js", "client-mode"}, "reasonix-server"},
{[]string{"-r", "./preload.cjs", "reasonix_server.js", "client-mode"}, "reasonix-server"},
{[]string{"--require=./preload.cjs", "reasonix_server.js", "client-mode"}, "reasonix-server"},
{[]string{"--import", "./preload.mjs", "reasonix_server.js", "client-mode"}, "reasonix-server"},
{[]string{"--import=./preload.mjs", "reasonix_server.js", "client-mode"}, "reasonix-server"},
{[]string{"--require", "./preload.cjs", "--import", "./preload.mjs", "reasonix_server.js"}, "reasonix-server"},
{[]string{"--", "reasonix_server.js", "--require", "client-mode"}, "reasonix-server"},
{[]string{"-e", "0", "client-mode"}, "mcp-server"},
{[]string{"--eval", "0", "client-mode"}, "mcp-server"},
{[]string{"--eval=0", "client-mode"}, "mcp-server"},
{[]string{"-p", "0", "client-mode"}, "mcp-server"},
{[]string{"--print", "0", "client-mode"}, "mcp-server"},
{[]string{"--print=true", "0", "client-mode"}, "mcp-server"},
{[]string{"--require", "./preload.cjs", "--eval", "0", "client-mode"}, "mcp-server"},
{[]string{"-", "client-mode"}, "mcp-server"},
{[]string{"--require", "./preload.cjs"}, "mcp-server"},
{[]string{"--require"}, "mcp-server"},
} {
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
if got := NameFromArgv("node", tc.args); got != tc.name {
t.Errorf("name = %q, want %q", got, tc.name)
}
entry, err := ParseArgs(append([]string{"--", "node"}, tc.args...))
if err != nil || entry.Name != tc.name || entry.Command != "node" || !reflect.DeepEqual(entry.Args, tc.args) {
t.Errorf("CLI entry = %+v, err=%v", entry, err)
}
explicit, err := ParseArgs(append([]string{"manual", "--", "node"}, tc.args...))
if err != nil || explicit.Name != "manual" || !reflect.DeepEqual(explicit.Args, tc.args) {
t.Errorf("explicit entry = %+v, err=%v", explicit, err)
}
})
}
if got := NameFromArgv("npx", []string{"-p", "bootstrap", "demo-server"}); got != "demo-server" {
t.Errorf("npx control = %q", got)
}
if got := NameFromArgv("python3", []string{"-m", "demo_server"}); got != "demo-server" {
t.Errorf("Python control = %q", got)
}
}
@@ -0,0 +1,51 @@
package serve
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"reasonix/internal/contract/config"
"reasonix/internal/session/control"
)
func TestMCPParseNodeNameUsesScriptEntryPoint(t *testing.T) {
ctrl := control.New(control.Options{})
t.Cleanup(ctrl.Close)
srv := httptest.NewServer(operatorHandler(New(ctrl, NewBroadcaster(), config.ServeConfig{})))
t.Cleanup(srv.Close)
for _, tc := range []struct {
input string
name string
}{
{"node --require ./preload.cjs reasonix_server.js client-mode", "reasonix-server"},
{"node --require=./preload.cjs reasonix_server.js client-mode", "reasonix-server"},
{"node --import ./preload.mjs reasonix_server.js client-mode", "reasonix-server"},
{"node --import=./preload.mjs reasonix_server.js client-mode", "reasonix-server"},
{"node --eval 0 client-mode", "mcp-server"},
{"node - client-mode", "mcp-server"},
} {
resp := postJSON(t, srv.URL+"/mcp/parse", map[string]string{"input": tc.input})
var got struct {
Servers []draftServer `json:"servers"`
Risks []draftRisk `json:"risks"`
}
err := json.NewDecoder(resp.Body).Decode(&got)
resp.Body.Close()
if err != nil || resp.StatusCode != http.StatusOK || len(got.Servers) != 1 {
t.Fatalf("preview %q: status=%d body=%+v err=%v", tc.input, resp.StatusCode, got, err)
}
entry := got.Servers[0]
if entry.Name != tc.name || entry.Transport != "stdio" || entry.Command+" "+strings.Join(entry.Args, " ") != tc.input {
t.Errorf("preview %q = %+v, want name %q and original argv", tc.input, entry, tc.name)
}
if len(got.Risks) != 1 || got.Risks[0].Server != tc.name || got.Risks[0].Kind != "shell" || got.Risks[0].Detail != tc.input {
t.Errorf("preview disclosure = %+v", got.Risks)
}
}
if names := ctrl.ConfiguredMCPNames(); len(names) != 0 {
t.Errorf("preview installed servers: %v", names)
}
}