1package main
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "io/fs"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12)
13
14type serverTemplate struct {
15 Name string
16 Description string
17 Config func(cwd string) ServerConfig
18}
19
20func setupTemplates() map[string]serverTemplate {
21 return map[string]serverTemplate{
22 "grep": {
23 Name: "grep",
24 Description: "Search real-world code across public GitHub repositories via grep.app.",
25 Config: func(cwd string) ServerConfig {
26 return ServerConfig{
27 Transport: "streamable-http",
28 URL: "https://mcp.grep.app/",
29 }
30 },
31 },
32 "filesystem": {
33 Name: "filesystem",
34 Description: "Read/write files under the current project directory using the official filesystem MCP server.",
35 Config: func(cwd string) ServerConfig {
36 return ServerConfig{
37 Transport: "stdio",
38 Command: "npx",
39 Args: []string{"-y", "@modelcontextprotocol/server-filesystem", cwd},
40 }
41 },
42 },
43 "context7": {
44 Name: "context7",
45 Description: "Fetch up-to-date library documentation and examples via Context7.",
46 Config: func(cwd string) ServerConfig {
47 return ServerConfig{
48 Transport: "stdio",
49 Command: "npx",
50 Args: []string{"-y", "@upstash/context7-mcp@latest"},
51 }
52 },
53 },
54 "playwright": {
55 Name: "playwright",
56 Description: "Browser automation via Playwright MCP.",
57 Config: func(cwd string) ServerConfig {
58 return ServerConfig{
59 Transport: "stdio",
60 Command: "npx",
61 Args: []string{"-y", "@executeautomation/playwright-mcp-server"},
62 }
63 },
64 },
65 }
66}
67
68func setupHelp() string {
69 templates := setupTemplates()
70 names := make([]string, 0, len(templates))
71 for name := range templates {
72 names = append(names, name)
73 }
74 sort.Strings(names)
75
76 var b strings.Builder
77 b.WriteString("mcp-bridge setup\n\n")
78 b.WriteString("Add a known MCP server template to your zut MCP config.\n\n")
79 b.WriteString("Usage:\n")
80 b.WriteString(" /mcp setup add <template> [--global|--project] [--name <server-name>]\n")
81 b.WriteString(" /mcp setup templates\n\n")
82 b.WriteString("Templates:\n")
83 for _, name := range names {
84 t := templates[name]
85 b.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, t.Description))
86 }
87 b.WriteString("\nExamples:\n")
88 b.WriteString(" /mcp setup add grep\n")
89 b.WriteString(" /mcp setup add filesystem --project\n")
90 b.WriteString(" /mcp setup add context7 --global --name docs\n")
91 b.WriteString("\nDefault target is global: $ZUT_HOME/mcp.json. Run /reload-ext after changes.\n")
92 return b.String()
93}
94
95func handleSetup(args []string, cwd string) (string, error) {
96 if len(args) == 0 || args[0] == "help" {
97 return setupHelp(), nil
98 }
99 if args[0] == "templates" || args[0] == "list" {
100 return setupHelp(), nil
101 }
102 if args[0] != "add" {
103 return "", fmt.Errorf("unknown setup command %q\n\n%s", args[0], setupHelp())
104 }
105 if len(args) < 2 {
106 return "", fmt.Errorf("usage: /mcp setup add <template> [--global|--project] [--name <server-name>]")
107 }
108
109 templateName := args[1]
110 templates := setupTemplates()
111 t, ok := templates[templateName]
112 if !ok {
113 return "", fmt.Errorf("unknown template %q\n\n%s", templateName, setupHelp())
114 }
115
116 target := "global"
117 serverName := templateName
118 for i := 2; i < len(args); i++ {
119 switch args[i] {
120 case "--global":
121 target = "global"
122 case "--project":
123 target = "project"
124 case "--name":
125 if i+1 >= len(args) {
126 return "", fmt.Errorf("--name requires a value")
127 }
128 serverName = args[i+1]
129 i++
130 default:
131 return "", fmt.Errorf("unknown setup option %q", args[i])
132 }
133 }
134
135 path := filepath.Join(zutHome(), "mcp.json")
136 if target == "project" {
137 if cwd == "" {
138 return "", fmt.Errorf("--project requires a working directory, but none is known")
139 }
140 path = filepath.Join(cwd, ".zut", "mcp.json")
141 }
142
143 cfg, err := readConfigFile(path)
144 if err != nil {
145 return "", err
146 }
147 if cfg.MCPServers == nil {
148 cfg.MCPServers = map[string]ServerConfig{}
149 }
150 if _, exists := cfg.MCPServers[serverName]; exists {
151 return "", fmt.Errorf("server %q already exists in %s", serverName, path)
152 }
153
154 cfg.MCPServers[serverName] = t.Config(cwd)
155 if err := writeConfigFile(path, cfg); err != nil {
156 return "", err
157 }
158
159 return fmt.Sprintf("Added MCP server %q from template %q to %s.\n\nRun /reload-ext to reload MCP tools.", serverName, templateName, path), nil
160}
161
162func readConfigFile(path string) (Config, error) {
163 cfg := Config{MCPServers: map[string]ServerConfig{}}
164 data, err := os.ReadFile(path)
165 if err != nil {
166 if errors.Is(err, fs.ErrNotExist) {
167 return cfg, nil
168 }
169 return cfg, err
170 }
171 if len(strings.TrimSpace(string(data))) == 0 {
172 return cfg, nil
173 }
174 if err := json.Unmarshal(data, &cfg); err != nil {
175 return cfg, fmt.Errorf("parse %s: %w", path, err)
176 }
177 if cfg.MCPServers == nil {
178 cfg.MCPServers = map[string]ServerConfig{}
179 }
180 return cfg, nil
181}
182
183func writeConfigFile(path string, cfg Config) error {
184 data, err := json.MarshalIndent(cfg, "", " ")
185 if err != nil {
186 return err
187 }
188 data = append(data, '\n')
189 // 0o600: mcp.json can contain auth headers / tokens.
190 return writeFileAtomic(path, data, 0o600)
191}