Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
47cbfa7
Add capability installer for MCP servers and skills
SivanCola Jun 5, 2026
de9bbac
Honor local MCP executable command overrides
SivanCola Jun 5, 2026
97da528
Fix capability installer CI issues
SivanCola Jun 6, 2026
ca501ce
refactor(config): move global MCP and skills to isolated files
SivanCola Jun 6, 2026
bf51318
fix(cli): reject setup custom config paths
SivanCola Jun 6, 2026
a7dc7a3
feat: configure subagent model effort
SivanCola Jun 6, 2026
e2e939f
feat: configure subagent model effort
SivanCola Jun 6, 2026
203ea28
Merge branch 'codex/mcp-skills' into sivancola_0606_dev
SivanCola Jun 6, 2026
b920eec
Merge branch 'sivancola_0606_dev' of github.com:esengine/DeepSeek-Rea…
SivanCola Jun 6, 2026
a6145d0
Merge PR #3330 into sivancola_0606_dev
SivanCola Jun 6, 2026
a5e5f58
Support nested skill discovery
SivanCola Jun 6, 2026
f88cae9
Fix sivancola dev CI issues
SivanCola Jun 6, 2026
4025d4b
Merge branch 'sivancola_0606_dev' of github.com:esengine/DeepSeek-Rea…
SivanCola Jun 6, 2026
2f5d335
Merge nested skill discovery into sivancola dev
SivanCola Jun 6, 2026
6f58bf7
fix(cli): honor subagent profiles in ACP task
SivanCola Jun 6, 2026
da8c03c
Merge PR #3330 ACP fix into sivancola_0606_dev
SivanCola Jun 6, 2026
4512c2d
fix(agent): satisfy subagent profile CI checks
SivanCola Jun 6, 2026
ebcc830
Merge PR #3330 CI fix into sivancola_0606_dev
SivanCola Jun 6, 2026
41b3b75
Fix skill installer canonical layout
SivanCola Jun 6, 2026
093f077
Simplify MCP startup behavior
SivanCola Jun 6, 2026
46a25c4
Merge codex/mcp-skills into sivancola_0606_dev
SivanCola Jun 6, 2026
3090367
Merge MCP startup simplification into sivancola_0606_dev
SivanCola Jun 6, 2026
02e352f
Merge latest sivancola_0606_dev
SivanCola Jun 6, 2026
9546bba
Fix skill install canonical format
SivanCola Jun 6, 2026
f9aa008
Merge codex/mcp-skills into sivancola_0606_dev
SivanCola Jun 6, 2026
90e9e67
fix(config): preserve inline skills during import
SivanCola Jun 6, 2026
aff190d
Fix Windows skill path assertion
SivanCola Jun 6, 2026
49d2892
Merge codex/mcp-skills into sivancola_0606_dev
SivanCola Jun 6, 2026
24f8b72
fix(test): align CI expectations with global config split
SivanCola Jun 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ url = "https://mcp.stripe.com"
headers = { Authorization = "Bearer ${STRIPE_KEY}" }
```

Enabled MCP servers start connecting automatically in the background after a
session begins, so chat stays usable while tools come online. Use `/mcp` or the
desktop MCP panel to refresh status, reconnect a server, inspect failures, or
disable a server for the current session.

**Already have an `.mcp.json`?** Drop it in the project root and Reasonix
reads it as-is — the `mcpServers` spec (`command`/`args`/`env`, `type`/`url`/
`headers`, `${VAR}` expansion) maps field-for-field onto `[[plugins]]`. Both
Expand Down
110 changes: 51 additions & 59 deletions desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1467,14 +1467,17 @@ func skillRootsView() []SkillRootView {
cfg, _ := config.Load()
userCfg := config.LoadForEdit(config.UserConfigPath())
var custom []string
maxDepth := 3
if cfg != nil {
custom = cfg.SkillCustomPaths()
maxDepth = cfg.SkillMaxDepth()
}
st := skill.New(skill.Options{ProjectRoot: cwd, CustomPaths: custom, DisableBuiltins: true, Stderr: io.Discard})
st := skill.New(skill.Options{ProjectRoot: cwd, CustomPaths: custom, MaxDepth: maxDepth, DisableBuiltins: true, Stderr: io.Discard})
counts := map[string]int{}
skillItems := map[string][]SkillRootSkillView{}
roots := st.Roots()
for _, sk := range st.List() {
root := config.CanonicalSkillPath(filepath.Dir(skillRootPath(sk.Path)))
root := skillDisplayRoot(sk, roots)
counts[root]++
skillItems[root] = append(skillItems[root], SkillRootSkillView{
Name: sk.Name,
Expand All @@ -1495,7 +1498,7 @@ func skillRootsView() []SkillRootView {
}
}
out := []SkillRootView{}
for _, r := range st.Roots() {
for _, r := range roots {
dir := config.CanonicalSkillPath(r.Dir)
view := SkillRootView{
Dir: r.Dir,
Expand Down Expand Up @@ -1626,6 +1629,21 @@ func skillRootPath(path string) string {
return path
}

func skillDisplayRoot(sk skill.Skill, roots []skill.Root) string {
cleanPath := filepath.Clean(sk.Path)
for _, r := range roots {
if r.Scope != sk.Scope {
continue
}
cleanRoot := filepath.Clean(r.Dir)
prefix := cleanRoot + string(filepath.Separator)
if cleanPath == cleanRoot || strings.HasPrefix(cleanPath, prefix) {
return config.CanonicalSkillPath(r.Dir)
}
}
return config.CanonicalSkillPath(filepath.Dir(skillRootPath(sk.Path)))
}

// MCPServerInput is the drawer's "add server" form. Transport is "stdio" (Command
// + Args + Env) or "http"/"sse" (URL). Mirrors config.PluginEntry's writable shape.
type MCPServerInput struct {
Expand All @@ -1635,7 +1653,6 @@ type MCPServerInput struct {
Args []string `json:"args"`
URL string `json:"url"`
Env map[string]string `json:"env"`
Tier string `json:"tier"`
}

// AddMCPServer connects a server live and persists it to config (Customize → MCP →
Expand All @@ -1652,8 +1669,8 @@ func (a *App) AddMCPServer(in MCPServerInput) (int, error) {
Args: in.Args,
URL: in.URL,
Env: in.Env,
Tier: normalizeMCPTier(in.Tier),
}
entry, _ = config.NormalizePluginCommandLine(entry)
if err := a.saveDesktopMCPServer(entry); err != nil {
return 0, err
}
Expand Down Expand Up @@ -1684,10 +1701,11 @@ func (a *App) UpdateMCPServer(name string, in MCPServerInput) error {
updated.Command = strings.TrimSpace(in.Command)
updated.Args = append([]string(nil), in.Args...)
updated.URL = strings.TrimSpace(in.URL)
updated.Tier = normalizeMCPTier(in.Tier)
updated.Tier = ""
if in.Env != nil {
updated.Env = in.Env
}
updated, _ = config.NormalizePluginCommandLine(updated)
if updated.Type == "stdio" {
updated.URL = ""
} else {
Expand Down Expand Up @@ -1743,15 +1761,26 @@ func (a *App) RemoveMCPServer(name string) error {
return fmt.Errorf("no MCP server named %q", name)
}

// RetryMCPServer reconnects a configured server that failed or was disconnected,
// without touching config (the failed row's retry button).
func (a *App) RetryMCPServer(name string) error {
// ReconnectMCPServer disconnects the server if it is already connected (to force
// a fresh handshake and tool re-registration), then reconnects. Failures are
// recorded on the Host so the UI can render them.
func (a *App) ReconnectMCPServer(name string) error {
tab := a.activeTab()
if tab == nil || tab.Ctrl == nil {
return fmt.Errorf("no active session")
}
if mcpConnected(tab.Ctrl, name) {
tab.Ctrl.DisconnectMCPServer(name)
}
_, err := a.connectConfiguredMCPServerForTab(tab, name)
return err
if err != nil {
recordMCPFailure(tab.Ctrl, config.PluginEntry{Name: name}, err)
return err
}
a.mu.Lock()
delete(tab.disabledMCP, name)
a.mu.Unlock()
return nil
}

// ClearMCPServerAuthentication removes local auth-like config for one MCP and
Expand Down Expand Up @@ -1829,9 +1858,9 @@ func (a *App) connectConfiguredMCPServerForTab(tab *WorkspaceTab, name string) (
return 0, fmt.Errorf("no configured MCP server named %q", name)
}

// SetMCPServerTier persists how a configured MCP server should start on future
// sessions. It does not tear down a connected server; the per-session toggle and
// "connect now" remain separate controls.
// SetMCPServerTier is kept for old desktop bindings. New config writes drop the
// retired tier field, so this only affects the active session before the next
// config reload.
func (a *App) SetMCPServerTier(name, tier string) error {
if name == "codegraph" {
return a.setCodegraphTier(tier)
Expand Down Expand Up @@ -1878,9 +1907,6 @@ func (a *App) setCodegraphEnabled(enabled bool) error {
if err := cfg.SaveTo(path); err != nil {
return err
}
if err := a.syncProjectCodegraphOverride(cfg.Codegraph); err != nil {
return err
}
if enabled {
a.mu.Lock()
delete(tab.disabledMCP, "codegraph")
Expand Down Expand Up @@ -1917,9 +1943,6 @@ func (a *App) setCodegraphTier(tier string) error {
if err := cfg.SaveTo(path); err != nil {
return err
}
if err := a.syncProjectCodegraphOverride(cfg.Codegraph); err != nil {
return err
}
tab := a.activeTab()
if tab == nil || tab.Ctrl == nil {
return nil
Expand Down Expand Up @@ -1953,37 +1976,21 @@ func (a *App) desktopMCPServerForEdit(name string) (config.PluginEntry, bool, er
}

func (a *App) saveDesktopMCPServer(entry config.PluginEntry) error {
cfg, path, err := a.loadDesktopUserConfigForEdit()
if err != nil {
if err := config.UpsertMCPPlugin(entry); err != nil {
return err
}
if err := cfg.UpsertPlugin(entry); err != nil {
return err
}
if err := cfg.SaveTo(path); err != nil {
return err
}
_, err = a.removeProjectMCPOverride(entry.Name)
return err
// Project-level overrides are no longer supported; clean up legacy entries.
_, _ = a.removeProjectMCPOverride(entry.Name)
return nil
}

func (a *App) removeDesktopMCPServer(name string) (bool, error) {
removed := false
cfg, path, err := a.loadDesktopUserConfigForEdit()
found, err := config.RemoveMCPPlugin(name)
if err != nil {
return false, err
}
if cfg.RemovePlugin(name) {
removed = true
if err := cfg.SaveTo(path); err != nil {
return false, err
}
}
projectRemoved, err := a.removeProjectMCPOverride(name)
if err != nil {
return removed, err
}
return removed || projectRemoved, nil
projectRemoved, _ := a.removeProjectMCPOverride(name)
return found || projectRemoved, nil
}

func (a *App) removeProjectMCPOverride(name string) (bool, error) {
Expand All @@ -2008,23 +2015,6 @@ func (a *App) removeProjectMCPOverride(name string) (bool, error) {
return true, nil
}

func (a *App) syncProjectCodegraphOverride(c config.CodegraphConfig) error {
path := projectConfigPathForRoot(a.activeWorkspaceRoot())
userPath := config.UserConfigPath()
if path == "" || sameConfigPath(path, userPath) {
return nil
}
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
cfg := config.LoadForEdit(path)
cfg.Codegraph = c
return cfg.SaveTo(path)
}

func findPluginEntry(entries []config.PluginEntry, name string) (config.PluginEntry, bool) {
for _, p := range entries {
if p.Name == name {
Expand All @@ -2040,6 +2030,8 @@ func normalizeMCPTier(tier string) string {
return "eager"
case "background":
return "background"
case "":
return "background"
default:
return "lazy"
}
Expand Down
Loading
Loading