From 846fbf8ae9ffd08a18e29605894b716558e93f13 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 23 Feb 2026 19:08:29 +0000 Subject: [PATCH] feat: context-aware completions with dynamic resource names - Hide attach-server/detach-server from --help (only relevant with --project) - --project shows only project-scoped commands in tab completion - Tab after resource type fetches live resource names from API - --project value auto-completes from existing project names - Stop offering resource types after one is already selected Co-Authored-By: Claude Opus 4.6 --- completions/mcpctl.bash | 101 +++++++++++++++++++++++++-------- completions/mcpctl.fish | 121 +++++++++++++++++++++++++++++++++------- src/cli/src/index.ts | 4 +- 3 files changed, 183 insertions(+), 43 deletions(-) diff --git a/completions/mcpctl.bash b/completions/mcpctl.bash index bd4136b..00811fb 100644 --- a/completions/mcpctl.bash +++ b/completions/mcpctl.bash @@ -3,12 +3,65 @@ _mcpctl() { _init_completion || return local commands="status login logout config get describe delete logs create edit apply backup restore help" - local global_opts="-v --version --daemon-url --direct -h --help" + local project_commands="attach-server detach-server get describe delete logs create edit help" + local global_opts="-v --version --daemon-url --direct --project -h --help" local resources="servers instances secrets templates projects users groups rbac" - case "${words[1]}" in + # Check if --project was given + local has_project=false + local i + for ((i=1; i < cword; i++)); do + if [[ "${words[i]}" == "--project" ]]; then + has_project=true + break + fi + done + + # Find the first subcommand (skip --project and its argument, skip flags) + local subcmd="" + local subcmd_pos=0 + for ((i=1; i < cword; i++)); do + if [[ "${words[i]}" == "--project" || "${words[i]}" == "--daemon-url" ]]; then + ((i++)) # skip the argument + continue + fi + if [[ "${words[i]}" != -* ]]; then + subcmd="${words[i]}" + subcmd_pos=$i + break + fi + done + + # Find the resource type after get/describe/delete/edit + local resource_type="" + if [[ -n "$subcmd_pos" ]] && [[ $subcmd_pos -gt 0 ]]; then + for ((i=subcmd_pos+1; i < cword; i++)); do + if [[ "${words[i]}" != -* ]] && [[ " $resources " == *" ${words[i]} "* ]]; then + resource_type="${words[i]}" + break + fi + done + fi + + # If completing the --project value + if [[ "$prev" == "--project" ]]; then + local names + names=$(mcpctl get projects -o json 2>/dev/null | grep -oP '"name":\s*"\K[^"]+') + COMPREPLY=($(compgen -W "$names" -- "$cur")) + return + fi + + # Fetch resource names dynamically + _mcpctl_resource_names() { + local rt="$1" + if [[ -n "$rt" ]]; then + mcpctl get "$rt" -o json 2>/dev/null | grep -oP '"name":\s*"\K[^"]+' + fi + } + + case "$subcmd" in config) - if [[ $cword -eq 2 ]]; then + if [[ $((cword - subcmd_pos)) -eq 1 ]]; then COMPREPLY=($(compgen -W "view set path reset claude-generate impersonate help" -- "$cur")) fi return ;; @@ -20,35 +73,29 @@ _mcpctl() { return ;; logout) return ;; - get) - if [[ $cword -eq 2 ]]; then + get|describe|delete) + if [[ -z "$resource_type" ]]; then COMPREPLY=($(compgen -W "$resources" -- "$cur")) else - COMPREPLY=($(compgen -W "-o --output -h --help" -- "$cur")) - fi - return ;; - describe) - if [[ $cword -eq 2 ]]; then - COMPREPLY=($(compgen -W "$resources" -- "$cur")) - else - COMPREPLY=($(compgen -W "-o --output --show-values -h --help" -- "$cur")) - fi - return ;; - delete) - if [[ $cword -eq 2 ]]; then - COMPREPLY=($(compgen -W "$resources" -- "$cur")) + local names + names=$(_mcpctl_resource_names "$resource_type") + COMPREPLY=($(compgen -W "$names -o --output -h --help" -- "$cur")) fi return ;; edit) - if [[ $cword -eq 2 ]]; then + if [[ -z "$resource_type" ]]; then COMPREPLY=($(compgen -W "servers projects" -- "$cur")) + else + local names + names=$(_mcpctl_resource_names "$resource_type") + COMPREPLY=($(compgen -W "$names -h --help" -- "$cur")) fi return ;; logs) COMPREPLY=($(compgen -W "--tail --since -f --follow -h --help" -- "$cur")) return ;; create) - if [[ $cword -eq 2 ]]; then + if [[ $((cword - subcmd_pos)) -eq 1 ]]; then COMPREPLY=($(compgen -W "server secret project user group rbac help" -- "$cur")) fi return ;; @@ -61,13 +108,23 @@ _mcpctl() { restore) COMPREPLY=($(compgen -W "-i --input -p --password -c --conflict -h --help" -- "$cur")) return ;; + attach-server|detach-server) + local names + names=$(_mcpctl_resource_names "servers") + COMPREPLY=($(compgen -W "$names" -- "$cur")) + return ;; help) COMPREPLY=($(compgen -W "$commands" -- "$cur")) return ;; esac - if [[ $cword -eq 1 ]]; then - COMPREPLY=($(compgen -W "$commands $global_opts" -- "$cur")) + # No subcommand yet — offer commands based on context + if [[ -z "$subcmd" ]]; then + if $has_project; then + COMPREPLY=($(compgen -W "$project_commands $global_opts" -- "$cur")) + else + COMPREPLY=($(compgen -W "$commands $global_opts" -- "$cur")) + fi fi } diff --git a/completions/mcpctl.fish b/completions/mcpctl.fish index e523ae9..9c2efae 100644 --- a/completions/mcpctl.fish +++ b/completions/mcpctl.fish @@ -1,6 +1,7 @@ # mcpctl fish completions set -l commands status login logout config get describe delete logs create edit apply backup restore help +set -l project_commands attach-server detach-server get describe delete logs create edit help # Disable file completions by default complete -c mcpctl -f @@ -9,30 +10,112 @@ complete -c mcpctl -f complete -c mcpctl -s v -l version -d 'Show version' complete -c mcpctl -l daemon-url -d 'mcplocal daemon URL' -x complete -c mcpctl -l direct -d 'Bypass mcplocal, connect directly to mcpd' +complete -c mcpctl -l project -d 'Target project context' -x complete -c mcpctl -s h -l help -d 'Show help' -# Top-level commands -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a status -d 'Show status and connectivity' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a login -d 'Authenticate with mcpd' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a logout -d 'Log out' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a config -d 'Manage configuration' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a get -d 'List resources' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a describe -d 'Show resource details' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a delete -d 'Delete a resource' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a logs -d 'Get instance logs' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a create -d 'Create a resource' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a edit -d 'Edit a resource' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a apply -d 'Apply configuration from file' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a backup -d 'Backup configuration' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a restore -d 'Restore from backup' -complete -c mcpctl -n "not __fish_seen_subcommand_from $commands" -a help -d 'Show help' +# Helper: check if --project was given +function __mcpctl_has_project + set -l tokens (commandline -opc) + for i in (seq (count $tokens)) + if test "$tokens[$i]" = "--project" + return 0 + end + end + return 1 +end -# Resource types for get/describe/delete/edit +# Helper: check if a resource type has been selected after get/describe/delete/edit set -l resources servers instances secrets templates projects users groups rbac -complete -c mcpctl -n "__fish_seen_subcommand_from get describe delete" -a "$resources" -d 'Resource type' -complete -c mcpctl -n "__fish_seen_subcommand_from edit" -a 'servers projects' -d 'Resource type' -# get/describe/delete options +function __mcpctl_needs_resource_type + set -l tokens (commandline -opc) + set -l found_cmd false + for tok in $tokens + if $found_cmd + # Check if next token after get/describe/delete/edit is a resource type + if contains -- $tok servers instances secrets templates projects users groups rbac + return 1 # resource type already present + end + end + if contains -- $tok get describe delete edit + set found_cmd true + end + end + if $found_cmd + return 0 # command found but no resource type yet + end + return 1 +end + +function __mcpctl_get_resource_type + set -l tokens (commandline -opc) + set -l found_cmd false + for tok in $tokens + if $found_cmd + if contains -- $tok servers instances secrets templates projects users groups rbac + echo $tok + return + end + end + if contains -- $tok get describe delete edit + set found_cmd true + end + end +end + +# Fetch resource names dynamically from the API +function __mcpctl_resource_names + set -l resource (__mcpctl_get_resource_type) + if test -z "$resource" + return + end + # Use mcpctl to fetch names (quick JSON parse with string manipulation) + mcpctl get $resource -o json 2>/dev/null | string match -rg '"name":\s*"([^"]+)"' +end + +# Fetch project names for --project value +function __mcpctl_project_names + mcpctl get projects -o json 2>/dev/null | string match -rg '"name":\s*"([^"]+)"' +end + +# --project value completion +complete -c mcpctl -l project -xa '(__mcpctl_project_names)' + +# Top-level commands (without --project) +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a status -d 'Show status and connectivity' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a login -d 'Authenticate with mcpd' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a logout -d 'Log out' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a config -d 'Manage configuration' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a get -d 'List resources' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a describe -d 'Show resource details' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a delete -d 'Delete a resource' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a logs -d 'Get instance logs' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a create -d 'Create a resource' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a edit -d 'Edit a resource' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a apply -d 'Apply configuration from file' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a backup -d 'Backup configuration' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a restore -d 'Restore from backup' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a help -d 'Show help' + +# Project-scoped commands (with --project) +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a attach-server -d 'Attach a server to the project' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a detach-server -d 'Detach a server from the project' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a get -d 'List resources (scoped to project)' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a describe -d 'Show resource details' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a delete -d 'Delete a resource' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a logs -d 'Get instance logs' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a create -d 'Create a resource' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a edit -d 'Edit a resource' +complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a help -d 'Show help' + +# Resource types — only when resource type not yet selected +complete -c mcpctl -n "__fish_seen_subcommand_from get describe delete; and __mcpctl_needs_resource_type" -a "$resources" -d 'Resource type' +complete -c mcpctl -n "__fish_seen_subcommand_from edit; and __mcpctl_needs_resource_type" -a 'servers projects' -d 'Resource type' + +# Resource names — after resource type is selected +complete -c mcpctl -n "__fish_seen_subcommand_from get describe delete edit; and not __mcpctl_needs_resource_type" -a '(__mcpctl_resource_names)' -d 'Resource name' + +# get/describe options complete -c mcpctl -n "__fish_seen_subcommand_from get" -s o -l output -d 'Output format' -xa 'table json yaml' complete -c mcpctl -n "__fish_seen_subcommand_from describe" -s o -l output -d 'Output format' -xa 'detail json yaml' complete -c mcpctl -n "__fish_seen_subcommand_from describe" -l show-values -d 'Show secret values' diff --git a/src/cli/src/index.ts b/src/cli/src/index.ts index cefad1c..f2e7461 100644 --- a/src/cli/src/index.ts +++ b/src/cli/src/index.ts @@ -148,8 +148,8 @@ export function createProgram(): Command { log: (...args: string[]) => console.log(...args), getProject: () => program.opts().project as string | undefined, }; - program.addCommand(createAttachServerCommand(projectOpsDeps)); - program.addCommand(createDetachServerCommand(projectOpsDeps)); + program.addCommand(createAttachServerCommand(projectOpsDeps), { hidden: true }); + program.addCommand(createDetachServerCommand(projectOpsDeps), { hidden: true }); return program; }