Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ module github.com/charmbracelet/crush

go 1.25.0

replace charm.land/fantasy => ../../fantasy

require (
charm.land/bubbles/v2 v2.0.0-rc.1
charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251126220703-2a0096c500a7
Expand Down Expand Up @@ -70,7 +72,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/RealAlexandreAI/json-repair v0.0.14 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.40.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
Expand Down Expand Up @@ -169,7 +171,7 @@ require (
golang.org/x/term v0.37.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.239.0 // indirect
google.golang.org/genai v1.34.0 // indirect
google.golang.org/genai v1.36.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.74.2 // indirect
google.golang.org/protobuf v1.36.10 // indirect
Expand Down
10 changes: 4 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM
charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251126220703-2a0096c500a7 h1:3qsObfEm0WuACFhe3MSTPX8QByjVcjWkZDO4o2VWFpc=
charm.land/bubbletea/v2 v2.0.0-rc.2.0.20251126220703-2a0096c500a7/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
charm.land/fantasy v0.3.2 h1:yHTsSZ25LcICMRw3xzdz3OkaZtDQch+B5ljJo17HxgU=
charm.land/fantasy v0.3.2/go.mod h1:sV8Ns/JTJHOaYOHPgVRDugMheAyxsW/nmdpVGrycYEk=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca h1:6bVc8OFotCS4sS7HKqxTudP7yn8Y0ODR6df2pdlY/+s=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251119143523-0334bb4562ca/go.mod h1:XSJjv7DaH4zd1Y27kZis295RkEj9OFR9zh2WffQQsKQ=
charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q=
Expand Down Expand Up @@ -44,8 +42,8 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
Expand Down Expand Up @@ -457,8 +455,8 @@ golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/genai v1.34.0 h1:lPRJRO+HqRX1SwFo1Xb/22nZ5MBEPUbXDl61OoDxlbY=
google.golang.org/genai v1.34.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
google.golang.org/genai v1.36.0 h1:sJCIjqTAmwrtAIaemtTiKkg2TO1RxnYEusTmEQ3nGxM=
google.golang.org/genai v1.36.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
Expand Down
3 changes: 3 additions & 0 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
prepared.Messages[i].ProviderOptions = nil
}

// Use latest tools (updated by SetTools when MCP tools change).
prepared.Tools = a.tools

queuedCalls, _ := a.messageQueue.Get(call.SessionID)
a.messageQueue.Del(call.SessionID)
for _, queued := range queuedCalls {
Expand Down
16 changes: 16 additions & 0 deletions internal/agent/coordinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Coordinator interface {
Summarize(context.Context, string) error
Model() Model
UpdateModels(ctx context.Context) error
RefreshTools(ctx context.Context) error
}

type coordinator struct {
Expand Down Expand Up @@ -742,6 +743,21 @@ func (c *coordinator) UpdateModels(ctx context.Context) error {
return nil
}

func (c *coordinator) RefreshTools(ctx context.Context) error {
agentCfg, ok := c.cfg.Agents[config.AgentCoder]
if !ok {
return errors.New("coder agent not configured")
}

tools, err := c.buildTools(ctx, agentCfg)
if err != nil {
return err
}
c.currentAgent.SetTools(tools)
slog.Debug("refreshed agent tools", "count", len(tools))
return nil
}

func (c *coordinator) QueuedPrompts(sessionID string) int {
return c.currentAgent.QueuedPrompts(sessionID)
}
Expand Down
74 changes: 74 additions & 0 deletions internal/agent/tools/mcp/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,80 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config
wg.Wait()
}

// InitializeSingle initializes a single MCP client by name.
func InitializeSingle(ctx context.Context, name string, cfg *config.Config) error {
m, exists := cfg.MCP[name]
if !exists {
return fmt.Errorf("mcp '%s' not found in configuration", name)
}

if m.Disabled {
updateState(name, StateDisabled, nil, nil, Counts{})
slog.Debug("skipping disabled mcp", "name", name)
return nil
}

// Set initial starting state.
updateState(name, StateStarting, nil, nil, Counts{})

// createSession handles its own timeout internally.
session, err := createSession(ctx, name, m, cfg.Resolver())
if err != nil {
return err
}

tools, err := getTools(ctx, session)
if err != nil {
slog.Error("error listing tools", "error", err)
updateState(name, StateError, err, nil, Counts{})
session.Close()
return err
}

prompts, err := getPrompts(ctx, session)
if err != nil {
slog.Error("error listing prompts", "error", err)
updateState(name, StateError, err, nil, Counts{})
session.Close()
return err
}

updateTools(name, tools)
updatePrompts(name, prompts)
sessions.Set(name, session)

updateState(name, StateConnected, nil, session, Counts{
Tools: len(tools),
Prompts: len(prompts),
})

return nil
}

// DisableSingle disables and closes a single MCP client by name.
func DisableSingle(name string) error {
session, ok := sessions.Get(name)
if ok {
if err := session.Close(); err != nil &&
!errors.Is(err, io.EOF) &&
!errors.Is(err, context.Canceled) &&
err.Error() != "signal: killed" {
slog.Warn("error closing mcp session", "name", name, "error", err)
}
sessions.Del(name)
}

// Clear tools and prompts for this MCP.
updateTools(name, nil)
updatePrompts(name, nil)

// Update state to disabled.
updateState(name, StateDisabled, nil, nil, Counts{})

slog.Info("Disabled mcp client", "name", name)
return nil
}

func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) {
sess, ok := sessions.Get(name)
if !ok {
Expand Down
75 changes: 75 additions & 0 deletions internal/config/docker_mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package config

import (
"context"
"fmt"
"os/exec"
"time"
)

// DockerMCPName is the name of the Docker MCP configuration.
const DockerMCPName = "crush_docker"

// IsDockerMCPAvailable checks if Docker MCP is available by running
// 'docker mcp version'.
func IsDockerMCPAvailable() bool {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, "docker", "mcp", "version")
err := cmd.Run()
return err == nil
}

// IsDockerMCPEnabled checks if Docker MCP is already configured.
func (c *Config) IsDockerMCPEnabled() bool {
if c.MCP == nil {
return false
}
_, exists := c.MCP[DockerMCPName]
return exists
}

// EnableDockerMCP adds Docker MCP configuration and persists it.
func (c *Config) EnableDockerMCP() error {
if !IsDockerMCPAvailable() {
return fmt.Errorf("docker mcp is not available, please ensure docker is installed and 'docker mcp version' succeeds")
}

mcpConfig := MCPConfig{
Type: MCPStdio,
Command: "docker",
Args: []string{"mcp", "gateway", "run"},
Disabled: false,
}

// Add to in-memory config.
if c.MCP == nil {
c.MCP = make(map[string]MCPConfig)
}
c.MCP[DockerMCPName] = mcpConfig

// Persist to config file.
if err := c.SetConfigField("mcp."+DockerMCPName, mcpConfig); err != nil {
return fmt.Errorf("failed to persist docker mcp configuration: %w", err)
}

return nil
}

// DisableDockerMCP removes Docker MCP configuration and persists the change.
func (c *Config) DisableDockerMCP() error {
if c.MCP == nil {
return nil
}

// Remove from in-memory config.
delete(c.MCP, DockerMCPName)

// Persist to config file by setting to null.
if err := c.SetConfigField("mcp", c.MCP); err != nil {
return fmt.Errorf("failed to persist docker mcp removal: %w", err)
}

return nil
}
Loading
Loading