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 "you": {
66 Name: "you",
67 Description: "Current web search via the keyless You.com MCP server (you-search). Add an Authorization Bearer header with a YDC_API_KEY for additional authenticated tools.",
68 Config: func(cwd string) ServerConfig {
69 return ServerConfig{
70 Transport: "streamable-http",
71 URL: "https://api.you.com/mcp?profile=free",
72 }
73 },
74 },
75 }
76}
77
78func setupHelp() string {
79 templates := setupTemplates()
80 names := make([]string, 0, len(templates))
81 for name := range templates {
82 names = append(names, name)
83 }
84 sort.Strings(names)
85
86 var b strings.Builder
87 b.WriteString("mcp-bridge setup\n\n")
88 b.WriteString("Add a known MCP server template to your zot MCP config.\n\n")
89 b.WriteString("Usage:\n")
90 b.WriteString(" /mcp setup add <template> [--global|--project] [--name <server-name>]\n")
91 b.WriteString(" /mcp setup templates\n\n")
92 b.WriteString("Templates:\n")
93 for _, name := range names {
94 t := templates[name]
95 b.WriteString(fmt.Sprintf(" %-12s %s\n", t.Name, t.Description))
96 }
97 b.WriteString("\nExamples:\n")
98 b.WriteString(" /mcp setup add grep\n")
99 b.WriteString(" /mcp setup add filesystem --project\n")
100 b.WriteString(" /mcp setup add context7 --global --name docs\n")
101 b.WriteString("\nDefault target is global: $ZOT_HOME/mcp.json. Run /reload-ext after changes.\n")
102 return b.String()
103}
104
105func handleSetup(args []string, cwd string) (string, error) {
106 if len(args) == 0 || args[0] == "help" {
107 return setupHelp(), nil
108 }
109 if args[0] == "templates" || args[0] == "list" {
110 return setupHelp(), nil
111 }
112 if args[0] != "add" {
113 return "", fmt.Errorf("unknown setup command %q\n\n%s", args[0], setupHelp())
114 }
115 if len(args) < 2 {
116 return "", fmt.Errorf("usage: /mcp setup add <template> [--global|--project] [--name <server-name>]")
117 }
118
119 templateName := args[1]
120 templates := setupTemplates()
121 t, ok := templates[templateName]
122 if !ok {
123 return "", fmt.Errorf("unknown template %q\n\n%s", templateName, setupHelp())
124 }
125
126 target := "global"
127 serverName := templateName
128 for i := 2; i < len(args); i++ {
129 switch args[i] {
130 case "--global":
131 target = "global"
132 case "--project":
133 target = "project"
134 case "--name":
135 if i+1 >= len(args) {
136 return "", fmt.Errorf("--name requires a value")
137 }
138 serverName = args[i+1]
139 i++
140 default:
141 return "", fmt.Errorf("unknown setup option %q", args[i])
142 }
143 }
144
145 path := filepath.Join(zotHome(), "mcp.json")
146 if target == "project" {
147 if cwd == "" {
148 return "", fmt.Errorf("--project requires a working directory, but none is known")
149 }
150 path = filepath.Join(cwd, ".zot", "mcp.json")
151 }
152
153 cfg, err := readConfigFile(path)
154 if err != nil {
155 return "", err
156 }
157 if cfg.MCPServers == nil {
158 cfg.MCPServers = map[string]ServerConfig{}
159 }
160 if _, exists := cfg.MCPServers[serverName]; exists {
161 return "", fmt.Errorf("server %q already exists in %s", serverName, path)
162 }
163
164 cfg.MCPServers[serverName] = t.Config(cwd)
165 if err := writeConfigFile(path, cfg); err != nil {
166 return "", err
167 }
168
169 return fmt.Sprintf("Added MCP server %q from template %q to %s.\n\nRun /reload-ext to reload MCP tools.", serverName, templateName, path), nil
170}
171
172func readConfigFile(path string) (Config, error) {
173 cfg := Config{MCPServers: map[string]ServerConfig{}}
174 data, err := os.ReadFile(path)
175 if err != nil {
176 if errors.Is(err, fs.ErrNotExist) {
177 return cfg, nil
178 }
179 return cfg, err
180 }
181 if len(strings.TrimSpace(string(data))) == 0 {
182 return cfg, nil
183 }
184 if err := json.Unmarshal(data, &cfg); err != nil {
185 return cfg, fmt.Errorf("parse %s: %w", path, err)
186 }
187 if cfg.MCPServers == nil {
188 cfg.MCPServers = map[string]ServerConfig{}
189 }
190 return cfg, nil
191}
192
193func writeConfigFile(path string, cfg Config) error {
194 data, err := json.MarshalIndent(cfg, "", " ")
195 if err != nil {
196 return err
197 }
198 data = append(data, '\n')
199 // 0o600: mcp.json can contain auth headers / tokens.
200 return writeFileAtomic(path, data, 0o600)
201}