v0.4.6 2ff82fa

setup.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close