v0.2.78 cc6b04d

config.go RawHistoryFolder

1// config.go — MCP server configuration loading.
2//
3// Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.)
4// from two locations:
5//
6//  1. Global:  $ZOT_HOME/mcp.json
7//  2. Project: .zot/mcp.json       (in the current working directory)
8//
9// Project config overrides global config per-server (shallow merge).
10package main
11
12import (
13	"encoding/json"
14	"errors"
15	"fmt"
16	"io/fs"
17	"os"
18	"path/filepath"
19	"runtime"
20)
21
22// ServerConfig describes one MCP server entry.
23type ServerConfig struct {
24	// Stdio transport fields
25	Command string            `json:"command,omitempty"` // executable to spawn
26	Args    []string          `json:"args,omitempty"`    // arguments
27	Env     map[string]string `json:"env,omitempty"`     // extra env vars
28
29	// HTTP transport fields
30	Transport string            `json:"transport,omitempty"` // "stdio" (default) | "streamable-http" | "sse"
31	URL       string            `json:"url,omitempty"`       // server URL for HTTP transports
32	Headers   map[string]string `json:"headers,omitempty"`   // custom HTTP headers
33
34	// Timeouts (in seconds)
35	ConnectTimeout int `json:"connectTimeout,omitempty"` // connection timeout (default: 30)
36	RequestTimeout int `json:"requestTimeout,omitempty"` // per-request timeout (default: 60)
37	IdleTimeout    int `json:"idleTimeout,omitempty"`    // idle timeout before stopping (default: 300)
38}
39
40// Config is the top-level MCP configuration.
41type Config struct {
42	MCPServers map[string]ServerConfig `json:"mcpServers"`
43}
44
45// zotHome returns the zot state directory.
46func zotHome() string {
47	if h := os.Getenv("ZOT_HOME"); h != "" {
48		return h
49	}
50	if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" {
51		return filepath.Join(xdg, "zot")
52	}
53	switch runtime.GOOS {
54	case "darwin":
55		home, _ := os.UserHomeDir()
56		return filepath.Join(home, "Library", "Application Support", "zot")
57	case "windows":
58		return filepath.Join(os.Getenv("APPDATA"), "zot")
59	default: // linux, freebsd, etc.
60		home, _ := os.UserHomeDir()
61		return filepath.Join(home, ".local", "state", "zot")
62	}
63}
64
65// loadConfig reads and merges global + project MCP configs.
66// cwd is the current working directory (for project config lookup).
67func loadConfig(cwd string) (Config, error) {
68	cfg := Config{MCPServers: make(map[string]ServerConfig)}
69
70	// 1. Global config
71	globalPath := filepath.Join(zotHome(), "mcp.json")
72	if err := mergeConfig(&cfg, globalPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
73		return cfg, fmt.Errorf("global config %s: %w", globalPath, err)
74	}
75
76	// 2. Project config (overrides global per-server)
77	if cwd != "" {
78		projectPath := filepath.Join(cwd, ".zot", "mcp.json")
79		if err := mergeConfig(&cfg, projectPath); err != nil && !errors.Is(err, fs.ErrNotExist) {
80			return cfg, fmt.Errorf("project config %s: %w", projectPath, err)
81		}
82	}
83
84	return cfg, nil
85}
86
87// mergeConfig reads a JSON config file and merges its servers into cfg.
88func mergeConfig(cfg *Config, path string) error {
89	data, err := os.ReadFile(path)
90	if err != nil {
91		return err
92	}
93	var file Config
94	if err := json.Unmarshal(data, &file); err != nil {
95		return fmt.Errorf("parse %s: %w", path, err)
96	}
97	for name, srv := range file.MCPServers {
98		// Apply defaults
99		if srv.Transport == "" {
100			srv.Transport = "stdio"
101		}
102		if srv.ConnectTimeout == 0 {
103			srv.ConnectTimeout = 30
104		}
105		if srv.RequestTimeout == 0 {
106			srv.RequestTimeout = 60
107		}
108		if srv.IdleTimeout == 0 {
109			srv.IdleTimeout = 300
110		}
111		cfg.MCPServers[name] = srv
112	}
113	return nil
114}

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