mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 04:25:46 +02:00
* fix: harden Windows browser MCP config Co-authored-by: multica-agent <github@multica.ai> * fix: address browser mcp review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
206 lines
4.6 KiB
Go
206 lines
4.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
browserMcpGOOS = runtime.GOOS
|
|
browserMcpStat = os.Stat
|
|
browserMcpEnv = os.Getenv
|
|
)
|
|
|
|
func hardenBrowserMcpConfig(raw json.RawMessage, tempDir string) ([]byte, error) {
|
|
if browserMcpGOOS != "windows" {
|
|
return raw, nil
|
|
}
|
|
return hardenWindowsBrowserMcpConfig(raw, tempDir)
|
|
}
|
|
|
|
func hardenWindowsBrowserMcpConfig(raw json.RawMessage, tempDir string) ([]byte, error) {
|
|
var top map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &top); err != nil {
|
|
return raw, nil
|
|
}
|
|
serversRaw, ok := top["mcpServers"]
|
|
if !ok {
|
|
return raw, nil
|
|
}
|
|
var servers map[string]json.RawMessage
|
|
if err := json.Unmarshal(serversRaw, &servers); err != nil {
|
|
return raw, nil
|
|
}
|
|
|
|
changed := false
|
|
for name, serverRaw := range servers {
|
|
var entry map[string]any
|
|
if err := json.Unmarshal(serverRaw, &entry); err != nil {
|
|
continue
|
|
}
|
|
args, ok := stringSlice(entry["args"])
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
lowerName := strings.ToLower(name)
|
|
switch {
|
|
case lowerName == "playwright" || argsContain(args, "@playwright/mcp") || argsContain(args, `@playwright\mcp`):
|
|
nextArgs, err := hardenWindowsPlaywrightMcpArgs(args, tempDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !sameStringSlice(args, nextArgs) {
|
|
entry["args"] = nextArgs
|
|
servers[name], changed = mustMarshalRaw(entry), true
|
|
}
|
|
case lowerName == "chrome-devtools" || argsContain(args, "chrome-devtools-mcp"):
|
|
if path, ok := windowsChromiumFallbackExecutable(); ok && shouldPinChromeDevToolsExecutable(args) {
|
|
entry["args"] = append(args, "--executablePath="+path)
|
|
servers[name], changed = mustMarshalRaw(entry), true
|
|
}
|
|
}
|
|
}
|
|
if !changed {
|
|
return raw, nil
|
|
}
|
|
|
|
top["mcpServers"] = mustMarshalRaw(servers)
|
|
data, err := json.Marshal(top)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal hardened mcp config: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func hardenWindowsPlaywrightMcpArgs(args []string, tempDir string) ([]string, error) {
|
|
if hasFlag(args, "--config") || hasFlag(args, "--cdp-endpoint") || hasFlag(args, "--extension") {
|
|
return args, nil
|
|
}
|
|
configPath := filepath.Join(tempDir, "playwright-windows-browser.json")
|
|
config := map[string]any{
|
|
"browser": map[string]any{
|
|
"launchOptions": map[string]any{
|
|
"args": []string{"--disable-gpu"},
|
|
},
|
|
},
|
|
}
|
|
data, err := json.Marshal(config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal playwright mcp browser config: %w", err)
|
|
}
|
|
if err := os.WriteFile(configPath, data, 0o600); err != nil {
|
|
return nil, fmt.Errorf("write playwright mcp browser config: %w", err)
|
|
}
|
|
return append(args, "--config", configPath), nil
|
|
}
|
|
|
|
func windowsChromiumFallbackExecutable() (string, bool) {
|
|
if path := strings.TrimSpace(browserMcpEnv("MULTICA_CHROME_DEVTOOLS_EXECUTABLE_PATH")); path != "" {
|
|
return path, true
|
|
}
|
|
for _, root := range []string{
|
|
browserMcpEnv("ProgramFiles(x86)"),
|
|
browserMcpEnv("ProgramFiles"),
|
|
browserMcpEnv("LocalAppData"),
|
|
} {
|
|
if strings.TrimSpace(root) == "" {
|
|
continue
|
|
}
|
|
path := windowsPathJoin(root, "Microsoft", "Edge", "Application", "msedge.exe")
|
|
if _, err := browserMcpStat(path); err == nil {
|
|
return path, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func windowsPathJoin(root string, elems ...string) string {
|
|
root = strings.TrimRight(root, `\/`)
|
|
if root == "" {
|
|
return ""
|
|
}
|
|
return root + `\` + strings.Join(elems, `\`)
|
|
}
|
|
|
|
func shouldPinChromeDevToolsExecutable(args []string) bool {
|
|
for _, flag := range []string{
|
|
"--executablePath",
|
|
"--executable-path",
|
|
"-e",
|
|
"--channel",
|
|
"--browserUrl",
|
|
"--browser-url",
|
|
"-u",
|
|
"--wsEndpoint",
|
|
"--ws-endpoint",
|
|
"-w",
|
|
"--autoConnect",
|
|
"--auto-connect",
|
|
} {
|
|
if hasFlag(args, flag) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func stringSlice(v any) ([]string, bool) {
|
|
raw, ok := v.([]any)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
out := make([]string, 0, len(raw))
|
|
for _, item := range raw {
|
|
s, ok := item.(string)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
func argsContain(args []string, needle string) bool {
|
|
needle = strings.ToLower(needle)
|
|
for _, arg := range args {
|
|
if strings.Contains(strings.ToLower(arg), needle) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasFlag(args []string, flag string) bool {
|
|
for _, arg := range args {
|
|
if arg == flag || strings.HasPrefix(arg, flag+"=") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func sameStringSlice(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func mustMarshalRaw(v any) json.RawMessage {
|
|
data, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return data
|
|
}
|