ollama/cmd/mcp_cmd.go

899 lines
23 KiB
Go

package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
"time"
"github.com/spf13/cobra"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/format"
"github.com/ollama/ollama/progress"
"github.com/ollama/ollama/server"
"github.com/ollama/ollama/types/model"
)
// MCPConfigFile represents the global MCP configuration file structure.
type MCPConfigFile struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers"`
}
// MCPServerConfig represents a single MCP server configuration.
type MCPServerConfig struct {
Type string `json:"type,omitempty"`
Command string `json:"command"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
Disabled bool `json:"disabled,omitempty"`
}
// getMCPConfigPath returns the path to the global MCP config file.
func getMCPConfigPath() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".ollama", "mcp.json")
}
// loadMCPConfig loads the global MCP configuration file.
func loadMCPConfig() (*MCPConfigFile, error) {
configPath := getMCPConfigPath()
if configPath == "" {
return nil, fmt.Errorf("could not determine home directory")
}
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
// Return empty config if file doesn't exist
return &MCPConfigFile{
MCPServers: make(map[string]MCPServerConfig),
}, nil
}
return nil, fmt.Errorf("reading config: %w", err)
}
var config MCPConfigFile
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
if config.MCPServers == nil {
config.MCPServers = make(map[string]MCPServerConfig)
}
return &config, nil
}
// saveMCPConfig saves the global MCP configuration file.
func saveMCPConfig(config *MCPConfigFile) error {
configPath := getMCPConfigPath()
if configPath == "" {
return fmt.Errorf("could not determine home directory")
}
// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
return fmt.Errorf("creating config directory: %w", err)
}
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshaling config: %w", err)
}
if err := os.WriteFile(configPath, data, 0o644); err != nil {
return fmt.Errorf("writing config: %w", err)
}
return nil
}
// MCPAddHandler handles the mcp add command.
func MCPAddHandler(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: ollama mcp add NAME COMMAND [ARGS...]")
}
name := args[0]
command := args[1]
cmdArgs := args[2:]
// Load existing config
config, err := loadMCPConfig()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
// Check if already exists
if _, exists := config.MCPServers[name]; exists {
fmt.Fprintf(os.Stderr, "Warning: overwriting existing MCP server '%s'\n", name)
}
// Add the new server
config.MCPServers[name] = MCPServerConfig{
Type: "stdio",
Command: command,
Args: cmdArgs,
}
// Save config
if err := saveMCPConfig(config); err != nil {
return fmt.Errorf("saving config: %w", err)
}
configPath := getMCPConfigPath()
fmt.Fprintf(os.Stderr, "Added MCP server '%s' to %s\n", name, configPath)
fmt.Fprintf(os.Stderr, " Command: %s %s\n", command, strings.Join(cmdArgs, " "))
return nil
}
// MCPRemoveGlobalHandler handles removing an MCP from global config.
func MCPRemoveGlobalHandler(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: ollama mcp remove-global NAME [NAME...]")
}
config, err := loadMCPConfig()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
for _, name := range args {
if _, exists := config.MCPServers[name]; !exists {
fmt.Fprintf(os.Stderr, "MCP server '%s' not found in global config\n", name)
continue
}
delete(config.MCPServers, name)
fmt.Fprintf(os.Stderr, "Removed MCP server '%s' from global config\n", name)
}
if err := saveMCPConfig(config); err != nil {
return fmt.Errorf("saving config: %w", err)
}
return nil
}
// MCPListGlobalHandler handles listing global MCP servers.
func MCPListGlobalHandler(cmd *cobra.Command, args []string) error {
config, err := loadMCPConfig()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
if len(config.MCPServers) == 0 {
fmt.Println("No global MCP servers configured")
fmt.Printf("Add one with: ollama mcp add NAME COMMAND [ARGS...]\n")
return nil
}
fmt.Printf("Global MCP servers (%s):\n\n", getMCPConfigPath())
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "NAME\tCOMMAND\tSTATUS")
for name, srv := range config.MCPServers {
cmdLine := srv.Command
if len(srv.Args) > 0 {
cmdLine += " " + strings.Join(srv.Args, " ")
}
status := "enabled"
if srv.Disabled {
status = "disabled"
}
fmt.Fprintf(w, "%s\t%s\t%s\n", name, cmdLine, status)
}
return w.Flush()
}
// MCPDisableHandler handles disabling an MCP server in global config.
func MCPDisableHandler(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: ollama mcp disable NAME [NAME...]")
}
config, err := loadMCPConfig()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
for _, name := range args {
srv, exists := config.MCPServers[name]
if !exists {
fmt.Fprintf(os.Stderr, "MCP server '%s' not found in global config\n", name)
continue
}
if srv.Disabled {
fmt.Fprintf(os.Stderr, "MCP server '%s' is already disabled\n", name)
continue
}
srv.Disabled = true
config.MCPServers[name] = srv
fmt.Fprintf(os.Stderr, "Disabled MCP server '%s'\n", name)
}
if err := saveMCPConfig(config); err != nil {
return fmt.Errorf("saving config: %w", err)
}
return nil
}
// MCPEnableHandler handles enabling an MCP server in global config.
func MCPEnableHandler(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: ollama mcp enable NAME [NAME...]")
}
config, err := loadMCPConfig()
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
for _, name := range args {
srv, exists := config.MCPServers[name]
if !exists {
fmt.Fprintf(os.Stderr, "MCP server '%s' not found in global config\n", name)
continue
}
if !srv.Disabled {
fmt.Fprintf(os.Stderr, "MCP server '%s' is already enabled\n", name)
continue
}
srv.Disabled = false
config.MCPServers[name] = srv
fmt.Fprintf(os.Stderr, "Enabled MCP server '%s'\n", name)
}
if err := saveMCPConfig(config); err != nil {
return fmt.Errorf("saving config: %w", err)
}
return nil
}
// MCPPushHandler handles the mcp push command.
func MCPPushHandler(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("usage: ollama mcp push NAME[:TAG] PATH")
}
name := args[0]
path := args[1]
// Expand path
if strings.HasPrefix(path, "~") {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("expanding home directory: %w", err)
}
path = filepath.Join(home, path[1:])
}
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("resolving path: %w", err)
}
// Validate MCP directory - check for mcp.json, package.json, or any config file
validFiles := []string{"mcp.json", "package.json", "server.py", "server.js", "main.py", "index.js"}
found := false
for _, vf := range validFiles {
if _, err := os.Stat(filepath.Join(absPath, vf)); err == nil {
found = true
break
}
}
if !found {
return fmt.Errorf("MCP directory should contain one of: %s", strings.Join(validFiles, ", "))
}
// Parse MCP name (will set Kind="mcp")
n := server.ParseMCPName(name)
if n.Model == "" {
return fmt.Errorf("invalid MCP name: %s", name)
}
p := progress.NewProgress(os.Stderr)
defer p.Stop()
// Create MCP layer
displayName := n.DisplayShortest()
status := fmt.Sprintf("Creating MCP layer for %s", displayName)
spinner := progress.NewSpinner(status)
p.Add(status, spinner)
layer, err := server.CreateMCPLayer(absPath)
if err != nil {
return fmt.Errorf("creating MCP layer: %w", err)
}
spinner.Stop()
// Create MCP manifest
manifest, configLayer, err := createMCPManifest(absPath, layer)
if err != nil {
return fmt.Errorf("creating MCP manifest: %w", err)
}
// Write manifest locally
manifestPath, err := server.GetMCPManifestPath(n)
if err != nil {
return fmt.Errorf("getting manifest path: %w", err)
}
if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil {
return fmt.Errorf("creating manifest directory: %w", err)
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
return fmt.Errorf("marshaling manifest: %w", err)
}
if err := os.WriteFile(manifestPath, manifestJSON, 0o644); err != nil {
return fmt.Errorf("writing manifest: %w", err)
}
fmt.Fprintf(os.Stderr, "MCP %s created locally\n", displayName)
fmt.Fprintf(os.Stderr, " Config: %s (%s)\n", configLayer.Digest, format.HumanBytes(configLayer.Size))
fmt.Fprintf(os.Stderr, " Layer: %s (%s)\n", layer.Digest, format.HumanBytes(layer.Size))
// Push to registry
client, err := api.ClientFromEnvironment()
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
insecure, _ := cmd.Flags().GetBool("insecure")
fmt.Fprintf(os.Stderr, "\nPushing to registry...\n")
fn := func(resp api.ProgressResponse) error {
if resp.Digest != "" {
bar := progress.NewBar(resp.Status, resp.Total, resp.Completed)
p.Add(resp.Digest, bar)
} else if resp.Status != "" {
spinner := progress.NewSpinner(resp.Status)
p.Add(resp.Status, spinner)
}
return nil
}
req := &api.PushRequest{
Model: displayName,
Insecure: insecure,
}
if err := client.Push(context.Background(), req, fn); err != nil {
// If push fails, still show success for local creation
fmt.Fprintf(os.Stderr, "\nNote: Local MCP created but push failed: %v\n", err)
fmt.Fprintf(os.Stderr, "You can try pushing later with: ollama mcp push %s\n", name)
return nil
}
fmt.Fprintf(os.Stderr, "Successfully pushed %s\n", displayName)
return nil
}
// MCPPullHandler handles the mcp pull command.
func MCPPullHandler(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("usage: ollama mcp pull NAME[:TAG]")
}
name := args[0]
n := server.ParseMCPName(name)
if n.Model == "" {
return fmt.Errorf("invalid MCP name: %s", name)
}
client, err := api.ClientFromEnvironment()
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
insecure, _ := cmd.Flags().GetBool("insecure")
p := progress.NewProgress(os.Stderr)
defer p.Stop()
fn := func(resp api.ProgressResponse) error {
if resp.Digest != "" {
bar := progress.NewBar(resp.Status, resp.Total, resp.Completed)
p.Add(resp.Digest, bar)
} else if resp.Status != "" {
spinner := progress.NewSpinner(resp.Status)
p.Add(resp.Status, spinner)
}
return nil
}
displayName := n.DisplayShortest()
req := &api.PullRequest{
Model: displayName,
Insecure: insecure,
}
if err := client.Pull(context.Background(), req, fn); err != nil {
return fmt.Errorf("pulling MCP: %w", err)
}
fmt.Fprintf(os.Stderr, "Successfully pulled %s\n", displayName)
return nil
}
// MCPListHandler handles the mcp list command.
func MCPListHandler(cmd *cobra.Command, args []string) error {
mcps, err := listLocalMCPs()
if err != nil {
return fmt.Errorf("listing MCPs: %w", err)
}
if len(mcps) == 0 {
fmt.Println("No MCPs installed")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "NAME\tTAG\tSIZE\tMODIFIED")
for _, mcp := range mcps {
fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\n",
mcp.Namespace,
mcp.Name,
mcp.Tag,
format.HumanBytes(mcp.Size),
format.HumanTime(mcp.ModifiedAt, "Never"),
)
}
return w.Flush()
}
// MCPRemoveHandler handles the mcp rm command.
func MCPRemoveHandler(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: ollama mcp rm NAME[:TAG] [NAME[:TAG]...]")
}
for _, name := range args {
n := server.ParseMCPName(name)
if n.Model == "" {
fmt.Fprintf(os.Stderr, "Invalid MCP name: %s\n", name)
continue
}
displayName := n.DisplayShortest()
manifestPath, err := server.GetMCPManifestPath(n)
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting manifest path for %s: %v\n", name, err)
continue
}
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "MCP not found: %s\n", displayName)
continue
}
if err := os.Remove(manifestPath); err != nil {
fmt.Fprintf(os.Stderr, "Error removing %s: %v\n", displayName, err)
continue
}
// Clean up empty parent directories
dir := filepath.Dir(manifestPath)
for dir != filepath.Join(os.Getenv("HOME"), ".ollama", "models", "manifests") {
entries, _ := os.ReadDir(dir)
if len(entries) == 0 {
os.Remove(dir)
dir = filepath.Dir(dir)
} else {
break
}
}
fmt.Fprintf(os.Stderr, "Deleted '%s'\n", displayName)
}
return nil
}
// MCPShowHandler handles the mcp show command.
func MCPShowHandler(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("usage: ollama mcp show NAME[:TAG]")
}
name := args[0]
n := server.ParseMCPName(name)
if n.Model == "" {
return fmt.Errorf("invalid MCP name: %s", name)
}
displayName := n.DisplayShortest()
manifestPath, err := server.GetMCPManifestPath(n)
if err != nil {
return fmt.Errorf("getting manifest path: %w", err)
}
data, err := os.ReadFile(manifestPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("MCP not found: %s", displayName)
}
return fmt.Errorf("reading manifest: %w", err)
}
var manifest server.Manifest
if err := json.Unmarshal(data, &manifest); err != nil {
return fmt.Errorf("parsing manifest: %w", err)
}
fmt.Printf("MCP: %s\n\n", displayName)
fmt.Println("Layers:")
for _, layer := range manifest.Layers {
fmt.Printf(" %s %s %s\n", layer.MediaType, layer.Digest[:19], format.HumanBytes(layer.Size))
}
// Try to read and display mcp.json or package.json content
if len(manifest.Layers) > 0 {
for _, layer := range manifest.Layers {
if layer.MediaType == server.MediaTypeMCP {
mcpPath, err := server.GetMCPsPath(layer.Digest)
if err == nil {
// Try mcp.json first
mcpJSONPath := filepath.Join(mcpPath, "mcp.json")
if content, err := os.ReadFile(mcpJSONPath); err == nil {
fmt.Println("\nConfig (mcp.json):")
fmt.Println(string(content))
} else {
// Try package.json
pkgJSONPath := filepath.Join(mcpPath, "package.json")
if content, err := os.ReadFile(pkgJSONPath); err == nil {
fmt.Println("\nConfig (package.json):")
fmt.Println(string(content))
}
}
// List files in the MCP
fmt.Println("\nFiles:")
filepath.Walk(mcpPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
relPath, _ := filepath.Rel(mcpPath, path)
if relPath == "." {
return nil
}
if info.IsDir() {
fmt.Printf(" %s/\n", relPath)
} else {
fmt.Printf(" %s (%s)\n", relPath, format.HumanBytes(info.Size()))
}
return nil
})
}
}
}
}
return nil
}
// MCPInfo represents information about an installed MCP.
type MCPInfo struct {
Namespace string
Name string
Tag string
Size int64
ModifiedAt time.Time
}
// listLocalMCPs returns a list of locally installed MCPs.
// MCPs are stored with 5-part paths: host/namespace/kind/model/tag
// where kind is "mcp".
func listLocalMCPs() ([]MCPInfo, error) {
manifestsPath := filepath.Join(os.Getenv("HOME"), ".ollama", "models", "manifests")
var mcps []MCPInfo
// Walk through all registries
registries, err := os.ReadDir(manifestsPath)
if err != nil {
if os.IsNotExist(err) {
return mcps, nil
}
return nil, err
}
for _, registry := range registries {
if !registry.IsDir() {
continue
}
// Walk namespaces
namespaces, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name()))
if err != nil {
continue
}
for _, namespace := range namespaces {
if !namespace.IsDir() {
continue
}
// Walk kinds looking for "mcp"
kinds, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name(), namespace.Name()))
if err != nil {
continue
}
for _, kind := range kinds {
if !kind.IsDir() {
continue
}
// Only process mcp kind
if kind.Name() != server.MCPNamespace {
continue
}
// Walk MCP names (model names)
mcpNames, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name(), namespace.Name(), kind.Name()))
if err != nil {
continue
}
for _, mcpName := range mcpNames {
if !mcpName.IsDir() {
continue
}
// Walk tags
tags, err := os.ReadDir(filepath.Join(manifestsPath, registry.Name(), namespace.Name(), kind.Name(), mcpName.Name()))
if err != nil {
continue
}
for _, tag := range tags {
manifestPath := filepath.Join(manifestsPath, registry.Name(), namespace.Name(), kind.Name(), mcpName.Name(), tag.Name())
fi, err := os.Stat(manifestPath)
if err != nil || fi.IsDir() {
continue
}
// Read manifest to get size
data, err := os.ReadFile(manifestPath)
if err != nil {
continue
}
var manifest server.Manifest
if err := json.Unmarshal(data, &manifest); err != nil {
continue
}
var totalSize int64
for _, layer := range manifest.Layers {
totalSize += layer.Size
}
// Build display name using model.Name
n := model.Name{
Host: registry.Name(),
Namespace: namespace.Name(),
Kind: kind.Name(),
Model: mcpName.Name(),
Tag: tag.Name(),
}
mcps = append(mcps, MCPInfo{
Namespace: n.Namespace + "/" + n.Kind,
Name: n.Model,
Tag: n.Tag,
Size: totalSize,
ModifiedAt: fi.ModTime(),
})
}
}
}
}
}
return mcps, nil
}
// createMCPManifest creates a manifest for a standalone MCP.
func createMCPManifest(mcpDir string, layer server.Layer) (*server.Manifest, *server.Layer, error) {
// Try to read mcp.json or package.json to extract metadata
name, description := extractMCPMetadata(mcpDir)
if name == "" {
// Use directory name as fallback
name = filepath.Base(mcpDir)
}
// Create config
config := map[string]any{
"name": name,
"description": description,
"architecture": "amd64",
"os": "linux",
}
configJSON, err := json.Marshal(config)
if err != nil {
return nil, nil, fmt.Errorf("marshaling config: %w", err)
}
// Create config layer
configLayer, err := server.NewLayer(strings.NewReader(string(configJSON)), "application/vnd.docker.container.image.v1+json")
if err != nil {
return nil, nil, fmt.Errorf("creating config layer: %w", err)
}
manifest := &server.Manifest{
SchemaVersion: 2,
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Config: configLayer,
Layers: []server.Layer{layer},
}
return manifest, &configLayer, nil
}
// extractMCPMetadata extracts name and description from mcp.json or package.json.
func extractMCPMetadata(mcpDir string) (name, description string) {
// Try mcp.json first
mcpJSONPath := filepath.Join(mcpDir, "mcp.json")
if data, err := os.ReadFile(mcpJSONPath); err == nil {
var config map[string]any
if err := json.Unmarshal(data, &config); err == nil {
if n, ok := config["name"].(string); ok {
name = n
}
if d, ok := config["description"].(string); ok {
description = d
}
return name, description
}
}
// Try package.json
pkgJSONPath := filepath.Join(mcpDir, "package.json")
if data, err := os.ReadFile(pkgJSONPath); err == nil {
var config map[string]any
if err := json.Unmarshal(data, &config); err == nil {
if n, ok := config["name"].(string); ok {
name = n
}
if d, ok := config["description"].(string); ok {
description = d
}
return name, description
}
}
return "", ""
}
// NewMCPCommand creates the mcp parent command with subcommands.
func NewMCPCommand() *cobra.Command {
mcpCmd := &cobra.Command{
Use: "mcp",
Short: "Manage MCP servers",
Long: "Commands for managing MCP (Model Context Protocol) servers (add, push, pull, list, rm, show)",
}
// Global config commands
addCmd := &cobra.Command{
Use: "add NAME COMMAND [ARGS...]",
Short: "Add an MCP server to global config",
Long: `Add an MCP server to the global config (~/.ollama/mcp.json).
Global MCP servers are available to all agents.
Examples:
ollama mcp add web-search uv run ./mcp-server.py
ollama mcp add calculator python3 /path/to/calc.py`,
Args: cobra.MinimumNArgs(2),
RunE: MCPAddHandler,
DisableFlagParsing: true, // Allow args with dashes
}
removeGlobalCmd := &cobra.Command{
Use: "remove-global NAME [NAME...]",
Aliases: []string{"rm-global"},
Short: "Remove an MCP server from global config",
Args: cobra.MinimumNArgs(1),
RunE: MCPRemoveGlobalHandler,
}
listGlobalCmd := &cobra.Command{
Use: "list-global",
Short: "List global MCP servers",
Args: cobra.NoArgs,
RunE: MCPListGlobalHandler,
}
// Registry commands
pushCmd := &cobra.Command{
Use: "push NAME[:TAG] PATH",
Short: "Push an MCP server to a registry",
Long: "Package a local MCP server directory and push it to a registry",
Args: cobra.ExactArgs(2),
PreRunE: checkServerHeartbeat,
RunE: MCPPushHandler,
}
pushCmd.Flags().Bool("insecure", false, "Use an insecure registry")
pullCmd := &cobra.Command{
Use: "pull NAME[:TAG]",
Short: "Pull an MCP server from a registry",
Args: cobra.ExactArgs(1),
PreRunE: checkServerHeartbeat,
RunE: MCPPullHandler,
}
pullCmd.Flags().Bool("insecure", false, "Use an insecure registry")
listCmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List installed MCP servers (from registry)",
Args: cobra.NoArgs,
RunE: MCPListHandler,
}
rmCmd := &cobra.Command{
Use: "rm NAME[:TAG] [NAME[:TAG]...]",
Aliases: []string{"remove", "delete"},
Short: "Remove an MCP server (from registry)",
Args: cobra.MinimumNArgs(1),
RunE: MCPRemoveHandler,
}
showCmd := &cobra.Command{
Use: "show NAME[:TAG]",
Short: "Show MCP server details",
Args: cobra.ExactArgs(1),
RunE: MCPShowHandler,
}
disableCmd := &cobra.Command{
Use: "disable NAME [NAME...]",
Short: "Disable an MCP server (keep in config)",
Long: `Disable an MCP server without removing it from config.
Disabled servers will not be started when running agents.
Use 'ollama mcp enable' to re-enable.`,
Args: cobra.MinimumNArgs(1),
RunE: MCPDisableHandler,
}
enableCmd := &cobra.Command{
Use: "enable NAME [NAME...]",
Short: "Enable a disabled MCP server",
Long: `Re-enable a previously disabled MCP server.`,
Args: cobra.MinimumNArgs(1),
RunE: MCPEnableHandler,
}
mcpCmd.AddCommand(addCmd, removeGlobalCmd, listGlobalCmd, disableCmd, enableCmd, pushCmd, pullCmd, listCmd, rmCmd, showCmd)
return mcpCmd
}