on main help sign in
feat(tools): add crush_info tool implementation files

Approximate overhead: • Tool definition cost (always in prompt): ~500 bytes ≈ 130 tokens • Per invocation response (typical): ~1,500 bytes ≈ 375 tokens • Per invocation response (worst-case): ~3,000 bytes ≈ 750 tokens 💘 Generated with Crush Assisted-by: Kimi-K2.5 via Crush <crush@charm.land>

commitdf39b6443987ca34ff516efd6b62e54f59b36efa authorChristian Rocha <christian@rocha.is> dateSun 5 Apr 2026, 15:38 (6 months ago) parent2772734
3 files changed+650
internal/agent/tools/crush_info.go+334 internal/agent/tools/crush_info.md+16 internal/agent/tools/crush_info_test.go+300
internal/agent/tools/crush_info.gonew file+334
@@ -0,0 +1,334 @@
1+package tools
2+
3+import (
4+ "context"
5+ _ "embed"
6+ "fmt"
7+ "slices"
8+ "strings"
9+
10+ "charm.land/fantasy"
11+ "github.com/charmbracelet/crush/internal/agent/tools/mcp"
12+ "github.com/charmbracelet/crush/internal/config"
13+ "github.com/charmbracelet/crush/internal/lsp"
14+)
15+
16+const CrushInfoToolName = "crush_info"
17+
18+//go:embed crush_info.md
19+var crushInfoDescription []byte
20+
21+type CrushInfoParams struct{}
22+
23+func NewCrushInfoTool(
24+ cfg *config.ConfigStore,
25+ lspManager *lsp.Manager,
26+) fantasy.AgentTool {
27+ return fantasy.NewAgentTool(
28+ CrushInfoToolName,
29+ string(crushInfoDescription),
30+ func(ctx context.Context, _ CrushInfoParams, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
31+ return fantasy.NewTextResponse(buildCrushInfo(cfg, lspManager)), nil
32+ })
33+}
34+
35+func buildCrushInfo(cfg *config.ConfigStore, lspManager *lsp.Manager) string {
36+ var b strings.Builder
37+
38+ writeConfigFiles(&b, cfg)
39+ writeModels(&b, cfg)
40+ writeProviders(&b, cfg)
41+ writeLSP(&b, lspManager, cfg)
42+ writeMCP(&b, mcp.GetStates(), cfg)
43+ writePermissions(&b, cfg)
44+ writeDisabledTools(&b, cfg)
45+ writeOptions(&b, cfg)
46+
47+ return b.String()
48+}
49+
50+func writeConfigFiles(b *strings.Builder, cfg *config.ConfigStore) {
51+ b.WriteString("[config_files]\n")
52+ paths := cfg.LoadedPaths()
53+ for _, p := range paths {
54+ b.WriteString(p + "\n")
55+ }
56+ b.WriteString("\n")
57+}
58+
59+func writeModels(b *strings.Builder, cfg *config.ConfigStore) {
60+ c := cfg.Config()
61+ if len(c.Models) == 0 {
62+ return
63+ }
64+ b.WriteString("[model]\n")
65+ for _, typ := range []config.SelectedModelType{config.SelectedModelTypeLarge, config.SelectedModelTypeSmall} {
66+ m, ok := c.Models[typ]
67+ if !ok {
68+ continue
69+ }
70+ fmt.Fprintf(b, "%s = %s (%s)\n", typ, m.Model, m.Provider)
71+ }
72+ b.WriteString("\n")
73+}
74+
75+func writeProviders(b *strings.Builder, cfg *config.ConfigStore) {
76+ c := cfg.Config()
77+ type pv struct {
78+ name string
79+ count int
80+ }
81+ var providers []pv
82+ for name, pc := range c.Providers.Seq2() {
83+ if pc.Disable {
84+ continue
85+ }
86+ providers = append(providers, pv{name: name, count: len(pc.Models)})
87+ }
88+ if len(providers) == 0 {
89+ return
90+ }
91+ slices.SortFunc(providers, func(a, b pv) int { return strings.Compare(a.name, b.name) })
92+ b.WriteString("[providers]\n")
93+ for _, p := range providers {
94+ fmt.Fprintf(b, "%s = enabled (%d models)\n", p.name, p.count)
95+ }
96+ b.WriteString("\n")
97+}
98+
99+func writeLSP(b *strings.Builder, lspManager *lsp.Manager, cfg *config.ConfigStore) {
100+ // Write runtime LSP clients
101+ if lspManager != nil && lspManager.Clients().Len() > 0 {
102+ type entry struct {
103+ name string
104+ state lsp.ServerState
105+ fileTypes []string
106+ }
107+ var entries []entry
108+ for name, client := range lspManager.Clients().Seq2() {
109+ entries = append(entries, entry{
110+ name: name,
111+ state: client.GetServerState(),
112+ fileTypes: client.FileTypes(),
113+ })
114+ }
115+ if len(entries) > 0 {
116+ slices.SortFunc(entries, func(a, b entry) int { return strings.Compare(a.name, b.name) })
117+ b.WriteString("[lsp]\n")
118+ for _, e := range entries {
119+ stateStr := lspStateString(e.state)
120+ if len(e.fileTypes) > 0 {
121+ sorted := slices.Clone(e.fileTypes)
122+ slices.Sort(sorted)
123+ fmt.Fprintf(b, "%s = %s (%s)\n", e.name, stateStr, strings.Join(sorted, ", "))
124+ } else {
125+ fmt.Fprintf(b, "%s = %s\n", e.name, stateStr)
126+ }
127+ }
128+ b.WriteString("\n")
129+ }
130+ }
131+
132+ // Write configured but not running LSP servers
133+ c := cfg.Config()
134+ if len(c.LSP) > 0 {
135+ runtimeNames := make(map[string]bool)
136+ if lspManager != nil {
137+ for name := range lspManager.Clients().Seq2() {
138+ runtimeNames[name] = true
139+ }
140+ }
141+
142+ type configuredEntry struct {
143+ name string
144+ status string
145+ }
146+ var entries []configuredEntry
147+ for name, lspCfg := range c.LSP {
148+ // Skip if already in runtime
149+ if runtimeNames[name] {
150+ continue
151+ }
152+ status := "not_started"
153+ if lspCfg.Disabled {
154+ status = "disabled"
155+ }
156+ entries = append(entries, configuredEntry{name: name, status: status})
157+ }
158+
159+ if len(entries) > 0 {
160+ slices.SortFunc(entries, func(a, b configuredEntry) int { return strings.Compare(a.name, b.name) })
161+ b.WriteString("[lsp_configured]\n")
162+ for _, e := range entries {
163+ fmt.Fprintf(b, "%s = %s\n", e.name, e.status)
164+ }
165+ b.WriteString("\n")
166+ }
167+ }
168+}
169+
170+func writeMCP(b *strings.Builder, states map[string]mcp.ClientInfo, cfg *config.ConfigStore) {
171+ // Write runtime MCP states
172+ if len(states) > 0 {
173+ type entry struct {
174+ name string
175+ state mcp.State
176+ err error
177+ tools int
178+ resources int
179+ connectedAt string
180+ }
181+ var entries []entry
182+ for name, info := range states {
183+ e := entry{
184+ name: name,
185+ state: info.State,
186+ err: info.Error,
187+ }
188+ if info.State == mcp.StateConnected {
189+ e.tools = info.Counts.Tools
190+ e.resources = info.Counts.Resources
191+ if !info.ConnectedAt.IsZero() {
192+ e.connectedAt = info.ConnectedAt.Format("15:04:05")
193+ }
194+ }
195+ entries = append(entries, e)
196+ }
197+ slices.SortFunc(entries, func(a, b entry) int { return strings.Compare(a.name, b.name) })
198+ b.WriteString("[mcp]\n")
199+ for _, e := range entries {
200+ switch e.state {
201+ case mcp.StateConnected:
202+ if e.connectedAt != "" {
203+ fmt.Fprintf(b, "%s = connected (%d tools, %d resources) since %s\n", e.name, e.tools, e.resources, e.connectedAt)
204+ } else {
205+ fmt.Fprintf(b, "%s = connected (%d tools, %d resources)\n", e.name, e.tools, e.resources)
206+ }
207+ case mcp.StateError:
208+ if e.err != nil {
209+ fmt.Fprintf(b, "%s = error: %s\n", e.name, e.err.Error())
210+ } else {
211+ fmt.Fprintf(b, "%s = error\n", e.name)
212+ }
213+ default:
214+ fmt.Fprintf(b, "%s = %s\n", e.name, e.state)
215+ }
216+ }
217+ b.WriteString("\n")
218+ }
219+
220+ // Write configured but not running MCP servers
221+ c := cfg.Config()
222+ if len(c.MCP) > 0 {
223+ runtimeNames := make(map[string]bool)
224+ for name := range states {
225+ runtimeNames[name] = true
226+ }
227+
228+ type configuredEntry struct {
229+ name string
230+ status string
231+ }
232+ var entries []configuredEntry
233+ for name, mcpCfg := range c.MCP {
234+ // Skip if already in runtime
235+ if runtimeNames[name] {
236+ continue
237+ }
238+ status := "not_started"
239+ if mcpCfg.Disabled {
240+ status = "disabled"
241+ }
242+ entries = append(entries, configuredEntry{name: name, status: status})
243+ }
244+
245+ if len(entries) > 0 {
246+ slices.SortFunc(entries, func(a, b configuredEntry) int { return strings.Compare(a.name, b.name) })
247+ b.WriteString("[mcp_configured]\n")
248+ for _, e := range entries {
249+ fmt.Fprintf(b, "%s = %s\n", e.name, e.status)
250+ }
251+ b.WriteString("\n")
252+ }
253+ }
254+}
255+
256+func writePermissions(b *strings.Builder, cfg *config.ConfigStore) {
257+ c := cfg.Config()
258+ overrides := cfg.Overrides()
259+
260+ if c.Permissions == nil {
261+ if !overrides.SkipPermissionRequests {
262+ return
263+ }
264+ } else if !overrides.SkipPermissionRequests && len(c.Permissions.AllowedTools) == 0 {
265+ return
266+ }
267+ b.WriteString("[permissions]\n")
268+ if overrides.SkipPermissionRequests {
269+ b.WriteString("mode = yolo\n")
270+ }
271+ if c.Permissions != nil && len(c.Permissions.AllowedTools) > 0 {
272+ sorted := slices.Clone(c.Permissions.AllowedTools)
273+ slices.Sort(sorted)
274+ fmt.Fprintf(b, "allowed_tools = %s\n", strings.Join(sorted, ", "))
275+ }
276+ b.WriteString("\n")
277+}
278+
279+func writeDisabledTools(b *strings.Builder, cfg *config.ConfigStore) {
280+ c := cfg.Config()
281+ if c.Options == nil || len(c.Options.DisabledTools) == 0 {
282+ return
283+ }
284+ sorted := slices.Clone(c.Options.DisabledTools)
285+ slices.Sort(sorted)
286+ b.WriteString("[tools]\n")
287+ fmt.Fprintf(b, "disabled = %s\n", strings.Join(sorted, ", "))
288+ b.WriteString("\n")
289+}
290+
291+func writeOptions(b *strings.Builder, cfg *config.ConfigStore) {
292+ c := cfg.Config()
293+ if c.Options == nil {
294+ return
295+ }
296+ type kv struct {
297+ key string
298+ value string
299+ }
300+ var opts []kv
301+
302+ opts = append(opts, kv{"data_directory", c.Options.DataDirectory})
303+ opts = append(opts, kv{"debug", fmt.Sprintf("%v", c.Options.Debug)})
304+ autoLSP := c.Options.AutoLSP == nil || *c.Options.AutoLSP
305+ opts = append(opts, kv{"auto_lsp", fmt.Sprintf("%v", autoLSP)})
306+ autoSummarize := !c.Options.DisableAutoSummarize
307+ opts = append(opts, kv{"auto_summarize", fmt.Sprintf("%v", autoSummarize)})
308+
309+ slices.SortFunc(opts, func(a, b kv) int { return strings.Compare(a.key, b.key) })
310+ b.WriteString("[options]\n")
311+ for _, o := range opts {
312+ fmt.Fprintf(b, "%s = %s\n", o.key, o.value)
313+ }
314+ b.WriteString("\n")
315+}
316+
317+func lspStateString(state lsp.ServerState) string {
318+ switch state {
319+ case lsp.StateUnstarted:
320+ return "unstarted"
321+ case lsp.StateStarting:
322+ return "starting"
323+ case lsp.StateReady:
324+ return "ready"
325+ case lsp.StateError:
326+ return "error"
327+ case lsp.StateStopped:
328+ return "stopped"
329+ case lsp.StateDisabled:
330+ return "disabled"
331+ default:
332+ return "unknown"
333+ }
334+}
internal/agent/tools/crush_info.mdnew file+16
@@ -0,0 +1,16 @@
1+Get information about Crush's current runtime configuration and service
2+state.
3+
4+<usage>
5+- Shows active model and provider, LSP/MCP server status, permissions mode,
6+ disabled tools, and key options
7+- Use when diagnosing why something isn't working (missing diagnostics,
8+ provider errors, MCP disconnections)
9+- No parameters needed — always returns the full current state
10+</usage>
11+
12+<tips>
13+- Check [lsp] and [mcp] sections for service health
14+- Check [providers] to see which providers are enabled and available
15+- Pair with the crush-config skill to fix configuration issues
16+</tips>
internal/agent/tools/crush_info_test.gonew file+300
@@ -0,0 +1,300 @@
1+package tools
2+
3+import (
4+ "errors"
5+ "strings"
6+ "testing"
7+ "time"
8+
9+ "charm.land/catwalk/pkg/catwalk"
10+ "github.com/charmbracelet/crush/internal/agent/tools/mcp"
11+ "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/crush/internal/csync"
13+ "github.com/charmbracelet/crush/internal/lsp"
14+ "github.com/stretchr/testify/require"
15+)
16+
17+func TestCrushInfo_MinimalConfig(t *testing.T) {
18+ t.Parallel()
19+
20+ cfg := config.NewTestStore(&config.Config{
21+ Providers: csync.NewMap[string, config.ProviderConfig](),
22+ })
23+ output := buildCrushInfo(cfg, nil)
24+ require.NotContains(t, output, "[providers]")
25+ require.NotContains(t, output, "[lsp]")
26+ require.NotContains(t, output, "[mcp]")
27+ require.NotContains(t, output, "[permissions]")
28+ require.NotContains(t, output, "[tools]")
29+}
30+
31+func TestCrushInfo_ConfigFiles(t *testing.T) {
32+ t.Parallel()
33+
34+ cfg := config.NewTestStore(
35+ &config.Config{Providers: csync.NewMap[string, config.ProviderConfig]()},
36+ "/home/user/.config/crush/crush.json",
37+ "/project/.crush/crush.json",
38+ )
39+ output := buildCrushInfo(cfg, nil)
40+ require.Contains(t, output, "[config_files]")
41+ require.Contains(t, output, "/home/user/.config/crush/crush.json")
42+ require.Contains(t, output, "/project/.crush/crush.json")
43+}
44+
45+func TestCrushInfo_Models(t *testing.T) {
46+ t.Parallel()
47+
48+ cfg := config.NewTestStore(&config.Config{
49+ Models: map[config.SelectedModelType]config.SelectedModel{
50+ config.SelectedModelTypeLarge: {Model: "claude-sonnet-4-20250514", Provider: "anthropic"},
51+ config.SelectedModelTypeSmall: {Model: "claude-haiku-3-20250307", Provider: "anthropic"},
52+ },
53+ Providers: csync.NewMap[string, config.ProviderConfig](),
54+ })
55+ output := buildCrushInfo(cfg, nil)
56+ require.Contains(t, output, "[model]")
57+ require.Contains(t, output, "large = claude-sonnet-4-20250514 (anthropic)")
58+ require.Contains(t, output, "small = claude-haiku-3-20250307 (anthropic)")
59+}
60+
61+func TestCrushInfo_Providers(t *testing.T) {
62+ t.Parallel()
63+
64+ providers := csync.NewMap[string, config.ProviderConfig]()
65+ providers.Set("openai", config.ProviderConfig{Models: make([]catwalk.Model, 8)})
66+ providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
67+
68+ cfg := config.NewTestStore(&config.Config{Providers: providers})
69+ output := buildCrushInfo(cfg, nil)
70+ require.Contains(t, output, "[providers]")
71+ anthropicIdx := strings.Index(output, "anthropic = enabled")
72+ openaiIdx := strings.Index(output, "openai = enabled")
73+ require.Greater(t, anthropicIdx, -1)
74+ require.Greater(t, openaiIdx, -1)
75+ require.Less(t, anthropicIdx, openaiIdx, "anthropic should appear before openai")
76+ require.Contains(t, output, "anthropic = enabled (12 models)")
77+ require.Contains(t, output, "openai = enabled (8 models)")
78+}
79+
80+func TestCrushInfo_DisabledProvidersOmitted(t *testing.T) {
81+ t.Parallel()
82+
83+ providers := csync.NewMap[string, config.ProviderConfig]()
84+ providers.Set("openai", config.ProviderConfig{Disable: true, Models: make([]catwalk.Model, 8)})
85+ providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
86+
87+ cfg := config.NewTestStore(&config.Config{Providers: providers})
88+ output := buildCrushInfo(cfg, nil)
89+ require.Contains(t, output, "anthropic = enabled")
90+ require.NotContains(t, output, "openai")
91+}
92+
93+func TestCrushInfo_LSPStates(t *testing.T) {
94+ t.Parallel()
95+
96+ mgr := lsp.NewManager(config.NewTestStore(&config.Config{
97+ Providers: csync.NewMap[string, config.ProviderConfig](),
98+ }))
99+ readyClient := &lsp.Client{}
100+ readyClient.SetServerState(lsp.StateReady)
101+ mgr.Clients().Set("gopls", readyClient)
102+
103+ errorClient := &lsp.Client{}
104+ errorClient.SetServerState(lsp.StateError)
105+ mgr.Clients().Set("pyright", errorClient)
106+
107+ cfg := config.NewTestStore(&config.Config{Providers: csync.NewMap[string, config.ProviderConfig]()})
108+ output := buildCrushInfo(cfg, mgr)
109+ require.Contains(t, output, "[lsp]")
110+ require.Contains(t, output, "gopls = ready")
111+ require.Contains(t, output, "pyright = error")
112+ goplsIdx := strings.Index(output, "gopls = ready")
113+ pyrightIdx := strings.Index(output, "pyright = error")
114+ require.Less(t, goplsIdx, pyrightIdx, "gopls should appear before pyright")
115+}
116+
117+func TestCrushInfo_MCPStates(t *testing.T) {
118+ t.Parallel()
119+
120+ connectedAt := time.Date(2025, 1, 15, 15, 4, 5, 0, time.UTC)
121+ states := map[string]mcp.ClientInfo{
122+ "github": {
123+ Name: "github",
124+ State: mcp.StateConnected,
125+ Counts: mcp.Counts{Tools: 42, Resources: 7},
126+ ConnectedAt: connectedAt,
127+ },
128+ "filesystem": {
129+ Name: "filesystem",
130+ State: mcp.StateError,
131+ Error: errors.New("connection refused"),
132+ },
133+ }
134+
135+ cfg := config.NewTestStore(&config.Config{
136+ Providers: csync.NewMap[string, config.ProviderConfig](),
137+ })
138+
139+ var b strings.Builder
140+ writeMCP(&b, states, cfg)
141+ output := b.String()
142+ require.Contains(t, output, "[mcp]")
143+ require.Contains(t, output, "filesystem = error: connection refused")
144+ require.Contains(t, output, "github = connected (42 tools, 7 resources) since 15:04:05")
145+ filesystemIdx := strings.Index(output, "filesystem")
146+ githubIdx := strings.Index(output, "github")
147+ require.Less(t, filesystemIdx, githubIdx, "filesystem should appear before github")
148+}
149+
150+func TestCrushInfo_YoloMode(t *testing.T) {
151+ t.Parallel()
152+
153+ cfg := config.NewTestStore(&config.Config{
154+ Providers: csync.NewMap[string, config.ProviderConfig](),
155+ Permissions: &config.Permissions{},
156+ })
157+ cfg.Overrides().SkipPermissionRequests = true
158+
159+ output := buildCrushInfo(cfg, nil)
160+ require.Contains(t, output, "[permissions]")
161+ require.Contains(t, output, "mode = yolo")
162+}
163+
164+func TestCrushInfo_AllowedTools(t *testing.T) {
165+ t.Parallel()
166+
167+ cfg := config.NewTestStore(&config.Config{
168+ Providers: csync.NewMap[string, config.ProviderConfig](),
169+ Permissions: &config.Permissions{AllowedTools: []string{"edit:write", "bash"}},
170+ })
171+
172+ output := buildCrushInfo(cfg, nil)
173+ require.Contains(t, output, "[permissions]")
174+ require.Contains(t, output, "allowed_tools = bash, edit:write")
175+}
176+
177+func TestCrushInfo_DisabledTools(t *testing.T) {
178+ t.Parallel()
179+
180+ cfg := config.NewTestStore(&config.Config{
181+ Providers: csync.NewMap[string, config.ProviderConfig](),
182+ Options: &config.Options{DisabledTools: []string{"sourcegraph", "agentic_fetch"}},
183+ })
184+
185+ output := buildCrushInfo(cfg, nil)
186+ require.Contains(t, output, "[tools]")
187+ require.Contains(t, output, "disabled = agentic_fetch, sourcegraph")
188+}
189+
190+func TestCrushInfo_Options(t *testing.T) {
191+ t.Parallel()
192+
193+ cfg := config.NewTestStore(&config.Config{
194+ Providers: csync.NewMap[string, config.ProviderConfig](),
195+ Options: &config.Options{
196+ DataDirectory: "/Users/user/project/.crush",
197+ Debug: true,
198+ DisableAutoSummarize: true,
199+ },
200+ })
201+
202+ output := buildCrushInfo(cfg, nil)
203+ require.Contains(t, output, "[options]")
204+ require.Contains(t, output, "auto_lsp = true")
205+ require.Contains(t, output, "auto_summarize = false")
206+ require.Contains(t, output, "data_directory = /Users/user/project/.crush")
207+ require.Contains(t, output, "debug = true")
208+}
209+
210+func TestCrushInfo_AutoSummarizeInversion(t *testing.T) {
211+ t.Parallel()
212+
213+ cfgFalse := config.NewTestStore(&config.Config{
214+ Providers: csync.NewMap[string, config.ProviderConfig](),
215+ Options: &config.Options{DisableAutoSummarize: true},
216+ })
217+ outputFalse := buildCrushInfo(cfgFalse, nil)
218+ require.Contains(t, outputFalse, "auto_summarize = false")
219+
220+ cfgTrue := config.NewTestStore(&config.Config{
221+ Providers: csync.NewMap[string, config.ProviderConfig](),
222+ Options: &config.Options{DisableAutoSummarize: false},
223+ })
224+ outputTrue := buildCrushInfo(cfgTrue, nil)
225+ require.Contains(t, outputTrue, "auto_summarize = true")
226+}
227+
228+func TestCrushInfo_NoSecrets(t *testing.T) {
229+ t.Parallel()
230+
231+ providers := csync.NewMap[string, config.ProviderConfig]()
232+ providers.Set("openai", config.ProviderConfig{
233+ APIKey: "sk-super-secret-key-12345",
234+ Models: make([]catwalk.Model, 8),
235+ })
236+
237+ cfg := config.NewTestStore(&config.Config{Providers: providers})
238+ output := buildCrushInfo(cfg, nil)
239+ require.NotContains(t, output, "sk-super-secret-key-12345")
240+ require.NotContains(t, output, "secret")
241+ require.Contains(t, output, "openai = enabled (8 models)")
242+}
243+
244+func TestCrushInfo_DeterministicOrdering(t *testing.T) {
245+ t.Parallel()
246+
247+ providers := csync.NewMap[string, config.ProviderConfig]()
248+ providers.Set("zebra", config.ProviderConfig{Models: make([]catwalk.Model, 1)})
249+ providers.Set("alpha", config.ProviderConfig{Models: make([]catwalk.Model, 2)})
250+ providers.Set("middle", config.ProviderConfig{Models: make([]catwalk.Model, 3)})
251+
252+ states := map[string]mcp.ClientInfo{
253+ "z-mcp": {Name: "z-mcp", State: mcp.StateConnected, Counts: mcp.Counts{Tools: 1}},
254+ "a-mcp": {Name: "a-mcp", State: mcp.StateConnected, Counts: mcp.Counts{Tools: 2}},
255+ }
256+
257+ cfg := config.NewTestStore(&config.Config{
258+ Providers: providers,
259+ Options: &config.Options{DisabledTools: []string{"z-tool", "a-tool"}},
260+ Permissions: &config.Permissions{
261+ AllowedTools: []string{"z-perm", "a-perm"},
262+ },
263+ })
264+ cfg.Overrides().SkipPermissionRequests = true
265+
266+ // Test MCP ordering via writeMCP directly.
267+ var mcpBuf strings.Builder
268+ writeMCP(&mcpBuf, states, cfg)
269+ mcpOutput := mcpBuf.String()
270+ aMcpIdx := strings.Index(mcpOutput, "a-mcp = connected")
271+ zMcpIdx := strings.Index(mcpOutput, "z-mcp = connected")
272+ require.Less(t, aMcpIdx, zMcpIdx)
273+
274+ output := buildCrushInfo(cfg, nil)
275+
276+ alphaIdx := strings.Index(output, "alpha = enabled")
277+ middleIdx := strings.Index(output, "middle = enabled")
278+ zebraIdx := strings.Index(output, "zebra = enabled")
279+ require.Less(t, alphaIdx, middleIdx)
280+ require.Less(t, middleIdx, zebraIdx)
281+
282+ require.Contains(t, output, "disabled = a-tool, z-tool")
283+ require.Contains(t, output, "allowed_tools = a-perm, z-perm")
284+}
285+
286+func TestCrushInfo_EmptySectionsOmitted(t *testing.T) {
287+ t.Parallel()
288+
289+ cfg := config.NewTestStore(&config.Config{
290+ Providers: csync.NewMap[string, config.ProviderConfig](),
291+ Permissions: &config.Permissions{},
292+ Options: &config.Options{},
293+ })
294+
295+ output := buildCrushInfo(cfg, nil)
296+ require.NotContains(t, output, "[tools]")
297+ require.NotContains(t, output, "[permissions]")
298+ require.NotContains(t, output, "[lsp]")
299+ require.NotContains(t, output, "[mcp]")
300+}