v0.9.3 0c58a62

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	}
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}

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