feat(tools): add crush_info tool implementation files

Christian Rocha created

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>

Change summary

internal/agent/tools/crush_info.go      | 334 +++++++++++++++++++++++++++
internal/agent/tools/crush_info.md      |  16 +
internal/agent/tools/crush_info_test.go | 300 ++++++++++++++++++++++++
3 files changed, 650 insertions(+)

Detailed changes

internal/agent/tools/crush_info.go 🔗

@@ -0,0 +1,334 @@
+package tools
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+	"slices"
+	"strings"
+
+	"charm.land/fantasy"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
+	"github.com/charmbracelet/crush/internal/config"
+	"github.com/charmbracelet/crush/internal/lsp"
+)
+
+const CrushInfoToolName = "crush_info"
+
+//go:embed crush_info.md
+var crushInfoDescription []byte
+
+type CrushInfoParams struct{}
+
+func NewCrushInfoTool(
+	cfg *config.ConfigStore,
+	lspManager *lsp.Manager,
+) fantasy.AgentTool {
+	return fantasy.NewAgentTool(
+		CrushInfoToolName,
+		string(crushInfoDescription),
+		func(ctx context.Context, _ CrushInfoParams, _ fantasy.ToolCall) (fantasy.ToolResponse, error) {
+			return fantasy.NewTextResponse(buildCrushInfo(cfg, lspManager)), nil
+		})
+}
+
+func buildCrushInfo(cfg *config.ConfigStore, lspManager *lsp.Manager) string {
+	var b strings.Builder
+
+	writeConfigFiles(&b, cfg)
+	writeModels(&b, cfg)
+	writeProviders(&b, cfg)
+	writeLSP(&b, lspManager, cfg)
+	writeMCP(&b, mcp.GetStates(), cfg)
+	writePermissions(&b, cfg)
+	writeDisabledTools(&b, cfg)
+	writeOptions(&b, cfg)
+
+	return b.String()
+}
+
+func writeConfigFiles(b *strings.Builder, cfg *config.ConfigStore) {
+	b.WriteString("[config_files]\n")
+	paths := cfg.LoadedPaths()
+	for _, p := range paths {
+		b.WriteString(p + "\n")
+	}
+	b.WriteString("\n")
+}
+
+func writeModels(b *strings.Builder, cfg *config.ConfigStore) {
+	c := cfg.Config()
+	if len(c.Models) == 0 {
+		return
+	}
+	b.WriteString("[model]\n")
+	for _, typ := range []config.SelectedModelType{config.SelectedModelTypeLarge, config.SelectedModelTypeSmall} {
+		m, ok := c.Models[typ]
+		if !ok {
+			continue
+		}
+		fmt.Fprintf(b, "%s = %s (%s)\n", typ, m.Model, m.Provider)
+	}
+	b.WriteString("\n")
+}
+
+func writeProviders(b *strings.Builder, cfg *config.ConfigStore) {
+	c := cfg.Config()
+	type pv struct {
+		name  string
+		count int
+	}
+	var providers []pv
+	for name, pc := range c.Providers.Seq2() {
+		if pc.Disable {
+			continue
+		}
+		providers = append(providers, pv{name: name, count: len(pc.Models)})
+	}
+	if len(providers) == 0 {
+		return
+	}
+	slices.SortFunc(providers, func(a, b pv) int { return strings.Compare(a.name, b.name) })
+	b.WriteString("[providers]\n")
+	for _, p := range providers {
+		fmt.Fprintf(b, "%s = enabled (%d models)\n", p.name, p.count)
+	}
+	b.WriteString("\n")
+}
+
+func writeLSP(b *strings.Builder, lspManager *lsp.Manager, cfg *config.ConfigStore) {
+	// Write runtime LSP clients
+	if lspManager != nil && lspManager.Clients().Len() > 0 {
+		type entry struct {
+			name      string
+			state     lsp.ServerState
+			fileTypes []string
+		}
+		var entries []entry
+		for name, client := range lspManager.Clients().Seq2() {
+			entries = append(entries, entry{
+				name:      name,
+				state:     client.GetServerState(),
+				fileTypes: client.FileTypes(),
+			})
+		}
+		if len(entries) > 0 {
+			slices.SortFunc(entries, func(a, b entry) int { return strings.Compare(a.name, b.name) })
+			b.WriteString("[lsp]\n")
+			for _, e := range entries {
+				stateStr := lspStateString(e.state)
+				if len(e.fileTypes) > 0 {
+					sorted := slices.Clone(e.fileTypes)
+					slices.Sort(sorted)
+					fmt.Fprintf(b, "%s = %s (%s)\n", e.name, stateStr, strings.Join(sorted, ", "))
+				} else {
+					fmt.Fprintf(b, "%s = %s\n", e.name, stateStr)
+				}
+			}
+			b.WriteString("\n")
+		}
+	}
+
+	// Write configured but not running LSP servers
+	c := cfg.Config()
+	if len(c.LSP) > 0 {
+		runtimeNames := make(map[string]bool)
+		if lspManager != nil {
+			for name := range lspManager.Clients().Seq2() {
+				runtimeNames[name] = true
+			}
+		}
+
+		type configuredEntry struct {
+			name   string
+			status string
+		}
+		var entries []configuredEntry
+		for name, lspCfg := range c.LSP {
+			// Skip if already in runtime
+			if runtimeNames[name] {
+				continue
+			}
+			status := "not_started"
+			if lspCfg.Disabled {
+				status = "disabled"
+			}
+			entries = append(entries, configuredEntry{name: name, status: status})
+		}
+
+		if len(entries) > 0 {
+			slices.SortFunc(entries, func(a, b configuredEntry) int { return strings.Compare(a.name, b.name) })
+			b.WriteString("[lsp_configured]\n")
+			for _, e := range entries {
+				fmt.Fprintf(b, "%s = %s\n", e.name, e.status)
+			}
+			b.WriteString("\n")
+		}
+	}
+}
+
+func writeMCP(b *strings.Builder, states map[string]mcp.ClientInfo, cfg *config.ConfigStore) {
+	// Write runtime MCP states
+	if len(states) > 0 {
+		type entry struct {
+			name        string
+			state       mcp.State
+			err         error
+			tools       int
+			resources   int
+			connectedAt string
+		}
+		var entries []entry
+		for name, info := range states {
+			e := entry{
+				name:  name,
+				state: info.State,
+				err:   info.Error,
+			}
+			if info.State == mcp.StateConnected {
+				e.tools = info.Counts.Tools
+				e.resources = info.Counts.Resources
+				if !info.ConnectedAt.IsZero() {
+					e.connectedAt = info.ConnectedAt.Format("15:04:05")
+				}
+			}
+			entries = append(entries, e)
+		}
+		slices.SortFunc(entries, func(a, b entry) int { return strings.Compare(a.name, b.name) })
+		b.WriteString("[mcp]\n")
+		for _, e := range entries {
+			switch e.state {
+			case mcp.StateConnected:
+				if e.connectedAt != "" {
+					fmt.Fprintf(b, "%s = connected (%d tools, %d resources) since %s\n", e.name, e.tools, e.resources, e.connectedAt)
+				} else {
+					fmt.Fprintf(b, "%s = connected (%d tools, %d resources)\n", e.name, e.tools, e.resources)
+				}
+			case mcp.StateError:
+				if e.err != nil {
+					fmt.Fprintf(b, "%s = error: %s\n", e.name, e.err.Error())
+				} else {
+					fmt.Fprintf(b, "%s = error\n", e.name)
+				}
+			default:
+				fmt.Fprintf(b, "%s = %s\n", e.name, e.state)
+			}
+		}
+		b.WriteString("\n")
+	}
+
+	// Write configured but not running MCP servers
+	c := cfg.Config()
+	if len(c.MCP) > 0 {
+		runtimeNames := make(map[string]bool)
+		for name := range states {
+			runtimeNames[name] = true
+		}
+
+		type configuredEntry struct {
+			name   string
+			status string
+		}
+		var entries []configuredEntry
+		for name, mcpCfg := range c.MCP {
+			// Skip if already in runtime
+			if runtimeNames[name] {
+				continue
+			}
+			status := "not_started"
+			if mcpCfg.Disabled {
+				status = "disabled"
+			}
+			entries = append(entries, configuredEntry{name: name, status: status})
+		}
+
+		if len(entries) > 0 {
+			slices.SortFunc(entries, func(a, b configuredEntry) int { return strings.Compare(a.name, b.name) })
+			b.WriteString("[mcp_configured]\n")
+			for _, e := range entries {
+				fmt.Fprintf(b, "%s = %s\n", e.name, e.status)
+			}
+			b.WriteString("\n")
+		}
+	}
+}
+
+func writePermissions(b *strings.Builder, cfg *config.ConfigStore) {
+	c := cfg.Config()
+	overrides := cfg.Overrides()
+
+	if c.Permissions == nil {
+		if !overrides.SkipPermissionRequests {
+			return
+		}
+	} else if !overrides.SkipPermissionRequests && len(c.Permissions.AllowedTools) == 0 {
+		return
+	}
+	b.WriteString("[permissions]\n")
+	if overrides.SkipPermissionRequests {
+		b.WriteString("mode = yolo\n")
+	}
+	if c.Permissions != nil && len(c.Permissions.AllowedTools) > 0 {
+		sorted := slices.Clone(c.Permissions.AllowedTools)
+		slices.Sort(sorted)
+		fmt.Fprintf(b, "allowed_tools = %s\n", strings.Join(sorted, ", "))
+	}
+	b.WriteString("\n")
+}
+
+func writeDisabledTools(b *strings.Builder, cfg *config.ConfigStore) {
+	c := cfg.Config()
+	if c.Options == nil || len(c.Options.DisabledTools) == 0 {
+		return
+	}
+	sorted := slices.Clone(c.Options.DisabledTools)
+	slices.Sort(sorted)
+	b.WriteString("[tools]\n")
+	fmt.Fprintf(b, "disabled = %s\n", strings.Join(sorted, ", "))
+	b.WriteString("\n")
+}
+
+func writeOptions(b *strings.Builder, cfg *config.ConfigStore) {
+	c := cfg.Config()
+	if c.Options == nil {
+		return
+	}
+	type kv struct {
+		key   string
+		value string
+	}
+	var opts []kv
+
+	opts = append(opts, kv{"data_directory", c.Options.DataDirectory})
+	opts = append(opts, kv{"debug", fmt.Sprintf("%v", c.Options.Debug)})
+	autoLSP := c.Options.AutoLSP == nil || *c.Options.AutoLSP
+	opts = append(opts, kv{"auto_lsp", fmt.Sprintf("%v", autoLSP)})
+	autoSummarize := !c.Options.DisableAutoSummarize
+	opts = append(opts, kv{"auto_summarize", fmt.Sprintf("%v", autoSummarize)})
+
+	slices.SortFunc(opts, func(a, b kv) int { return strings.Compare(a.key, b.key) })
+	b.WriteString("[options]\n")
+	for _, o := range opts {
+		fmt.Fprintf(b, "%s = %s\n", o.key, o.value)
+	}
+	b.WriteString("\n")
+}
+
+func lspStateString(state lsp.ServerState) string {
+	switch state {
+	case lsp.StateUnstarted:
+		return "unstarted"
+	case lsp.StateStarting:
+		return "starting"
+	case lsp.StateReady:
+		return "ready"
+	case lsp.StateError:
+		return "error"
+	case lsp.StateStopped:
+		return "stopped"
+	case lsp.StateDisabled:
+		return "disabled"
+	default:
+		return "unknown"
+	}
+}

internal/agent/tools/crush_info.md 🔗

@@ -0,0 +1,16 @@
+Get information about Crush's current runtime configuration and service
+state.
+
+<usage>
+- Shows active model and provider, LSP/MCP server status, permissions mode,
+  disabled tools, and key options
+- Use when diagnosing why something isn't working (missing diagnostics,
+  provider errors, MCP disconnections)
+- No parameters needed — always returns the full current state
+</usage>
+
+<tips>
+- Check [lsp] and [mcp] sections for service health
+- Check [providers] to see which providers are enabled and available
+- Pair with the crush-config skill to fix configuration issues
+</tips>

internal/agent/tools/crush_info_test.go 🔗

@@ -0,0 +1,300 @@
+package tools
+
+import (
+	"errors"
+	"strings"
+	"testing"
+	"time"
+
+	"charm.land/catwalk/pkg/catwalk"
+	"github.com/charmbracelet/crush/internal/agent/tools/mcp"
+	"github.com/charmbracelet/crush/internal/config"
+	"github.com/charmbracelet/crush/internal/csync"
+	"github.com/charmbracelet/crush/internal/lsp"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCrushInfo_MinimalConfig(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+	})
+	output := buildCrushInfo(cfg, nil)
+	require.NotContains(t, output, "[providers]")
+	require.NotContains(t, output, "[lsp]")
+	require.NotContains(t, output, "[mcp]")
+	require.NotContains(t, output, "[permissions]")
+	require.NotContains(t, output, "[tools]")
+}
+
+func TestCrushInfo_ConfigFiles(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(
+		&config.Config{Providers: csync.NewMap[string, config.ProviderConfig]()},
+		"/home/user/.config/crush/crush.json",
+		"/project/.crush/crush.json",
+	)
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "[config_files]")
+	require.Contains(t, output, "/home/user/.config/crush/crush.json")
+	require.Contains(t, output, "/project/.crush/crush.json")
+}
+
+func TestCrushInfo_Models(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(&config.Config{
+		Models: map[config.SelectedModelType]config.SelectedModel{
+			config.SelectedModelTypeLarge: {Model: "claude-sonnet-4-20250514", Provider: "anthropic"},
+			config.SelectedModelTypeSmall: {Model: "claude-haiku-3-20250307", Provider: "anthropic"},
+		},
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+	})
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "[model]")
+	require.Contains(t, output, "large = claude-sonnet-4-20250514 (anthropic)")
+	require.Contains(t, output, "small = claude-haiku-3-20250307 (anthropic)")
+}
+
+func TestCrushInfo_Providers(t *testing.T) {
+	t.Parallel()
+
+	providers := csync.NewMap[string, config.ProviderConfig]()
+	providers.Set("openai", config.ProviderConfig{Models: make([]catwalk.Model, 8)})
+	providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
+
+	cfg := config.NewTestStore(&config.Config{Providers: providers})
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "[providers]")
+	anthropicIdx := strings.Index(output, "anthropic = enabled")
+	openaiIdx := strings.Index(output, "openai = enabled")
+	require.Greater(t, anthropicIdx, -1)
+	require.Greater(t, openaiIdx, -1)
+	require.Less(t, anthropicIdx, openaiIdx, "anthropic should appear before openai")
+	require.Contains(t, output, "anthropic = enabled (12 models)")
+	require.Contains(t, output, "openai = enabled (8 models)")
+}
+
+func TestCrushInfo_DisabledProvidersOmitted(t *testing.T) {
+	t.Parallel()
+
+	providers := csync.NewMap[string, config.ProviderConfig]()
+	providers.Set("openai", config.ProviderConfig{Disable: true, Models: make([]catwalk.Model, 8)})
+	providers.Set("anthropic", config.ProviderConfig{Models: make([]catwalk.Model, 12)})
+
+	cfg := config.NewTestStore(&config.Config{Providers: providers})
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "anthropic = enabled")
+	require.NotContains(t, output, "openai")
+}
+
+func TestCrushInfo_LSPStates(t *testing.T) {
+	t.Parallel()
+
+	mgr := lsp.NewManager(config.NewTestStore(&config.Config{
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+	}))
+	readyClient := &lsp.Client{}
+	readyClient.SetServerState(lsp.StateReady)
+	mgr.Clients().Set("gopls", readyClient)
+
+	errorClient := &lsp.Client{}
+	errorClient.SetServerState(lsp.StateError)
+	mgr.Clients().Set("pyright", errorClient)
+
+	cfg := config.NewTestStore(&config.Config{Providers: csync.NewMap[string, config.ProviderConfig]()})
+	output := buildCrushInfo(cfg, mgr)
+	require.Contains(t, output, "[lsp]")
+	require.Contains(t, output, "gopls = ready")
+	require.Contains(t, output, "pyright = error")
+	goplsIdx := strings.Index(output, "gopls = ready")
+	pyrightIdx := strings.Index(output, "pyright = error")
+	require.Less(t, goplsIdx, pyrightIdx, "gopls should appear before pyright")
+}
+
+func TestCrushInfo_MCPStates(t *testing.T) {
+	t.Parallel()
+
+	connectedAt := time.Date(2025, 1, 15, 15, 4, 5, 0, time.UTC)
+	states := map[string]mcp.ClientInfo{
+		"github": {
+			Name:        "github",
+			State:       mcp.StateConnected,
+			Counts:      mcp.Counts{Tools: 42, Resources: 7},
+			ConnectedAt: connectedAt,
+		},
+		"filesystem": {
+			Name:  "filesystem",
+			State: mcp.StateError,
+			Error: errors.New("connection refused"),
+		},
+	}
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+	})
+
+	var b strings.Builder
+	writeMCP(&b, states, cfg)
+	output := b.String()
+	require.Contains(t, output, "[mcp]")
+	require.Contains(t, output, "filesystem = error: connection refused")
+	require.Contains(t, output, "github = connected (42 tools, 7 resources) since 15:04:05")
+	filesystemIdx := strings.Index(output, "filesystem")
+	githubIdx := strings.Index(output, "github")
+	require.Less(t, filesystemIdx, githubIdx, "filesystem should appear before github")
+}
+
+func TestCrushInfo_YoloMode(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers:   csync.NewMap[string, config.ProviderConfig](),
+		Permissions: &config.Permissions{},
+	})
+	cfg.Overrides().SkipPermissionRequests = true
+
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "[permissions]")
+	require.Contains(t, output, "mode = yolo")
+}
+
+func TestCrushInfo_AllowedTools(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers:   csync.NewMap[string, config.ProviderConfig](),
+		Permissions: &config.Permissions{AllowedTools: []string{"edit:write", "bash"}},
+	})
+
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "[permissions]")
+	require.Contains(t, output, "allowed_tools = bash, edit:write")
+}
+
+func TestCrushInfo_DisabledTools(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+		Options:   &config.Options{DisabledTools: []string{"sourcegraph", "agentic_fetch"}},
+	})
+
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "[tools]")
+	require.Contains(t, output, "disabled = agentic_fetch, sourcegraph")
+}
+
+func TestCrushInfo_Options(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+		Options: &config.Options{
+			DataDirectory:        "/Users/user/project/.crush",
+			Debug:                true,
+			DisableAutoSummarize: true,
+		},
+	})
+
+	output := buildCrushInfo(cfg, nil)
+	require.Contains(t, output, "[options]")
+	require.Contains(t, output, "auto_lsp = true")
+	require.Contains(t, output, "auto_summarize = false")
+	require.Contains(t, output, "data_directory = /Users/user/project/.crush")
+	require.Contains(t, output, "debug = true")
+}
+
+func TestCrushInfo_AutoSummarizeInversion(t *testing.T) {
+	t.Parallel()
+
+	cfgFalse := config.NewTestStore(&config.Config{
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+		Options:   &config.Options{DisableAutoSummarize: true},
+	})
+	outputFalse := buildCrushInfo(cfgFalse, nil)
+	require.Contains(t, outputFalse, "auto_summarize = false")
+
+	cfgTrue := config.NewTestStore(&config.Config{
+		Providers: csync.NewMap[string, config.ProviderConfig](),
+		Options:   &config.Options{DisableAutoSummarize: false},
+	})
+	outputTrue := buildCrushInfo(cfgTrue, nil)
+	require.Contains(t, outputTrue, "auto_summarize = true")
+}
+
+func TestCrushInfo_NoSecrets(t *testing.T) {
+	t.Parallel()
+
+	providers := csync.NewMap[string, config.ProviderConfig]()
+	providers.Set("openai", config.ProviderConfig{
+		APIKey: "sk-super-secret-key-12345",
+		Models: make([]catwalk.Model, 8),
+	})
+
+	cfg := config.NewTestStore(&config.Config{Providers: providers})
+	output := buildCrushInfo(cfg, nil)
+	require.NotContains(t, output, "sk-super-secret-key-12345")
+	require.NotContains(t, output, "secret")
+	require.Contains(t, output, "openai = enabled (8 models)")
+}
+
+func TestCrushInfo_DeterministicOrdering(t *testing.T) {
+	t.Parallel()
+
+	providers := csync.NewMap[string, config.ProviderConfig]()
+	providers.Set("zebra", config.ProviderConfig{Models: make([]catwalk.Model, 1)})
+	providers.Set("alpha", config.ProviderConfig{Models: make([]catwalk.Model, 2)})
+	providers.Set("middle", config.ProviderConfig{Models: make([]catwalk.Model, 3)})
+
+	states := map[string]mcp.ClientInfo{
+		"z-mcp": {Name: "z-mcp", State: mcp.StateConnected, Counts: mcp.Counts{Tools: 1}},
+		"a-mcp": {Name: "a-mcp", State: mcp.StateConnected, Counts: mcp.Counts{Tools: 2}},
+	}
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers: providers,
+		Options:   &config.Options{DisabledTools: []string{"z-tool", "a-tool"}},
+		Permissions: &config.Permissions{
+			AllowedTools: []string{"z-perm", "a-perm"},
+		},
+	})
+	cfg.Overrides().SkipPermissionRequests = true
+
+	// Test MCP ordering via writeMCP directly.
+	var mcpBuf strings.Builder
+	writeMCP(&mcpBuf, states, cfg)
+	mcpOutput := mcpBuf.String()
+	aMcpIdx := strings.Index(mcpOutput, "a-mcp = connected")
+	zMcpIdx := strings.Index(mcpOutput, "z-mcp = connected")
+	require.Less(t, aMcpIdx, zMcpIdx)
+
+	output := buildCrushInfo(cfg, nil)
+
+	alphaIdx := strings.Index(output, "alpha = enabled")
+	middleIdx := strings.Index(output, "middle = enabled")
+	zebraIdx := strings.Index(output, "zebra = enabled")
+	require.Less(t, alphaIdx, middleIdx)
+	require.Less(t, middleIdx, zebraIdx)
+
+	require.Contains(t, output, "disabled = a-tool, z-tool")
+	require.Contains(t, output, "allowed_tools = a-perm, z-perm")
+}
+
+func TestCrushInfo_EmptySectionsOmitted(t *testing.T) {
+	t.Parallel()
+
+	cfg := config.NewTestStore(&config.Config{
+		Providers:   csync.NewMap[string, config.ProviderConfig](),
+		Permissions: &config.Permissions{},
+		Options:     &config.Options{},
+	})
+
+	output := buildCrushInfo(cfg, nil)
+	require.NotContains(t, output, "[tools]")
+	require.NotContains(t, output, "[permissions]")
+	require.NotContains(t, output, "[lsp]")
+	require.NotContains(t, output, "[mcp]")
+}