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