Skip to content
Merged
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
94 changes: 94 additions & 0 deletions cmd/config/volumes/clear.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package volumes

import (
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/mozilla-ai/mcpd/internal/cmd"
cmdopts "github.com/mozilla-ai/mcpd/internal/cmd/options"
"github.com/mozilla-ai/mcpd/internal/context"
"github.com/mozilla-ai/mcpd/internal/flags"
)

// clearCmd handles clearing all volume mappings from an MCP server.
type clearCmd struct {
*cmd.BaseCmd
force bool
ctxLoader context.Loader
}

// NewClearCmd creates a new clear command for volume configuration.
func NewClearCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
opts, err := cmdopts.NewOptions(opt...)
if err != nil {
return nil, err
}

c := &clearCmd{
BaseCmd: baseCmd,
ctxLoader: opts.ContextLoader,
}

cobraCmd := &cobra.Command{
Use: "clear <server-name>",
Short: "Clear all volume mappings for a server",
Long: "Clears all volume mappings for the specified server from the " +
"runtime context configuration file (e.g. " + flags.RuntimeFile + ").\n\n" +
"This is a destructive operation and requires the --force flag.\n\n" +
"Examples:\n" +
" # Clear all volume mappings\n" +
" mcpd config volumes clear filesystem --force",
RunE: c.run,
Args: cobra.ExactArgs(1),
}

cobraCmd.Flags().BoolVar(
&c.force,
"force",
false,
"Force clearing of all volume mappings for the specified server without confirmation",
)

return cobraCmd, nil
}

// run executes the clear command, removing all volume mappings from the server config.
func (c *clearCmd) run(cobraCmd *cobra.Command, args []string) error {
serverName := strings.TrimSpace(args[0])
if serverName == "" {
return fmt.Errorf("server-name is required")
}

if !c.force {
return fmt.Errorf("this is a destructive operation. To clear all volumes for '%s', "+
"please re-run the command with the --force flag", serverName)
}

cfg, err := c.ctxLoader.Load(flags.RuntimeFile)
if err != nil {
return fmt.Errorf("failed to load execution context config: %w", err)
}

server, ok := cfg.Get(serverName)
if !ok {
return fmt.Errorf("server '%s' not found in configuration", serverName)
}

server = withVolumes(server, context.VolumeExecutionContext{})

res, err := cfg.Upsert(server)
if err != nil {
return fmt.Errorf("error clearing volumes for server '%s': %w", serverName, err)
}

if _, err := fmt.Fprintf(
cobraCmd.OutOrStdout(),
"✓ Volumes cleared for server '%s' (operation: %s)\n", serverName, string(res),
); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}

return nil
}
181 changes: 181 additions & 0 deletions cmd/config/volumes/clear_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package volumes

import (
"bytes"
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/mozilla-ai/mcpd/internal/cmd"
cmdopts "github.com/mozilla-ai/mcpd/internal/cmd/options"
"github.com/mozilla-ai/mcpd/internal/context"
)

func TestNewClearCmd(t *testing.T) {
t.Parallel()

base := &cmd.BaseCmd{}
c, err := NewClearCmd(base)
require.NoError(t, err)
require.NotNil(t, c)

require.True(t, strings.HasPrefix(c.Use, "clear "))
require.Contains(t, c.Short, "Clear")
require.NotNil(t, c.RunE)

forceFlag := c.Flags().Lookup("force")
require.NotNil(t, forceFlag)
require.Equal(t, "false", forceFlag.DefValue)
}

func TestClearCmd_run(t *testing.T) {
t.Parallel()

tests := []struct {
name string
serverName string
force bool
existingServers map[string]context.ServerExecutionContext
expectedOutput string
expectedError string
expectedVolumes context.VolumeExecutionContext
}{
{
name: "clear all volumes with force",
serverName: "filesystem",
force: true,
existingServers: map[string]context.ServerExecutionContext{
"filesystem": {
Name: "filesystem",
Volumes: context.VolumeExecutionContext{
"workspace": "/Users/foo/repos",
"gdrive": "/mcp/gdrive",
},
RawVolumes: context.VolumeExecutionContext{
"workspace": "/Users/foo/repos",
"gdrive": "/mcp/gdrive",
},
},
},
expectedOutput: "✓ Volumes cleared for server 'filesystem' (operation: deleted)",
expectedVolumes: context.VolumeExecutionContext{},
},
{
name: "clear without force returns error",
serverName: "filesystem",
force: false,
existingServers: map[string]context.ServerExecutionContext{
"filesystem": {
Name: "filesystem",
Volumes: context.VolumeExecutionContext{"workspace": "/path"},
RawVolumes: context.VolumeExecutionContext{"workspace": "/path"},
},
},
expectedError: "this is a destructive operation. To clear all volumes for 'filesystem', " +
"please re-run the command with the --force flag",
},
{
name: "server not found",
serverName: "nonexistent",
force: true,
existingServers: map[string]context.ServerExecutionContext{},
expectedError: "server 'nonexistent' not found in configuration",
},
{
name: "clear server with no volumes is noop",
serverName: "filesystem",
force: true,
existingServers: map[string]context.ServerExecutionContext{
"filesystem": {
Name: "filesystem",
},
},
expectedOutput: "✓ Volumes cleared for server 'filesystem' (operation: noop)",
expectedVolumes: context.VolumeExecutionContext{},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

modifier := &mockModifier{
servers: tc.existingServers,
}
loader := &mockLoader{modifier: modifier}

base := &cmd.BaseCmd{}
clearCmd, err := NewClearCmd(base, cmdopts.WithContextLoader(loader))
require.NoError(t, err)

var output bytes.Buffer
clearCmd.SetOut(&output)
clearCmd.SetErr(&output)

args := []string{tc.serverName}
if tc.force {
args = append(args, "--force")
}
clearCmd.SetArgs(args)

err = clearCmd.Execute()

if tc.expectedError != "" {
require.EqualError(t, err, tc.expectedError)
return
}

require.NoError(t, err)

actualOutput := strings.TrimSpace(output.String())
require.Equal(t, tc.expectedOutput, actualOutput)

if tc.expectedVolumes != nil {
require.Equal(t, tc.expectedVolumes, modifier.lastUpsert.Volumes)
require.Equal(t, tc.expectedVolumes, modifier.lastUpsert.RawVolumes)
}
})
}
}

func TestClearCmd_LoaderError(t *testing.T) {
t.Parallel()

loader := &mockLoader{
loadError: fmt.Errorf("failed to load"),
}

base := &cmd.BaseCmd{}
clearCmd, err := NewClearCmd(base, cmdopts.WithContextLoader(loader))
require.NoError(t, err)

clearCmd.SetArgs([]string{"server", "--force"})
err = clearCmd.Execute()
require.EqualError(t, err, "failed to load execution context config: failed to load")
}

func TestClearCmd_UpsertError(t *testing.T) {
t.Parallel()

modifier := &mockModifier{
servers: map[string]context.ServerExecutionContext{
"server": {
Name: "server",
Volumes: context.VolumeExecutionContext{"workspace": "/path"},
RawVolumes: context.VolumeExecutionContext{"workspace": "/path"},
},
},
upsertError: fmt.Errorf("upsert failed"),
}
loader := &mockLoader{modifier: modifier}

base := &cmd.BaseCmd{}
clearCmd, err := NewClearCmd(base, cmdopts.WithContextLoader(loader))
require.NoError(t, err)

clearCmd.SetArgs([]string{"server", "--force"})
err = clearCmd.Execute()
require.EqualError(t, err, "error clearing volumes for server 'server': upsert failed")
}
1 change: 1 addition & 0 deletions cmd/config/volumes/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func NewCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, err

// Sub-commands for: mcpd config volumes
fns := []func(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error){
NewClearCmd, // clear
NewListCmd, // list
NewRemoveCmd, // remove
NewSetCmd, // set
Expand Down