fix: prevent attach/detach-server from repeating server arg on tab

Added __mcpctl_needs_server_arg guard in fish and position check in
bash so completions stop after one server name is selected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-23 19:36:45 +00:00
parent 413dd783cd
commit d00973dc54
3 changed files with 36 additions and 9 deletions

View File

@@ -125,6 +125,8 @@ _mcpctl() {
COMPREPLY=($(compgen -W "-i --input -p --password -c --conflict -h --help" -- "$cur")) COMPREPLY=($(compgen -W "-i --input -p --password -c --conflict -h --help" -- "$cur"))
return ;; return ;;
attach-server) attach-server)
# Only complete if no server arg given yet (first arg after subcmd)
if [[ $((cword - subcmd_pos)) -ne 1 ]]; then return; fi
local proj names all_servers proj_servers local proj names all_servers proj_servers
proj=$(_mcpctl_get_project_value) proj=$(_mcpctl_get_project_value)
if [[ -n "$proj" ]]; then if [[ -n "$proj" ]]; then
@@ -137,6 +139,8 @@ _mcpctl() {
COMPREPLY=($(compgen -W "$names" -- "$cur")) COMPREPLY=($(compgen -W "$names" -- "$cur"))
return ;; return ;;
detach-server) detach-server)
# Only complete if no server arg given yet (first arg after subcmd)
if [[ $((cword - subcmd_pos)) -ne 1 ]]; then return; fi
local proj names local proj names
proj=$(_mcpctl_get_project_value) proj=$(_mcpctl_get_project_value)
if [[ -n "$proj" ]]; then if [[ -n "$proj" ]]; then

View File

@@ -159,11 +159,31 @@ complete -c mcpctl -n "__fish_seen_subcommand_from edit; and __mcpctl_needs_reso
# Resource names — after resource type is selected # 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' 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'
# attach-server: show servers NOT in the project # Helper: check if attach-server/detach-server already has a server argument
complete -c mcpctl -n "__fish_seen_subcommand_from attach-server" -a '(__mcpctl_available_servers)' -d 'Server' function __mcpctl_needs_server_arg
set -l tokens (commandline -opc)
set -l found_cmd false
for tok in $tokens
if $found_cmd
if not string match -q -- '-*' $tok
return 1 # server arg already present
end
end
if contains -- $tok attach-server detach-server
set found_cmd true
end
end
if $found_cmd
return 0 # command found but no server arg yet
end
return 1
end
# detach-server: show servers IN the project # attach-server: show servers NOT in the project (only if no server arg yet)
complete -c mcpctl -n "__fish_seen_subcommand_from detach-server" -a '(__mcpctl_project_servers)' -d 'Server' complete -c mcpctl -n "__fish_seen_subcommand_from attach-server; and __mcpctl_needs_server_arg" -a '(__mcpctl_available_servers)' -d 'Server'
# detach-server: show servers IN the project (only if no server arg yet)
complete -c mcpctl -n "__fish_seen_subcommand_from detach-server; and __mcpctl_needs_server_arg" -a '(__mcpctl_project_servers)' -d 'Server'
# get/describe options # 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 get" -s o -l output -d 'Output format' -xa 'table json yaml'

View File

@@ -94,19 +94,20 @@ describe('fish completions', () => {
expect(resourceNamesFn, 'must handle instances via server.name').toContain('.server.name'); expect(resourceNamesFn, 'must handle instances via server.name').toContain('.server.name');
}); });
it('attach-server completes with available (unattached) servers', () => { it('attach-server completes with available (unattached) servers and guards against repeat', () => {
// Find the line that provides argument completions AFTER attach-server is selected
const attachLine = fishFile.split('\n').find((l) => const attachLine = fishFile.split('\n').find((l) =>
l.startsWith('complete') && l.includes('__fish_seen_subcommand_from attach-server')); l.startsWith('complete') && l.includes('__fish_seen_subcommand_from attach-server'));
expect(attachLine, 'attach-server argument completion must exist').toBeDefined(); expect(attachLine, 'attach-server argument completion must exist').toBeDefined();
expect(attachLine, 'attach-server must use __mcpctl_available_servers').toContain('__mcpctl_available_servers'); expect(attachLine, 'attach-server must use __mcpctl_available_servers').toContain('__mcpctl_available_servers');
expect(attachLine, 'attach-server must guard with __mcpctl_needs_server_arg').toContain('__mcpctl_needs_server_arg');
}); });
it('detach-server completes with project servers', () => { it('detach-server completes with project servers and guards against repeat', () => {
const detachLine = fishFile.split('\n').find((l) => const detachLine = fishFile.split('\n').find((l) =>
l.startsWith('complete') && l.includes('__fish_seen_subcommand_from detach-server')); l.startsWith('complete') && l.includes('__fish_seen_subcommand_from detach-server'));
expect(detachLine, 'detach-server argument completion must exist').toBeDefined(); expect(detachLine, 'detach-server argument completion must exist').toBeDefined();
expect(detachLine, 'detach-server must use __mcpctl_project_servers').toContain('__mcpctl_project_servers'); expect(detachLine, 'detach-server must use __mcpctl_project_servers').toContain('__mcpctl_project_servers');
expect(detachLine, 'detach-server must guard with __mcpctl_needs_server_arg').toContain('__mcpctl_needs_server_arg');
}); });
it('non-project commands do not show with --project', () => { it('non-project commands do not show with --project', () => {
@@ -142,16 +143,18 @@ describe('bash completions', () => {
expect(bashFile).toMatch(/get\|describe\|delete\)[\s\S]*?_mcpctl_resource_names/); expect(bashFile).toMatch(/get\|describe\|delete\)[\s\S]*?_mcpctl_resource_names/);
}); });
it('attach-server filters out already-attached servers using project context', () => { it('attach-server filters out already-attached servers and guards against repeat', () => {
const attachBlock = bashFile.match(/attach-server\)[\s\S]*?return ;;/)?.[0] ?? ''; const attachBlock = bashFile.match(/attach-server\)[\s\S]*?return ;;/)?.[0] ?? '';
expect(attachBlock, 'attach-server must use _mcpctl_get_project_value').toContain('_mcpctl_get_project_value'); expect(attachBlock, 'attach-server must use _mcpctl_get_project_value').toContain('_mcpctl_get_project_value');
expect(attachBlock, 'attach-server must query project servers to exclude').toContain('--project'); expect(attachBlock, 'attach-server must query project servers to exclude').toContain('--project');
expect(attachBlock, 'attach-server must check position to prevent repeat').toContain('cword - subcmd_pos');
}); });
it('detach-server shows only project servers', () => { it('detach-server shows only project servers and guards against repeat', () => {
const detachBlock = bashFile.match(/detach-server\)[\s\S]*?return ;;/)?.[0] ?? ''; const detachBlock = bashFile.match(/detach-server\)[\s\S]*?return ;;/)?.[0] ?? '';
expect(detachBlock, 'detach-server must use _mcpctl_get_project_value').toContain('_mcpctl_get_project_value'); expect(detachBlock, 'detach-server must use _mcpctl_get_project_value').toContain('_mcpctl_get_project_value');
expect(detachBlock, 'detach-server must query project servers').toContain('--project'); expect(detachBlock, 'detach-server must query project servers').toContain('--project');
expect(detachBlock, 'detach-server must check position to prevent repeat').toContain('cword - subcmd_pos');
}); });
it('instances use server.name instead of name', () => { it('instances use server.name instead of name', () => {