Add MCP server management commands and CLI completion

This commit is contained in:
Cheikh Seck committed 2026-04-12 14:54:11 +00:00
1 parent 83303b0055
commit 65f93dba3d
5 files changed
+443 -2

No files matched your search

+9
View File
@@ -6,6 +6,15 @@ GoDex supports integrating with external MCP (Model Context Protocol) servers to
External MCP servers are configured in your `~/.godex/providers.yaml` file under each provider's `mcp_servers` section.
You can also manage MCP servers from the CLI:
```bash
godex mcp add --provider ollama --name filesystem --allowed-path /home/user/project
godex mcp add --provider ollama --name webscraper --allowed-url https://docs.example.com
godex mcp add --provider ollama --name myserver --command /usr/local/bin/my-mcp --args --flag --allowed-path /tmp
godex mcp remove --provider ollama --name myserver
```
### MCPServer Structure
Each MCP server is defined with the following properties:
+7
View File
@@ -278,6 +278,12 @@ godex --prompt "list files in current directory"
# Run wizard to create config
godex --wizard
# Manage MCP servers in config
godex mcp add --provider ollama --name filesystem --allowed-path /home/user/project
godex mcp add --provider ollama --name webscraper --allowed-url https://docs.example.com
godex mcp add --provider ollama --name myserver --command /usr/local/bin/my-mcp --args --flag --allowed-path /tmp
godex mcp remove --provider ollama --name myserver
```
### Shell Completion
@@ -303,6 +309,7 @@ After sourcing, pressing Tab will show:
- All available flags with descriptions
- Provider names when using `--provider`
- File paths when using `--config`
- `mcp` subcommands and flags
### Commands in TUI
+187 -1
View File
@@ -32,6 +32,17 @@ PROG="{{.ProgName}}"
fi
}
{{.ProgName}}_get_mcp_servers() {
local config_path=$(${PROG}_get_config_path)
local provider_name="${1:-}"
if [[ -z "$provider_name" ]]; then
provider_name=$(${PROG}_get_default_provider)
fi
if [[ -f "$config_path" ]]; then
${PROG} --completion mcp-servers "$config_path" "$provider_name"
fi
}
{{.ProgName}}_get_default_provider() {
local config_path=$(${PROG}_get_config_path)
if [[ -f "$config_path" ]]; then
@@ -63,11 +74,60 @@ PROG="{{.ProgName}}"
echo "--debug:enable debug mode"
echo "--completion:generate shell completion"
echo "--llama-server:external llama.cpp server URL"
echo "mcp:manage MCP servers (subcommand)"
}
_{{.ProgName}}_completion() {
local cur prev words cword
_init_completion || return
if [[ "${words[1]}" == "mcp" ]]; then
if [[ $cword -eq 2 ]]; then
COMPREPLY=($(compgen -W "add remove" -- "${cur}"))
return
fi
case "${prev}" in
--name)
local provider_name=""
local i
for ((i=1; i<cword; i++)); do
if [[ "${words[i]}" == "--provider" ]] && [[ -n "${words[i+1]}" ]] && [[ "${words[i+1]}" != --* ]]; then
provider_name="${words[i+1]}"
break
fi
done
if [[ "${words[2]}" == "remove" ]]; then
local servers
servers=$(${PROG}_get_mcp_servers "$provider_name")
COMPREPLY=($(compgen -W "$servers" -- "${cur}"))
else
COMPREPLY=($(compgen -W "filesystem bash webscraper" -- "${cur}"))
fi
return
;;
--provider)
local providers
providers=$(${PROG}_get_providers)
COMPREPLY=($(compgen -W "$providers" -- "${cur}"))
return
;;
--config)
COMPREPLY=($(compgen -d -S/ -- "${cur}"))
COMPREPLY+=($(compgen -f -X '!*.yaml' -X '!*.yml' -- "${cur}"))
return
;;
--allowed-path)
COMPREPLY=($(compgen -d -S/ -- "${cur}"))
return
;;
esac
local mcp_flags="--provider --name --command --args --env --transport --allowed-path --allowed-url"
if [[ "${cur}" == -* ]]; then
COMPREPLY=($(compgen -W "${mcp_flags}" -- "${cur}"))
return
fi
fi
case "${prev}" in
--provider)
@@ -127,6 +187,7 @@ _{{.ProgName}}_completion() {
while IFS= read -r p; do
COMPREPLY+=("$p")
done <<< "$providers"
COMPREPLY+=("mcp")
fi
}
@@ -150,10 +211,43 @@ const zshCompletionTemplate = `#compdef godex
case "$state" in
cmd)
local -a commands
commands=("mcp:manage MCP servers")
_describe 'commands' commands
_describe 'providers' providers
;;
args)
case "$words[1]" in
mcp)
if [[ $CURRENT -eq 2 ]]; then
_describe 'subcommands' 'add:register MCP server' 'remove:unregister MCP server'
return
fi
local provider_name=""
for ((i=2; i<=CURRENT; i++)); do
if [[ "${words[i-1]}" == "--provider" ]] && [[ -n "${words[i]}" ]] && [[ "${words[i]}" != --* ]]; then
provider_name="${words[i]}"
break
fi
done
if [[ -z "$provider_name" ]]; then
provider_name="${providers[1]}"
fi
local -a mcp_servers
if [[ -n "$provider_name" ]]; then
mcp_servers=(${(f)"$({{.ProgName}} --completion mcp-servers "$config_path" "$provider_name")"})
fi
_arguments \
'--provider[provider name]:provider:->providers' \
'--name[MCP server name]:server:->mcp_name' \
'--command[external MCP command]' \
'--args[command arg]' \
'--env[env var KEY=VALUE]' \
'--transport[MCP transport]' \
'--allowed-path[allowed path]' \
'--allowed-url[allowed URL]'
return
;;
--config)
_files -g "*.yaml" -g "*.yml"
;;
@@ -177,6 +271,22 @@ const zshCompletionTemplate = `#compdef godex
;;
esac
;;
mcp_servers)
if (( ${#mcp_servers[@]} )); then
_describe 'mcp servers' mcp_servers
fi
;;
mcp_name)
if [[ "${words[2]}" == "add" ]]; then
local -a builtins
builtins=("filesystem" "bash" "webscraper")
_describe 'mcp servers' builtins
else
if (( ${#mcp_servers[@]} )); then
_describe 'mcp servers' mcp_servers
fi
fi
;;
esac
}
@@ -275,6 +385,35 @@ func listModels(configPath string, providerName string, query string) []string {
return models
}
func listMCPServers(configPath string, providerName string) []string {
cfg, err := config.Load(configPath)
if err != nil {
return nil
}
var provider *config.Provider
for i := range cfg.Providers {
if cfg.Providers[i].Name == providerName {
provider = &cfg.Providers[i]
break
}
}
if provider == nil {
if len(cfg.Providers) > 0 {
provider = &cfg.Providers[0]
}
}
if provider == nil {
return nil
}
var names []string
for _, s := range provider.MCPServers {
names = append(names, s.Name)
}
return names
}
func listAllModels(ctx context.Context, p modelquery.Provider) ([]string, error) {
models, err := modelquery.ListModels(ctx, p)
if err != nil {
@@ -338,6 +477,19 @@ func runCompletion(args []string) {
for _, m := range models {
fmt.Println(m)
}
case "mcp-servers":
configPath := os.ExpandEnv("${HOME}/.godex/providers.yaml")
providerName := ""
if len(args) > 1 {
configPath = args[1]
}
if len(args) > 2 {
providerName = args[2]
}
servers := listMCPServers(configPath, providerName)
for _, s := range servers {
fmt.Println(s)
}
default:
printCompletionHelp()
}
@@ -400,6 +552,26 @@ function __godex_complete_models
end
end
function __godex_complete_mcp_servers
set -l default_path %s
set -l config_path "$default_path"
set -l current_provider ""
for i in (seq (count (commandline -opc)))
set -l arg (commandline -opc)[$i]
if test "$arg" = "--config"
set config_path (commandline -opc)[(math $i + 1)]
else if test "$arg" = "--provider"
set current_provider (commandline -opc)[(math $i + 1)]
end
end
if test -z "$current_provider"
set current_provider (godex --completion providers "$config_path" 2>/dev/null | head -1)
end
if test -f "$config_path"
godex --completion mcp-servers "$config_path" "$current_provider"
end
end
complete -c godex -l config -r -f
complete -c godex -l provider -x -a "(__godex_complete_providers)"
complete -c godex -l model -x -a "(__godex_complete_models)"
@@ -410,7 +582,19 @@ complete -c godex -l version -s v -f
complete -c godex -l debug -s d -f
complete -c godex -l completion -x -a "bash zsh fish" -d "Generate shell completion"
complete -c godex -l llama-server -s l -x -d "External llama.cpp server URL"
`, defaultConfigPath, defaultConfigPath)
complete -c godex -n "__fish_use_subcommand" -a "mcp" -d "Manage MCP servers"
complete -c godex -n "__fish_seen_subcommand_from mcp" -a "add remove" -d "MCP subcommand"
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l provider -x -a "(__godex_complete_providers)"
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l name -x -a "filesystem bash webscraper"
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l command -x
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l args -x
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l env -x
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l transport -x
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l allowed-path -x -a "(__fish_complete_directories)"
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from add" -l allowed-url -x
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from remove" -l provider -x -a "(__godex_complete_providers)"
complete -c godex -n "__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from remove" -l name -x -a "(__godex_complete_mcp_servers)"
`, defaultConfigPath, defaultConfigPath, defaultConfigPath)
}
func printCompletionHelp() {
@@ -419,6 +603,8 @@ func printCompletionHelp() {
Usage:
godex --completion <shell>
Includes flags plus the "mcp" subcommands (add/remove) and their options.
Shells:
bash Generate bash completion script
zsh Generate zsh completion script
+175
View File
@@ -142,6 +142,13 @@ func main() {
os.Exit(0)
}
if args := flag.Args(); len(args) > 0 && args[0] == "mcp" {
if err := handleMCPSubcommand(args[1:], configPath); err != nil {
log.Fatalf("mcp: %v", err)
}
return
}
// Handle version flag
if printVersion {
fmt.Printf("godex version %s\n", version)
@@ -1223,6 +1230,174 @@ promptLoop:
fmt.Println("Goodbye!")
}
type stringList []string
func (s *stringList) String() string {
return strings.Join(*s, ",")
}
func (s *stringList) Set(value string) error {
*s = append(*s, value)
return nil
}
func handleMCPSubcommand(args []string, configPath string) error {
if len(args) == 0 {
return fmt.Errorf("usage: godex mcp <add|remove> [options]")
}
isFilesystem := func(name string) bool {
switch strings.ToLower(strings.TrimSpace(name)) {
case "filesystem", "files":
return true
default:
return false
}
}
isBash := func(name string) bool {
switch strings.ToLower(strings.TrimSpace(name)) {
case "bash", "shell", "exec":
return true
default:
return false
}
}
isWeb := func(name string) bool {
switch strings.ToLower(strings.TrimSpace(name)) {
case "webscraper", "web", "browser":
return true
default:
return false
}
}
switch args[0] {
case "add":
fs := flag.NewFlagSet("mcp add", flag.ContinueOnError)
var providerName string
var serverName string
var command string
var transport string
var rawArgs stringList
var rawEnv stringList
var allowedPaths stringList
var allowedURLs stringList
fs.StringVar(&providerName, "provider", "", "provider name to update (defaults to configured default)")
fs.StringVar(&serverName, "name", "", "MCP server name (e.g., filesystem, bash, webscraper, or external name)")
fs.StringVar(&command, "command", "", "command for external MCP server")
fs.Var(&rawArgs, "args", "MCP server arg (repeatable)")
fs.Var(&rawEnv, "env", "MCP server env (repeatable, KEY=VALUE)")
fs.StringVar(&transport, "transport", "", "MCP transport (e.g., stdio)")
fs.Var(&allowedPaths, "allowed-path", "allowed path for MCP server (repeatable)")
fs.Var(&allowedURLs, "allowed-url", "allowed URL for MCP server (repeatable)")
if err := fs.Parse(args[1:]); err != nil {
return err
}
if strings.TrimSpace(serverName) == "" {
return fmt.Errorf("missing --name")
}
if !isFilesystem(serverName) && !isBash(serverName) && !isWeb(serverName) {
if strings.TrimSpace(command) == "" {
return fmt.Errorf("--command is required for external MCP servers")
}
}
cfg, err := config.Load(configPath)
if err != nil {
return err
}
provider := cfg.DefaultOrFirst()
if strings.TrimSpace(providerName) != "" {
if p := cfg.ProviderByName(providerName); p != nil {
provider = p
} else {
return fmt.Errorf("provider %q not found", providerName)
}
}
if provider == nil {
return fmt.Errorf("no providers configured")
}
for _, s := range provider.MCPServers {
if strings.EqualFold(s.Name, serverName) {
return fmt.Errorf("MCP server %q already exists on provider %q", serverName, provider.Name)
}
}
allows := append([]string{}, allowedPaths...)
allows = append(allows, allowedURLs...)
provider.MCPServers = append(provider.MCPServers, config.MCPServer{
Name: strings.TrimSpace(serverName),
Command: strings.TrimSpace(command),
Args: []string(rawArgs),
Env: []string(rawEnv),
Transport: strings.TrimSpace(transport),
AllowedPaths: allows,
})
if err := config.Save(configPath, cfg); err != nil {
return err
}
fmt.Printf("Added MCP server %q to provider %q\n", serverName, provider.Name)
return nil
case "remove":
fs := flag.NewFlagSet("mcp remove", flag.ContinueOnError)
var providerName string
var serverName string
fs.StringVar(&providerName, "provider", "", "provider name to update (defaults to configured default)")
fs.StringVar(&serverName, "name", "", "MCP server name to remove")
if err := fs.Parse(args[1:]); err != nil {
return err
}
if strings.TrimSpace(serverName) == "" {
return fmt.Errorf("missing --name")
}
cfg, err := config.Load(configPath)
if err != nil {
return err
}
provider := cfg.DefaultOrFirst()
if strings.TrimSpace(providerName) != "" {
if p := cfg.ProviderByName(providerName); p != nil {
provider = p
} else {
return fmt.Errorf("provider %q not found", providerName)
}
}
if provider == nil {
return fmt.Errorf("no providers configured")
}
found := false
before := len(provider.MCPServers)
var kept []config.MCPServer
for _, s := range provider.MCPServers {
if strings.EqualFold(s.Name, serverName) {
found = true
continue
}
if s.Name != serverName {
kept = append(kept, s)
}
}
provider.MCPServers = kept
if !found || len(provider.MCPServers) == before {
return fmt.Errorf("no MCP server named %q found on provider %q", serverName, provider.Name)
}
if err := config.Save(configPath, cfg); err != nil {
return err
}
fmt.Printf("Removed MCP server %q from provider %q\n", serverName, provider.Name)
return nil
default:
return fmt.Errorf("unknown subcommand %q (expected add or remove)", args[0])
}
}
type mcpLog struct {
lines []string
}
+65 -1
View File
@@ -139,6 +139,17 @@ godex_get_models() {
fi
}
godex_get_mcp_servers() {
local config_path=$(godex_get_config_path)
local provider_name="${1:-}"
if [[ -z "$provider_name" ]]; then
provider_name=$(godex_get_default_provider)
fi
if [[ -f "$config_path" ]]; then
godex --completion mcp-servers "$config_path" "$provider_name"
fi
}
godex_get_flags_with_desc() {
echo "--config:provider configuration YAML"
echo "--provider:provider name to use"
@@ -151,11 +162,59 @@ godex_get_flags_with_desc() {
echo "--debug:enable debug mode"
echo "--completion:generate shell completion"
echo "--llama-server:external llama.cpp server URL"
echo "mcp:manage MCP servers (subcommand)"
}
_cgodex_completion() {
local cur prev words cword
_init_completion || return
if [[ "${words[1]}" == "mcp" ]]; then
if [[ $cword -eq 2 ]]; then
COMPREPLY=($(compgen -W "add remove" -- "${cur}"))
return 0
fi
case "$prev" in
--name)
local provider_name=""
local i
for ((i=1; i<cword; i++)); do
if [[ "${words[i]}" == "--provider" ]] && [[ -n "${words[i+1]}" ]] && [[ "${words[i+1]}" != --* ]]; then
provider_name="${words[i+1]}"
break
fi
done
if [[ "${words[2]}" == "remove" ]]; then
local servers=$(godex_get_mcp_servers "$provider_name")
COMPREPLY=($(compgen -W "$servers" -- "$cur"))
else
COMPREPLY=($(compgen -W "filesystem bash webscraper" -- "$cur"))
fi
return 0
;;
--provider)
local providers=$(godex_get_providers)
if [[ -n "$providers" ]]; then
COMPREPLY=($(compgen -W "$providers" -- "$cur"))
fi
return 0
;;
--config)
COMPREPLY=($(compgen -d -S/ -- "$cur"))
COMPREPLY+=($(compgen -f -X '!*.yaml' -X '!*.yml' -- "$cur"))
return 0
;;
--allowed-path)
COMPREPLY=($(compgen -d -S/ -- "$cur"))
return 0
;;
esac
if [[ "$cur" == -* ]]; then
local mcp_flags="--provider --name --command --args --env --transport --allowed-path --allowed-url"
COMPREPLY=($(compgen -W "$mcp_flags" -- "$cur"))
return 0
fi
fi
case "$prev" in
--provider)
@@ -193,7 +252,12 @@ _cgodex_completion() {
local flags=$(godex_get_flags_with_desc | cut -d: -f1)
COMPREPLY=($(compgen -W "$flags" -- "$cur"))
else
COMPREPLY=($(compgen -f -- "$cur"))
local providers=$(godex_get_providers)
if [[ -n "$providers" ]]; then
COMPREPLY=($(compgen -W "$providers mcp" -- "$cur"))
else
COMPREPLY=($(compgen -W "mcp" -- "$cur"))
fi
fi
}