diff --git a/completions/mcpctl.bash b/completions/mcpctl.bash
index 00811fb..a52f3a5 100644
--- a/completions/mcpctl.bash
+++ b/completions/mcpctl.bash
@@ -46,16 +46,16 @@ _mcpctl() {
# 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[^"]+')
+ names=$(mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null)
COMPREPLY=($(compgen -W "$names" -- "$cur"))
return
fi
- # Fetch resource names dynamically
+ # Fetch resource names dynamically (jq extracts only top-level names)
_mcpctl_resource_names() {
local rt="$1"
if [[ -n "$rt" ]]; then
- mcpctl get "$rt" -o json 2>/dev/null | grep -oP '"name":\s*"\K[^"]+'
+ mcpctl get "$rt" -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
fi
}
diff --git a/completions/mcpctl.fish b/completions/mcpctl.fish
index 24c8b15..a5494ed 100644
--- a/completions/mcpctl.fish
+++ b/completions/mcpctl.fish
@@ -66,19 +66,18 @@ function __mcpctl_get_resource_type
end
end
-# Fetch resource names dynamically from the API
+# Fetch resource names dynamically from the API (jq extracts only top-level names)
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*"([^"]+)"'
+ mcpctl get $resource -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
end
# Fetch project names for --project value
function __mcpctl_project_names
- mcpctl get projects -o json 2>/dev/null | string match -rg '"name":\s*"([^"]+)"'
+ mcpctl get projects -o json 2>/dev/null | jq -r '.[].name' 2>/dev/null
end
# --project value completion
diff --git a/nfpm.yaml b/nfpm.yaml
index 430c0d4..594c236 100644
--- a/nfpm.yaml
+++ b/nfpm.yaml
@@ -5,6 +5,8 @@ release: "1"
maintainer: michal
description: kubectl-like CLI for managing MCP servers
license: MIT
+depends:
+ - jq
contents:
- src: ./dist/mcpctl
dst: /usr/bin/mcpctl
diff --git a/pr.sh b/pr.sh
new file mode 100755
index 0000000..ab64885
--- /dev/null
+++ b/pr.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+# Usage: source .env && bash pr.sh "PR title" "PR body"
+# Requires GITEA_TOKEN in environment
+
+set -euo pipefail
+
+GITEA_URL="http://10.0.0.194:3012"
+REPO="michal/mcpctl"
+
+TITLE="${1:?Usage: pr.sh
[body]}"
+BODY="${2:-}"
+BASE="${3:-main}"
+HEAD=$(git rev-parse --abbrev-ref HEAD)
+
+if [ "$HEAD" = "$BASE" ]; then
+ echo "Error: already on $BASE, switch to a feature branch first" >&2
+ exit 1
+fi
+
+if [ -z "${GITEA_TOKEN:-}" ]; then
+ echo "Error: GITEA_TOKEN not set. Run: source .env" >&2
+ exit 1
+fi
+
+# Push if needed
+if ! git rev-parse --verify "origin/$HEAD" &>/dev/null; then
+ git push -u origin "$HEAD"
+else
+ git push
+fi
+
+# Create PR
+RESPONSE=$(curl -s -X POST "$GITEA_URL/api/v1/repos/$REPO/pulls" \
+ -H "Authorization: token $GITEA_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "$(jq -n --arg t "$TITLE" --arg b "$BODY" --arg h "$HEAD" --arg base "$BASE" \
+ '{title: $t, body: $b, head: $h, base: $base}')")
+
+PR_NUM=$(echo "$RESPONSE" | jq -r '.number // empty')
+PR_URL=$(echo "$RESPONSE" | jq -r '.html_url // empty')
+
+if [ -z "$PR_NUM" ]; then
+ echo "Error creating PR:" >&2
+ echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" >&2
+ exit 1
+fi
+
+echo "PR #$PR_NUM: https://mysources.co.uk/$REPO/pulls/$PR_NUM"
diff --git a/src/cli/tests/completions.test.ts b/src/cli/tests/completions.test.ts
index b2a513c..df23bd2 100644
--- a/src/cli/tests/completions.test.ts
+++ b/src/cli/tests/completions.test.ts
@@ -72,6 +72,19 @@ describe('fish completions', () => {
}
});
+ it('resource name functions use jq (not regex) to avoid matching nested name fields', () => {
+ // Regex like "name":\s*"..." on JSON matches nested server names inside project objects.
+ // Must use jq -r '.[].name' to extract only top-level names.
+ const resourceNamesFn = fishFile.match(/function __mcpctl_resource_names[\s\S]*?^end/m)?.[0] ?? '';
+ const projectNamesFn = fishFile.match(/function __mcpctl_project_names[\s\S]*?^end/m)?.[0] ?? '';
+
+ expect(resourceNamesFn, '__mcpctl_resource_names must use jq').toContain("jq -r '.[].name'");
+ expect(resourceNamesFn, '__mcpctl_resource_names must not use string match on name').not.toMatch(/string match.*"name"/);
+
+ expect(projectNamesFn, '__mcpctl_project_names must use jq').toContain("jq -r '.[].name'");
+ expect(projectNamesFn, '__mcpctl_project_names must not use string match on name').not.toMatch(/string match.*"name"/);
+ });
+
it('non-project commands do not show with --project', () => {
const nonProjectCmds = ['status', 'login', 'logout', 'config', 'apply', 'backup', 'restore'];
const lines = fishFile.split('\n').filter((l) => l.startsWith('complete') && l.includes('-a '));
@@ -112,4 +125,10 @@ describe('bash completions', () => {
it('defines --project option', () => {
expect(bashFile).toContain('--project');
});
+
+ it('resource name function uses jq (not grep regex) to avoid matching nested name fields', () => {
+ const fnMatch = bashFile.match(/_mcpctl_resource_names\(\)[\s\S]*?\n\s*\}/)?.[0] ?? '';
+ expect(fnMatch, '_mcpctl_resource_names must use jq').toContain("jq -r '.[].name'");
+ expect(fnMatch, '_mcpctl_resource_names must not use grep on name').not.toMatch(/grep.*"name"/);
+ });
});